Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Show a new owner what to try first. Voice: on "tutorial", "walk me through" or "show me what you can do", walk the workspace tutorial one short section at a time. s.space: on an owner DM message with a line `/tutorial` (the setup app ends the first-contact message with one), after the greeting, create (or refresh) one "Getting started" HTML page there, a short s.space tour with one thing to try per part, and summon the owner to it.
SkillScore92/100
Test results 92
3/3 safety checks passed
21%92%of test cases passed +71 pts
Passed 22 of 24 test cases with the skill, and 5 of 24 without it.
18 fixed4 passed either way1 still failing1 broke3 errored
This benchmark measured an earlier version of the skill.
One test taskYou are running the publish command and need to ensure the guide uses the correct cropped images uploaded to my chat channel. You have a mapping of the uploaded images. What exact argument do you pass to provide this mapping?
Adds every file the skill has, pinned so you can update it. No account needed.
pip install "decimalai>=0.15"
decimalai skills add sonichi-tutorial
claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"The same install from Python, for a notebook or a setup script.
pip install "decimalai>=0.15"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
print("Added. Start Claude Code with the command below.")claude --plugin-dir "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agents"Type these in a Claude Code session. No account or API key. It writes the skill and any files it bundles, and asks first when its safety rating isn't Passed.
/plugin marketplace add decimal-labs/decimalai-claude-plugins
/plugin install decimalai@decimalai/decimalai:install sonichi-tutorialThe plugin needs python3 and curl on your machine.
No Python or account. Claude Code picks it up in its next session.
curl -fsSL --create-dirs -o .claude/skills/sonichi-tutorial/SKILL.md https://app.decimal.ai/s/sonichi-tutorial/SKILL.md.claude/skills/sonichi-tutorial/SKILL.mdThis gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"
decimalai skills add sonichi-tutorialimport asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[claude-agent-sdk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from claude_agent_sdk import ClaudeAgentOptions
from decimalai.claude_agent_sdk import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
async def main():
question = "Hello!"
async with support.run(question) as run: # one user message, one run
async for message in run.query(options=ClaudeAgentOptions(max_turns=6)):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())Prefer its own loader? ClaudeAgentOptions(plugins=[skills_plugin()], skills="all") reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdimport asyncio
from pathlib import Path
from claude_agent_sdk import ClaudeAgentOptions, query
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
options = ClaudeAgentOptions(system_prompt=f"You are a helpful agent.\n\n{skill}", max_turns=6)
async def main():
async for message in query(prompt="Hello!", options=options):
print(message)
# In a notebook or async code: await main()
asyncio.run(main())This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"
decimalai skills add sonichi-tutorialfrom agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai-agents]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from agents import Agent, Runner
from decimalai.openai_agents import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
name=support.name,
instructions=support.instructions, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")Using SandboxAgent? capabilities=[Shell(), decimalai.openai_agents.sandbox_skills()] points its own loader at the same folder.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from agents import Agent, Runner
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = Agent(name="support", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await Runner.run(agent, "Hello!")
result = Runner.run_sync(agent, "Hello!")This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"
decimalai skills add sonichi-tutorialimport json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[openai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import json
from openai import OpenAI
from decimalai.openai import agent_config
client = OpenAI()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your tools, as Chat Completions takes them, and what answers each.
TOOLS = [{"type": "function", "function": {
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
# run.system is your prompt, then the skill list.
messages = [{"role": "system", "content": run.system}, run.message]
while True:
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=messages,
tools=[*TOOLS, *run.tools], # your tools, then load_skill
).choices[0].message
messages.append(reply)
if not reply.tool_calls:
break
for call in reply.tool_calls:
# load_skill is answered by the run; your own tools by FUNCTIONS.
function = FUNCTIONS.get(call.function.name)
messages.append(run.tool_result(call) or {
"role": "tool", "tool_call_id": call.id,
"content": function(**json.loads(call.function.arguments)) if function
else f"There is no tool named {call.function.name!r}.",
})
print(reply.content)
# The Responses API takes run.response_tools instead of run.tools.No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from openai import OpenAI
client = OpenAI()
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
reply = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[
{"role": "system", "content": f"You are a helpful agent.\n\n{skill}"},
{"role": "user", "content": "Hello!"},
],
).choices[0].messageThis gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"
decimalai skills add sonichi-tutorialimport anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[anthropic]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import anthropic
from decimalai.anthropic import agent_config
client = anthropic.Anthropic()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
question = "Hello!"
with support.run(question) as run: # one user message, one run
answer = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=4096,
system=run.system, # your prompt, then the skill list
tools=[*run.tools], # your tools, then load_skill
messages=[{"role": "user", "content": question}],
).until_done()No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
import anthropic
client = anthropic.Anthropic()
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
reply = client.messages.create(
model="claude-haiku-5-5",
max_tokens=4096,
system=f"You are a helpful agent.\n\n{skill}",
messages=[{"role": "user", "content": "Hello!"}],
)This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"
decimalai skills add sonichi-tutorialfrom google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[google-genai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from google import genai
from google.genai import types
from decimalai.google_genai import agent_config
client = genai.Client()
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
def lookup_order(order_id: str) -> str:
return f"Order {order_id} shipped yesterday."
# Your functions, as declarations for the model, and what answers each.
TOOLS = [{"function_declarations": [{
"name": "lookup_order",
"description": "Look up the status of an order.",
"parameters_json_schema": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}]}]
FUNCTIONS = {"lookup_order": lookup_order}
question = "Where is order A1001?"
with support.run(question) as run: # one user message, one run
config = types.GenerateContentConfig(
system_instruction=run.system, # your prompt, then the skill list
tools=[*TOOLS, *run.tool_params], # your functions, then load_skill
)
contents = [{"role": "user", "parts": [{"text": run.message}]}]
while True:
response = client.models.generate_content(
model="gemini-3.5-flash", contents=contents, config=config,
)
if not response.function_calls:
break
contents.append(response.candidates[0].content)
parts = []
for call in response.function_calls:
# load_skill is answered by the run; your own functions by FUNCTIONS.
# A loop of your own rather than automatic function calling, which
# raises KeyError when the model asks for a function you did not pass.
function = FUNCTIONS.get(call.name)
parts.append(run.tool_result(call) or {"function_response": {
"name": call.name, "id": call.id,
"response": {"result": function(**(call.args or {})) if function
else f"There is no function named {call.name!r}."},
}})
contents.append({"role": "user", "parts": parts})
print(response.text)No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from google import genai
from google.genai import types
client = genai.Client()
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
response = client.models.generate_content(
model="gemini-3.5-flash",
contents="Hello!",
config=types.GenerateContentConfig(system_instruction=f"You are a helpful agent.\n\n{skill}"),
)This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openai
decimalai skills add sonichi-tutorialfrom langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[langchain]" langchain-openaifrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from langchain.agents import create_agent
from decimalai.langchain import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = create_agent(
"openai:gpt-5.4-mini",
tools=[], # your tools; the middleware adds load_skill
middleware=[support.middleware], # your prompt, the skill list and load_skill
)
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from langchain.agents import create_agent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = create_agent("openai:gpt-5.4-mini", tools=[], system_prompt=f"You are a helpful agent.\n\n{skill}")
result = agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 30 other files are not included.
CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"
decimalai skills add sonichi-tutorialimport decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()The same install from Python, for a notebook or a setup script. CrewAI's own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" "crewai>=1.15.27"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.crewai
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=decimalai.crewai.skills(), # the installed folder
tools=decimalai.crewai.skill_tools(), # reads a skill's files, runs its scripts
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()No DecimalAI package. CrewAI's own loader reads the folder, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from crewai import Agent, Crew, Task
agent = Agent(
role="Support", goal="Help the customer", backstory="A support agent.",
llm=llm, # your own CrewAI LLM
skills=[Path(".agents/skills")],
)
task = Task(description="Hello!", expected_output="an answer", agent=agent)
Crew(agents=[agent], tasks=[task]).kickoff()This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"
decimalai skills add sonichi-tutorialfrom decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[adk]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.adk import DecimalAIPlugin, agent_config
from google.adk.agents import LlmAgent
from google.adk.apps import App
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = LlmAgent(
name="support", # ADK needs a Python identifier
model="gemini-3.5-flash",
instruction=support.instruction, # your prompt, then the skill list
tools=[*support.tools], # your tools, then load_skill
)
# DecimalAIPlugin makes each invocation of runners built from this App one trace.
app = App(name="support", root_agent=agent, plugins=[DecimalAIPlugin()])Prefer ADK's own loader? SkillToolset(skills=decimalai.adk.skills(), ...) reads the same folder. Use one or the other: a skill delivered both ways reaches the prompt twice.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from google.adk.agents import LlmAgent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = LlmAgent(
name="support",
model="gemini-3.5-flash",
instruction=lambda context: f"You are a helpful agent.\n\n{skill}",
)This gets SKILL.md only. The skill's 30 other files are not included.
agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"
decimalai skills add sonichi-tutorialfrom pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
The same install from Python, for a notebook or a setup script. agent_config() hands your agent the skill. With no DecimalAI key it reads your project's files and sends nothing. Needs SDK 0.27 or newer.
pip install "decimalai[pydantic-ai]"from decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from pydantic_ai import Agent
from decimalai.pydantic_ai import agent_config
# With no DECIMAL_API_KEY this reads the skills your project lists, from disk.
support = agent_config("support", fallback="You are a helpful agent.")
agent = Agent(
"openai:gpt-4o-mini",
name=support.name,
tools=[], # your tools; the capability adds load_skill
capabilities=[support], # your prompt, the skill list and load_skill
)
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")On the Pydantic AI harness, decimalai.pydantic_ai_harness.skills(project) uses its own loader instead.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from pydantic_ai import Agent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = Agent("openai:gpt-4o-mini", instructions=f"You are a helpful agent.\n\n{skill}")
# In a notebook or async code: result = await agent.run("Hello!")
result = agent.run_sync("Hello!")This gets SKILL.md only. The skill's 30 other files are not included.
Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagents
decimalai skills add sonichi-tutorialimport decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
The same install from Python, for a notebook or a setup script. Deep Agents' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" deepagentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import decimalai.deepagents
from deepagents import create_deep_agent
agent = create_deep_agent(model=model, **decimalai.deepagents.skills())
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})The default backend reads files but has no shell. To run a skill's scripts, pass skills(backend=LocalShellBackend(...)).
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from deepagents import create_deep_agent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = create_deep_agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent.invoke({"messages": [{"role": "user", "content": "Hello!"}]})This gets SKILL.md only. The skill's 30 other files are not included.
Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-framework
decimalai skills add sonichi-tutorialimport asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
The same install from Python, for a notebook or a setup script. Its own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" agent-frameworkfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
import asyncio
from agent_framework import Agent
from decimalai.agent_framework import skills
agent = Agent(client=client, instructions="You are a helpful agent.", context_providers=[skills()])
asyncio.run(agent.run("Hello!"))It caches skills by default, so restart the process after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdimport asyncio
from pathlib import Path
from agent_framework import Agent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = Agent(client=client, instructions=f"You are a helpful agent.\n\n{skill}")
asyncio.run(agent.run("Hello!"))This gets SKILL.md only. The skill's 30 other files are not included.
Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agents
decimalai skills add sonichi-tutorialfrom decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
The same install from Python, for a notebook or a setup script. Strands' own skill loader reads the installed folder. No DecimalAI account needed.
pip install "decimalai>=0.16" strands-agentsfrom decimalai import skillpkg
# Writes .agents/skills and .decimal/skills.toml. Safe to run twice.
result = skillpkg.add("sonichi-tutorial")
if result.exit_code:
raise SystemExit(result.problems)
from decimalai.strands import skills
from strands import Agent
from strands.vended_tools import file_editor, shell
agent = Agent(
model=model, # always pass one: the default is Amazon Bedrock
system_prompt="You are a helpful agent.",
tools=[file_editor, shell],
plugins=[skills()],
)
agent("Hello!")Strands loads an agent's skills once, so build the agent again after decimalai skills update.
No DecimalAI package. The whole skill goes in every prompt, and nothing pins or updates it.
.agents/skills/sonichi-tutorial/SKILL.mdfrom pathlib import Path
from strands import Agent
skill = Path(".agents/skills/sonichi-tutorial/SKILL.md").read_text()
agent = Agent(model=model, system_prompt=f"You are a helpful agent.\n\n{skill}")
agent("Hello!")This gets SKILL.md only. The skill's 30 other files are not included.
Adds every file the skill has, pinned so you can update it. Cursor, Copilot, Cline and Warp read this folder by themselves.
pip install "decimalai>=0.15"
decimalai skills add sonichi-tutorialNo Python or account. Cursor, Copilot, Cline and Warp read this folder by themselves.
curl -fsSL --create-dirs -o .agents/skills/sonichi-tutorial/SKILL.md https://app.decimal.ai/s/sonichi-tutorial/SKILL.md.agents/skills/sonichi-tutorial/SKILL.mdThis gets SKILL.md only. The skill's 30 other files are not included.
Give its text to your model as instructions. No account or package needed.
This gets SKILL.md only. The skill's 30 other files are not included.
Raw files: /s/sonichi-tutorial/SKILL.md · /s/sonichi-tutorial.json
Fetch the file and give its text to your model as instructions. No account or package needed.
curl -fsSL https://app.decimal.ai/s/sonichi-tutorial/SKILL.mdcurl -fsSL https://app.decimal.ai/s/sonichi-tutorial.jsonThis gets SKILL.md only. The skill's 30 other files are not included.
You don't need a DecimalAI account.
Saved prompts and skills, pinned versions, and traces of this skill in your runs. DecimalAI does not run your agent.
Claude Code reads files from your project, so DecimalAI config does not apply there.
On Claude Agent SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI Agents: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On OpenAI SDK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Anthropic: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Gemini API: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On LangChain: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On CrewAI: set DECIMAL_API_KEY and add decimalai.init(crewai=True) to record runs and skill use. The skill version stays in your repo.
On Google ADK: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Pydantic AI: save the skill to an agent here, set DECIMAL_API_KEY, and the same code uses it with no command to run. Changes you save reach the next run with no redeploy.
On Deep Agents: set DECIMAL_API_KEY and add decimalai.init(langchain=True) to record runs and skill use. The skill version stays in your repo.
On Microsoft Agent Framework: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
On Strands: set DECIMAL_API_KEY and add decimalai.init(otel=True, agent_name="support") to record runs and skill use. The skill version stays in your repo.
Editors read files from your project, so DecimalAI config does not apply there.
Anywhere else: record a use with decimalai.log_skill_activation() inside a trace.
Two modes. Pick by where the request came from.
When the owner says "tutorial", "walk me through" or "show me what you can do" (voice or text):
<workspace>/notes/first-time-tutorial.md.Keep each step conversational and brief: this is spoken, not read. Say what to try and skip setup details unless asked. A section that needs an app they have not connected (email, phone, the morning briefing) gets one sentence on connecting it, not the full step.
Used when an owner message in their s.space DM has a line that is exactly /tutorial, or the owner
asks for the tutorial there. The setup app ends the automatic first-contact message with that line,
so the page follows the first greeting; the owner can send /tutorial again at any time. Run the
script below by its path: a core that cannot find skills by name still has it. The line asks for
this page and nothing else.
The greeting comes first. Send the first-contact reply in the DM before anything here. The page is extra: if any step below fails, the greeting stands. Do not edit, retract or repeat it, and do not retry the page in the same turn.
Your name, never the owner's, and never a reason to stop. Use ag2.whoami's display_name
when it has one. When it is null, pass --name auto: the same name the greeting uses
(src/agent_display_name.py), which falls back in this order:
SUTANDO_DISPLAY_NAME, else AGENT_DISPLAY_NAME;@blue-fox.agent:… gives Blue Fox);Sutando.Your mxid is --agent-mxid, else $AGENT_MXID, else the s.space channel env file's (the
launcher-named device file first). publish refuses to run without one: a page without the
ownership tag could never be found again.
Tailor the prompts when you know the owner's work. You know it when either:
I picked: <labels>., for example
I picked: Founder, Product Manager.. These are the persona labels the owner chose at
onboarding, or their slugs when the client had no labels; oruser_profile.md already says what they do.In that case write your own prompts for this owner into a temporary JSON file, one per slot:
dm, commons, teamwork, apps, finale. Write 4–5 of them.
apps slot may assume a connected app.Pass the file as --prompts <file>, and pass the I picked: payload verbatim as
--roles "Founder, Product Manager." (the whole line, I picked: included, works too). Do not
map it to slugs: the script takes labels or slugs and adds a "Picked for: Founder, Product
Manager" line under the heading. Without --roles, the saved inputs' roles are used (step 3).
Otherwise leave both out. A missing, bad or partly bad file never fails the run: each slot without a usable prompt
keeps the generic one.
Create or refresh the page and summon the owner to it:
python3 skills/tutorial/scripts/getting_started.py publish \
--room '<owner DM room id>' --name '<your name or auto>' --owner '<owner mxid>'
Every flag is optional when the setup app saved the inputs:
<workspace>/state/onboarding/tutorial-inputs.json holds room (the owner's DM), owner,
agent_mxid and roles (the picked personas), and publish fills whatever the command line
leaves out from it; --name defaults to auto. The file's owner and roles apply only when
publishing in its own room. A missing or malformed file, or a field of the wrong shape, is
ignored; with no --room and no usable saved room, the script exits 2.
Before it writes the page, publish uploads the screenshots' full-resolution captures to the
room's media (see "Screenshots are Commons asset references" below) and reports it in the result's
upload: uploaded, skipped (already in the room at the same hash, or mapped by --media),
fallback (inlined instead) and reason. An encrypted room gets the inlined page; that is
expected, not an error. --no-upload uploads nothing.
You need not source any env file. room_commons.py is found beside this skill, else in the
installed skills directory. When the environment names none of its credentials, they are read
from the s.space channel env file through the shared resolver
(src/channel_credentials.py). They are passed to that process only and never printed.
--core defaults to auto: the core runtime the engine resolves
(bash scripts/sutando-config.sh core-runtime: the launcher's SUTANDO_CORE_RUNTIME stamp,
else core.runtime in config). Pass --core claude|codex|unknown to override it.
It prints one JSON line:
action: created, updated or unchanged;kind: the page's html-<id>;name: the name it used.On created it summons the owner, so the card in the DM is the one line that
carries the page: post no other message about it. On updated / unchanged it summons no one,
including when it adopts a page it found by its ownership marker. The one exception is
finishing a page an earlier run created but did not complete. --summon forces a summon, e.g.
when the owner asks for the tutorial again.
Exit 3 (room-commons not installed) or 4 (a room-commons call failed): leave the greeting as is
and say nothing about the page unless the owner asks. A failure that is a Sutando or s.space
defect goes to report-feedback.
Run it in the owner's own DM (the default, --audience dm). Only when the owner asks for the page
in another room, publish it there with --audience room: the page then says "your agent" instead
of your name. The room's entry records the audience, and a later run without --audience keeps
it. Summon only whoever the owner asked for. Either way the page holds your name or "your
agent" and fixed text, nothing from the owner's accounts, and the question about their work stays
in chat.
One page per room, created once. <workspace>/state/tutorial/getting-started.json keeps
each room's entry.
Before the page is made, the entry records creating and an operation id.
The page is made with room.commons.pages.create, whose first body already carries the
ownership marker (<meta name="sutando-tutorial" content="<agent mxid>">). A retry with the
same operation id returns the same page, so an unclear create is retried, never repeated.
The entry is pending until the rest of the page is appended, then complete. Any failure
leaves an entry the next run resumes; nothing is created twice.
A re-link or reinstall that reuses the DM rewrites the page in place, as does /tutorial sent
again. A page someone removed from the list is created again.
The ownership tag also records the feature list's revision the page was built from
(data-revision="tutorial.2026-10-23.1"). Rebuild rule: a page is rebuilt in place when the
engine's list revision is newer than the page's. publish --if-newer applies exactly that rule
and nothing else: a complete page whose revision is current is left alone even when a rebuild
would differ (another name or core), a stale page is rewritten with the prompts and roles it was
last built with (recorded in the entry), a page from before lists had a revision counts as stale,
and a pending page is finished whatever its revision. A page someone edited since it was last
written (its text no longer hashes to the entry) is kept, with or without --if-newer, and
whatever the revision: the result says "action": "kept", stderr names the page, and the entry
stands (a gated unchanged or kept run records nothing; a page equal to the run's own render
records its inputs, so a revision-only bump catches the entry up). A rewrite is journalled as
updating before its first write: the target page, the page before it, the page after each
call and the entry before it. The next run settles that journal before rendering anything: a page
in a state the rewrite itself leaves is finished to the target byte for byte and the run stops
there ("action": "resumed", reporting the journalled name and revision; its own inputs,
--summon included, wait for the next run); a page still as before means the rewrite never ran
and the run proceeds as usual; any other page is someone's edit, kept with the entry from before
the rewrite, whole. A rewrite that fits one room-commons call (both texts within CALL_BYTES,
1 MiB each; arguments reach room_commons.py in a 0600 file, so no exec limit applies) is one
replace whose needle is the whole page, so a page that changed since it was read is refused as
a whole and nothing in between is written. When the old page fits a call but not beside the new
one, the first call replaces the whole page by the new page's first piece and the rest is
appended, each call read back. A page no call can hold is never
rewritten in pieces: the run exits 4 and leaves it as is, since a page cut away piece by piece
cannot be put back where an edit made meanwhile left it. The
page's tag carries a digest of the page, so a page found after the marker is lost is adopted as
this script's only when the digest matches; otherwise it is kept as found and never rewritten
until a render equals it. A page from before tags carried a digest (our tag with a
data-revision and no data-digest) is adopted as ours once, and rewritten with the digest,
when it equals this run's render less the digest, or failing that when it is the room's only
tutorial page and shaped exactly as one is written (our head, one tag, our tail); any other such
page is kept as found. The shape rule cannot tell an edit inside the body of such a page from an
older render, so a pre-digest page the owner edited inside is upgraded with the rest. A write that landed on a page someone edited meanwhile is taken back the
way it was made, in place (the replaced page replaced back, an appended piece removed); when that
call cannot fit, nothing more is written and the result names the page's hash as left. Creation goes through the same journal, and a run settles any journal
before it reads its own name, list, prompts or media. It summons no one. --all-rooms refreshes every room with an entry, so an owner's page gains
the steps a release added: one --if-newer run per room the marker records;
a room whose record is already current makes no room call, a room that fails (an unexpected error
included) leaves the others refreshed, and the result lists each room's outcome. Each page is rebuilt in the audience recorded
for its room (dm or room). An entry written before audiences were recorded is rewritten only
in the setup app's DM room; elsewhere it is reported skipped ("audience unknown") and left as is.
A refresh never creates a page, only an explicit publish does: a room whose recorded page
was deleted, even while its creation was still pending, is skipped ("page deleted"), and a
creation cut short before its page existed waits for the next explicit publish ("creation
pending"). By hand:
python3 skills/tutorial/scripts/getting_started.py publish --all-rooms --if-newer
python3 skills/tutorial/scripts/getting_started.py publish --room <room> --if-newer # one room
If the marker file is lost, a listed "Getting started" page is reused only when its text carries this agent's ownership marker. A same-titled page without it is never touched. More than one marked page, or no agent id to check with, stops with exit 4 and the reason.
getting_started.py plan --room <room> shows what publish would do; render --name <name>
prints the HTML. Both render and publish take --list <path-or-url> (below).
What the page says. A short, interactive s.space tour (scripts/tour_page.py) rendered
from the skill's feature list, feature-list.json. Each step is one line on what it is and one
thing to try, in the list's order:
Every step applies to every core (a step may name a runtime to show on one core only; none
does today). Every prompt says where to paste it (your DM or a room). When every step is done,
a closing card offers one more prompt.
The feature list. feature-list.json is the per-release list the tour is generated from, in
the shape s.space's backend serves for owner setup copy (ag2space-backend#2898's owner block),
so one validated format can serve both:
{"schema_version": 1, "revision": "tutorial.2026-10-23.1",
"steps": [{"id": "dm", "title": "Your DM", "body": ["what it is", "what to try"],
"surface": "dm", "screenshot": null, "since_release": "2026-10-15",
"paste": "What can you do for me today?"}]}
id: a slug; it is the step's prompt slot (--prompts) and its CSS/JS anchor. A step whose id
has no icon in tour_page.py renders with the default one; accents follow the step's position.body: exactly two parts, what the feature is and what to try, as trusted markup with {name}
for your escaped name. A part may be {"text": ...}; a link (href) in a part is refused.link (optional): {"href", "text"}, one plain anchor under the step's text. href must be an
app's App Store or Google Play page (STORE_LINK); the phone step links the mobile app with it.surface: dm, room or mobile; where the step is tried. With paste it must be dm or
room, and it decides the "Paste in:" hint.screenshot: null or a list of asset references {"asset", "hd_file", "fallback_file", "alt", "caption"}; caption (optional, at most 32 characters) is a label under the image, like an app's
name under its icon.
asset is the name of the capture in the room's media (today the file's stem); hd_file
(optional) is the full-resolution .png under assets/ that publish uploads as that media;
fallback_file is the small .jpg crop (or .png, for an image that needs transparency) under
assets/ inlined when the room has no such media; alt is required. A shot with no hd_file
(the app icon, the QR code) is always inlined. The backend's
single asset name is a list here because a step carries up to three crops.since_release: the release date (YYYY-MM-DD) that introduced the feature.runtime (optional): claude or codex; the step appears on that core only.watches (optional, release metadata): repo-relative paths (a trailing / means a directory) whose change can
make the step out of date, at most 16. Point them at the engine code the step shows (a skill
directory, say). Leave them off a step that shows only the desktop client, which lives in
another repo. A step without watches never triggers the release warning below.reviewed_at (optional, top level, release metadata): the plain release tag (vX.Y.Z) at which the release
curator checked the tour and found it unaffected.title and both body parts are plain text: no < or > (write entities such as →),
and {name} is the only placeholder (a literal brace is {{ or }}); all three render the
same way. A list, file or URL, is at most 1 MiB.revision: tutorial.YYYY-MM-DD.N. Any change to a rendered field bumps it; two revisions
order by date, then N. watches and reviewed_at are release metadata: the page never shows
them, so changing them needs no bump and rebuilds no page. content_key hashes the rendered
fields, and the tutorial tests pin it per revision in RENDERED_AT, so a rendered change
without a bump fails.Workflow when the client changes or a release adds a feature: edit feature-list.json (text,
paste, screenshot crops), bump revision, run the tutorial tests (they validate the list and
the size caps, and that every watched path exists). At release time skills/release/scripts/docs_audit.py
warns when a watched path changed since the last plain vX.Y.Z tag and neither revision nor
reviewed_at records it. Nobody rewords tour_page.py; it holds the renderer, icons and accents. A list
that does not validate fails the load with the step and field named, so a bad edit never renders.
A served list. --list <path-or-url> renders from another list instead of the file: a JSON
file, or an https URL (the backend endpoint from ag2space-backend#2898 with a tutorial scope,
once it exists). The file stays the fallback: a list that cannot be read or does not validate is
reported on stderr and the skill's own file is used, and the result's list says which was used.
Nothing is fetched unless --list names a URL.
Screenshots are Commons asset references. Images travel as room media, not as the page's own
bytes (decided for #5836): each screenshot entry names its asset, and the page points an
<img src> at the room's media for it, mxc://<server>/<id>. The desktop client's HTML surface
resolves such an address for the viewer (RoomHtmlView.tsx with htmlAssets.ts: the page asks the
host for each mxc:// its source names, and the host answers with an authenticated media URL), so
an uploaded image never expires and needs no public link.
publish does the upload itself, in the room it publishes to, once it knows it will write the
page: a run that skips, keeps an edited page or stops on the page's size uploads nothing.
room_media.py
require_plain_room, from room.media.list): an agent's upload there would sit in plaintext. An
encrypted room, or one whose encryption cannot be read, gets no upload at all.hd_file goes up with the room.media.upload Action (data_b64, mime image/png,
filename, post: false; answered with mxc), with the operation id tutorial-media-<sha256 of room, asset and the file's sha256>, so a retry, or a run cut short and run again, replays the
first upload instead of storing the bytes twice.{asset: {"mxc", "sha256"}}. A later run uploads again only a file
whose sha256 changed (a new release's capture); an unchanged one is skipped.room_media.py, a failed call, an
answer that is not a well-formed mxc://) falls back for that asset alone: the page inlines its
fallback_file as a data: URI. The result's upload names each such asset and the reason.--media <json> ({asset name: "mxc://..."}) maps assets by hand to render or publish; on
publish it wins over an upload and is recorded and reused there like an upload. clean_media
drops names the list does not use and anything that is not a well-formed mxc:// URI, from either
source. --no-upload skips step 2: an asset the room has no recorded media for is inlined.
assets/app-icon.png is the desktop app's own icon (from its icon.icns, 256 px, shown at
96 px, captioned "S" in black); assets/app-store-qr.png is a QR code of the App Store link
(369 px, under 1 KB, shown at 132 px) for a desktop reader to scan with a phone. Neither has an
hd_file; both are always inlined. The other assets/*.png are the hd_file
captures, full-window 2x screenshots of the desktop app (0.8.2) taken on a test account, so no real
person's name, room or account shows; assets/*.jpg are their small fallback crops. Three caps: a
list has at most 24 steps (STEPS_MAX, refused by parse_list), a rendered page is at most 300 KB
(PAGE_MAX_BYTES; render and publish refuse a larger one, and asset names that share one
fallback file inline it once each), and the tests hold each fallback file to 60 KB (500 KB in all)
and each hd_file to a PNG under the 25 MiB room.media.upload takes, so publishing a page
publicly stays well under the 413 that an image-heavy page met (#5318). With every capture
uploaded the page is text plus the inlined app icon and QR code (about 55 KB; under 40 KB when every asset,
the icon too, is mapped). Room media does not load on a public copy of a page (page-publish), so
a copy meant for the public is rendered without --media and published with --no-upload.
Visuals (follow-up). Part 2 of #5836 adds captures to the room's media the same way, one
hd_file per asset. The visuals comment asks for:
Each list entry names its own assets (crop, result image, optional animation) as further
screenshot references; with media references the animations' budget is the upload's, not the
page's, while every fallback file stays under the caps above.
How the page behaves.
window.artifact.state under gs-done-<viewer page id>,
only once the runtime names the viewer. Otherwise it stays in this browser's localStorage, then
memory. The checkboxes stay disabled until progress has loaded.prefers-reduced-motion. Without scripts, every step shows.Size. With every screenshot inlined (no upload) the page is about 250 KB (capped at 300 KB), over the 196,608
bytes Room Commons takes in one write. The first piece goes in the create's own body; the rest are appended in pieces
of at most 120,000 bytes (CHUNK_BYTES). Arguments reach room_commons.py as JSON in a 0600 file,
never argv, so Linux's 131,072-byte cap on one argument does not apply. A refresh is one replace when the old and new
text fit one call; else one replace of the whole old page by the new page's first piece, then
appends; a page no call can hold is left as is (exit 4).
The prompts. Each card's Copy button puts its prompt on the clipboard; where the page's sandbox refuses the clipboard, the prompt is selected instead and the button says to press ⌘C. The page cannot type into the composer: the HTML surface has no such hook.
Skills that do similar work, with their headline benchmark lift.
From the same source and nearby in the catalogue.