Sub-Agents
Delegate to focused, prompt-defined assistants — the power of custom agents with no code required.
- What Sub-Agents Are
- Your First Sub-Agent
- The Frontmatter Reference
- Registering in config.json
- Macros
- Conversation Visibility
- Sub-Agents vs. Context Search
- Testing with @ Commands
- Agent Grants
- Default Data for Private Instances
- The Model Policy
- Token Caps and Metering
- The Sub-Agent Builder
- Generate and Polish
- Size and Cost Warnings
- Security and Trust
- Sub-Agents in the Players
The previous chapter ended with custom agent files: JavaScript modules that give your character tools, memory, and state. They're powerful, but they ask you to be a programmer. Sub-agents are the other half of the answer. A sub-agent is a snippet of system prompt with a description — a markdown file, nothing more — that registers with the primary model exactly the way a coded agent does. The model sees a tool. When it calls that tool, instead of running JavaScript, Studio makes a second, separate model call with the sub-agent's own prompt, its own context, and its own tools, then hands the answer back to the primary model as the tool result. The narrator asks; the specialist answers; the story continues.
Why is a second model call worth having? Because context is a budget and attention is a resource. A lore file too large to keep in the narrator's system prompt can live entirely inside a sub-agent that is consulted only when needed. A judgment call that deserves a clean head — "which of these names has not been used yet?", "does this contradict the established timeline?" — gets a model that sees only that question and the reference material it needs, not two hundred turns of roleplay. And with agent grants (later in this chapter), a sub-agent can hold private state the narrator structurally cannot read — which makes secrets, hidden NPC motives, and game-master knowledge possible for the first time.
There's a natural authoring ladder here, and we built each rung deliberately. Plain instructions in the system prompt are rung one. Built-in agents are rung two. Sub-agents are rung three: authored in prose, as powerful as many coded agents, and buildable entirely from a form. Custom JavaScript agents remain the top rung for logic that genuinely needs code. Most authors will never need that top rung once they've climbed this one.
What Sub-Agents Are
A sub-agent is a single file in your project's agents/ directory with a .subagent.md extension. It has two parts: YAML frontmatter between two --- fences, holding the registration metadata; and everything below the closing fence, which is the sub-agent's system prompt. When the sub-agent is called, the prompt body (after macro expansion) becomes the system message, and the request — whatever the primary model passed as the tool argument — becomes the user message.
---
description: >
Finds a name from the curated name list that has not yet been used
in this story. Call this when a new character needs naming.
history: none
maxTokens: 400
---
You are a naming assistant. Using the name database tool, retrieve
candidate names. Using the history search tool, verify which candidates
have never appeared in the conversation. Recommend the best unused name
and briefly say why it fits. If every candidate has been used, say so.
From the primary model's point of view there is no difference between this and a coded agent: it sees a tool named after the instance, with the description as its advertisement, and calls it like any other tool. The description is the sub-agent's triggering contract — it is the model's entire basis for deciding to delegate. Write it clear and a little pushy: what this sub-agent knows, and when to call it. A weak description produces a sub-agent that is never consulted; that's the single most common authoring mistake.
Sub-agent files sit in the same agents/ directory as JavaScript agents, register through the same config array, and dispatch through the same broker. The split in the UI — a Sub-Agents tab group separate from the Agents tab group — is purely by file kind. Everything you know from Chapter 7 about enabling, stores, defaults, and the debug log carries over.
Your First Sub-Agent
Open the Sub-Agents tab and click the + button. This opens the Sub-Agent Builder — a form that writes both the .subagent.md file and the config.json entry for you, so you never have to touch either by hand. Fill in a name, a description, and a prompt body, click Save, and the sub-agent is live immediately: the tool is registered, and an @ command is ready for testing.
If you prefer working in the file directly, every sub-agent opens as an ordinary editor tab in markdown mode. Click a sub-agent's tab and a pencil (✎) appears in the tab itself — click it to reopen that file in the builder with everything prefilled. Files you create outside Studio (or drag into the tab row) get the same treatment; new files are scaffolded with commented-out optional fields, and every commented line in the scaffold is written so that deleting the leading # produces a valid value.
Test it instantly: type @yourname some question in the chat input. The sub-agent runs directly — no primary-model round trip, no tokens spent on the narrator — and prints its answer as a dim log entry in the chat. This is the authoring loop: edit the prompt, @ it again, repeat. Because of Live Edits — Studio's standing rule that an open editor is the source of truth — your unsaved changes take effect on the very next call; no save, no reload. (If the editor is clean, the file is re-read from disk at call time, so edits made in an external editor are hot too.)
The Frontmatter Reference
Every field except description is optional, and omitted fields take the defaults below. The builder writes only the fields that differ from their defaults, which keeps the files short and readable.
| Field | Default | What it does |
|---|---|---|
id | filename stem | Instance ID for the shorthand registration form. marcus.subagent.md → id marcus, tool marcus, command @marcus. Must not begin with studio. |
description | required | The tool description the primary model reads. The triggering contract — the model's entire basis for delegating. |
params | single request string | JSON Schema for the tool parameters. The default — one free-text request — is right for nearly every sub-agent. |
history | none | How much of the live conversation the sub-agent sees: none, recent:N, or full. See Conversation Visibility. |
contextFiles | [] | Dot-prefix context files included whole in the sub-agent's prompt, in order. See Macros for placement. |
model | worker | worker or main — never an explicit model ID. See The Model Policy. |
maxTokens | 1024 | Cap on each answer's length (1–8192). Keep it small — the answer is consumed by the calling model, not read by a person. |
maxRounds | 5 | Inner tool-round cap (1–10). Only meaningful when the sub-agent has grants. |
temperature / topP | worker defaults | Per-instance sampling overrides — e.g. temperature: 0.2 for a factual lookup. |
debug | false | Enables the standard Agent Log: a per-call cost summary (prompt size, history tier, model, tokens, duration), a per-round entry for each inner-loop model call, and every granted-tool execution. See Size and Cost Warnings. |
Frontmatter supports inline # comments after values, and the parser is forgiving of hand-authoring wobble. The Monaco editor helps too: inside a .subagent.md frontmatter block you get field completions, value completions for history and model, and your project's actual context files completing inside contextFiles.
One principle worth stating because it explains several behaviors below: frontmatter is behavior; config is wiring. The frontmatter describes how the sub-agent acts. The config entry declares that an instance exists, binds its data, and may override any frontmatter field per instance. Grants — connecting a sub-agent to other agents — are config-only, because declaring instances and wiring them together is config's job everywhere else in Studio.
Registering in config.json
The simplest registration is a bare string in the agents array, exactly like enabling a built-in:
{
"agents": [
"studio.memory-store",
"studio.history-search",
"marcus.subagent.md"
]
}
Every feature has a sensible default, so a sub-agent whose frontmatter says everything needs nothing but its filename. The rule for when you need more — we call it the graduation rule — is simple: the shorthand covers any sub-agent fully described by its frontmatter. The moment an instance needs wiring — grants, per-instance overrides, or instantiating one prompt file twice — it graduates to the object form:
{
"agents": [
"studio.memory-store",
"studio.history-search",
{ "id": "eve.baby-names", "implementation": "studio.agent-db",
"contextFile": ".baby-names.json" },
{ "id": "eve.find-unused-baby-name", "implementation": "studio.sub-agent",
"promptFile": "find-unused-name.subagent.md",
"shared-agents": ["studio.history-search", "eve.baby-names"] },
{ "id": "eve.marcus", "implementation": "studio.sub-agent",
"promptFile": "marcus.subagent.md",
"history": "recent:10",
"model": "main",
"private-agents": ["studio.memory-store"] }
]
}
Notes on the object form:
promptFileresolves relative to the project'sagents/directory.- Tool names derive from instance IDs the same way as composable agents:
eve.find-unused-baby-name→find_unused_baby_name. - Any frontmatter field can be overridden per instance.
descriptionoverrides matter especially when one prompt file is instantiated multiple times — the description is the only thing the primary model has to tell the instances apart. - A config
contextFilesreplaces the frontmatter list entirely — override semantics, not merge. If you want both files, list both. - When a config entry overrides frontmatter fields for a file you have open in the editor, an info bar above the editor names the instance and the shadowed fields, so you know an edit to that field won't take effect for that instance.
If you open a sub-agent file that isn't registered in config yet, the same amber notice bar appears as for JS agents — with an Enable it button that writes the config entry for you.
Macros
Four macros expand in the prompt body at call time:
| Macro | Expands to |
|---|---|
%%CONTEXT%% | The effective contextFiles list, concatenated in order, whole-file. |
%%CHARACTER_INSTRUCTIONS%% | The character instructions — for sub-agents that should be grounded in the character definition. Off by default: most sub-agents should not inherit the persona. |
%%HISTORY%% | Explicit placement of the conversation slice when history ≠ none. |
%%STORE:ref%% | The current JSON contents of a granted agent's store. See Agent Grants. |
Two placement rules are worth knowing. If you declare contextFiles but never write %%CONTEXT%%, the content is automatically prepended above your prompt body — declaring a context file always has an effect, and reference-above-instructions is the stronger default because models weight the end of the prompt most heavily. Write the macro only when you want to control placement yourself. Similarly, if history is enabled but %%HISTORY%% is absent, the slice is delivered as ordinary conversation messages instead of inline text — which is usually what you want.
Conversation Visibility
What a sub-agent sees determines what it is:
history | The sub-agent is a… | Cost | Use it for |
|---|---|---|---|
none (default) | Consultant — sees only the request, its context files, and its granted tools | Cheapest, most predictable | Lore lookup, list cross-checks, name generation, format conversion |
recent:N | Observer — plus the last N conversation messages | Moderate | Scene-aware NPC replies, continuity checks |
full | Auditor — the whole conversation | Expensive | Deep continuity audits, whole-story analysis |
none is the default because it is the honest default: with a clean context, a sub-agent's answers are a function of its prompt and its request, which makes it testable with @ commands and portable across conversations. Opt into visibility only when the job requires it. And note a useful middle ground: a none sub-agent with a shared studio.history-search grant doesn't carry the conversation, but it can query it — often the cheapest way to be history-aware.
History slices are sanitized the same way the agent bridge's history API sanitizes them — roles and content only, with tool-call plumbing and log entries filtered out. A full history that exceeds the sub-agent model's window is truncated oldest-first, with the truncation noted in the injected text so the sub-agent doesn't mistake the window for the whole story.
history: full re-sends the whole conversation to your configured API on every call. That's no different in kind from a normal turn — but it multiplies payload, and it does so every time the narrator delegates. Reach for recent:N or a history-search grant first.
Sub-Agents vs. Context Search
Composed sub-agents and studio.context-access are two answers to "make this file available to the model," and choosing between them is easy once you see the distinction:
studio.context-accessis grep. A narrow question goes in; matching excerpts come out. It's cheap, it scales to huge files, but the model only ever sees fragments.- A sub-agent with a context file is comprehension. The entire file sits in the sub-agent's context, so it can answer questions that require having read everything: "which of these hasn't been used?", "does anything here contradict X?", "pick the best three and say why." Excerpt search structurally cannot answer these.
Whole-file inclusion is the point — and also the cost, since the file is re-sent on every call. The size warnings make that cost visible before it becomes painful. When your file is large and your questions are narrow, compose a context-access instance. When your questions need the whole picture, use a sub-agent — or do both: grant a context-access instance to a sub-agent, and it can search a huge file on demand while keeping its own context small.
Testing with @ Commands
Every sub-agent auto-registers a direct command named after its tool: @marcus how do you feel about the player? runs the sub-agent immediately — no primary-model round trip — and prints the response as a dim, persistent log entry in the chat. Direct calls honor grants, so a sub-agent can use its granted tools during an @ invocation exactly as it would mid-conversation. Combined with Live Edits, this is the tight iteration loop sub-agents were designed around: tweak the prompt, @ it, read the answer, repeat.
Direct invocation is a developer and authoring tool, and it deliberately does not exist in the players. They keep the command machinery internally, but no player UI exposes it — in a player, sub-agents are reached exclusively through the model. Don't build character behavior that depends on users typing @ commands.
Agent Grants
Here is where sub-agents stop being clever prompt injection and become something genuinely new. A sub-agent can be granted access to real agents — the built-ins from Chapter 7, composable instances, even your custom JS agents. During the sub-agent's model call, the granted agents' tools ride along, and the sub-agent can call them in an inner tool loop: look something up, store something, roll dice, search the conversation — then answer the narrator with the result.
Grants come in two flavors, and the difference between them is the difference between the two archetypes this system was designed for.
shared-agents — the lens
"shared-agents" entries are references: each names an instance ID that must already exist in the top-level agents array (a reference to anything unregistered is a load-time error, not a silent no-op). The sub-agent's calls carry those instances' tools, operating on the same state the primary model sees. A shared store grant is real shared state — the sub-agent's writes are visible to the narrator, which is sometimes exactly the point.
The worked example from the top of this chapter is a lens. find_unused_baby_name is granted studio.history-search and a studio.agent-db instance bound to a name database — both shared, both the same instances the narrator could use. What the sub-agent adds is focus: a clean context whose only job is to cross-reference the database against the conversation and return the best unused name. The narrator gets one tool call's worth of an answer that would otherwise have taken several rounds of its own attention, tangled in the middle of narrating a birth scene.
private-agents — the confidant
"private-agents" entries are declarations, using the full top-level syntax — strings for built-ins, objects for composed instances. Each one is instantiated fresh and private to the sub-agent, with its store namespaced under the sub-agent's ID:
conversation_01.studio.memory-store.agent.json ← the narrator's memories
conversation_01.eve.marcus.studio.memory-store.agent.json ← what Marcus knows
Privacy here is structural, not cosmetic. A hidden private instance is excluded from the primary model's tool list, from preload injection, and from the @ command dialog. The narrator model cannot read Marcus's memory because no mechanism exists by which it could — the tools are simply never offered to it. This is what makes an NPC with genuine secrets possible: Marcus can know things about the player, hold grudges, remember promises — and the narrator learns them only when Marcus chooses to say them out loud.
Composed instances work in the private list too. Here's a real configuration — a lorekeeper with a private context-access instance bound to a reference file the narrator's context assembly never touches:
{ "id": "lorekeeper", "implementation": "studio.sub-agent",
"promptFile": "lorekeeper.subagent.md",
"private-agents": [
{ "id": "swords", "implementation": "studio.context-access",
"contextFile": ".swords.txt" }
] }
The private instance's full ID becomes lorekeeper.swords — keep the local id short, since the namespace prefix is added for you. One current limitation to know about: the builder preserves composed private entries like this one when it round-trips your config, but it can't yet author them from the form — write them in the Config tab for now.
The inner loop, bounded
Three constraints keep grants predictable and affordable:
- Code tools only. A sub-agent never appears in another sub-agent's grant lists — this is rejected at config load. Granted tools execute in the sandbox at effectively zero token cost, and model-call depth is capped at one by construction. There are no sub-agent pyramids.
- A round cap. Each inner tool round re-calls the sub-agent's model, and that's real tokens.
maxRounds(default 5, max 10) bounds the loop; on hitting the cap, the sub-agent's last response returns to the narrator. Graceful degradation, never a hang. - One shared meter. Every round counts against the same token caps as everything else. There is exactly one budget.
A sub-agent with grants needs a tool-capable model; if the effective model is known not to support tool calls, the agent is skipped at registration with a toast explaining why. A sub-agent without grants never sends a tools array, so even a small non-tool-calling worker model can run it.
Reading and writing state
The natural pattern for stateful sub-agents is read by injection, write by tool call. Writing %%STORE:memory-store%% (or %%STORE:swords%% — the reference resolves against the sub-agent's own grants, private instances first, matching by ID suffix, full ID, or implementation) injects the granted store's current JSON into the prompt at call time. Marcus starts every call already knowing what he knows, without spending a tool round on recall_memory. Writes go the other way — through the granted store's ordinary tools in the inner loop — so every state change is a real, auditable tool call in the debug log.
You can watch all of this happen. In the chat, a sub-agent's collapsed activity entry expands to show its inner tool calls one level deep — Marcus rolled a d20, Marcus stored a grudge — and the whole block has a copy button that produces a clean plain-text transcript for bug reports. In the Agent Stores panel, sub-agents appear as expandable nodes containing their private instances' stores, with the same rich editors as top-level agents; shared grants render as links to the one canonical row rather than duplicate editors — one store, one editor, ever.
Default Data for Private Instances
Default agent data extends to private instances without modification — the default file is simply the sidecar name minus the conversation prefix, in the project root, git-tracked:
conversation_01.eve.marcus.studio.memory-store.agent.json ← live sidecar
eve.marcus.studio.memory-store.agent.json ← default, shipped with the character
This is what makes the confidant pattern shippable. Playtest until Marcus knows what Marcus should know. Open the Agent Stores panel, expand Marcus's node, and click Set default on his memory store — same gesture as any top-level agent, one more level of indentation. The snapshot opens in the store's Default (shipped) section, where you prune the playtest noise before committing. Every recipient's Marcus then arrives already holding his secrets — privately, in a file the narrator model structurally cannot read.
The Model Policy
A sub-agent's model field accepts exactly two values. worker (the default) runs it on your configured worker model — right for most jobs: lookups, cross-checks, conversions, referee calls. main runs it on the conversation's main model — for NPC brains where prose quality is the product and the worker model is too weak.
There is deliberately no way to name an explicit model ID, and this is worth explaining because it's a principle that runs through everything in this chapter. Sub-agents ship with characters, and characters run on the recipient's API key. A distributed character that could pin a model would be spending someone else's credits on a model they never chose — an author could quietly select the most expensive model available. worker and main don't have this problem: both are indirections into the user's own settings. An author chooses between the user's configured models, and can never escalate beyond them. An unrecognized value falls back to worker with a registration warning.
Token Caps and Metering
Everything a sub-agent does — the first model call and every inner-loop round — runs through the same brokered call path and counts against two caps you control in Settings: a per-response cap (tokens agent-initiated calls may spend within a single narrator turn) and a per-conversation cap. When a cap is exhausted, sub-agent calls fail gracefully with a descriptive result to the narrator ("could not answer: budget exhausted") rather than hanging or erroring the turn.
What's metered follows one rule: chains rooted in a model call meter; chains rooted in your own action don't. When the narrator model decides to consult a sub-agent mid-response, that spend is metered — the model chose it, so the caps bound it. When you type @marcus, that call is unmetered — you chose to spend, exactly as if you'd sent a message. The caps live in your Settings only; a character's config.json can never raise them. They are the enforcement backstop behind a simple promise: a character author can never spend your tokens in a way you didn't choose or can't see.
The Sub-Agent Builder
The builder is the form face of everything above, aimed at authors who never want to see YAML or JSON. The + button in the Sub-Agents tab opens it in create mode; the ✎ pencil in the active sub-agent's tab opens it in edit mode with the frontmatter and config grants prefilled.
The form covers: name (locked when editing) and description, the prompt body, the history dropdown with its N input, the worker/main model choice, the answer cap, a context-file checklist showing each file's estimated token size (amber when a file is large enough to matter), a shared-agents checklist of the project's top-level instances with their granted tool names, a private-agents picker, and — appearing only when grants are selected — the inner round cap.
Save writes both artifacts: the regenerated frontmatter into the .subagent.md, and the config entry — a shorthand string when the entry would carry nothing but a filename, the object form only when wiring exists, per the graduation rule. The open editor buffer is refreshed so Live Edits can't serve a stale copy, and the agents reload so the sub-agent is live immediately. One tradeoff to know: because Save regenerates the frontmatter, hand-written # comments in the frontmatter are not preserved through a builder round trip. (Comments in the prompt body are untouched.)
Generate and Polish
The Generate and Polish buttons know about sub-agents. Anywhere in the Sub-Agents group — even before the first file exists — Generate opens a dedicated dialog: give it a name and describe what the sub-agent should do, and the worker model writes a complete new file, frontmatter and prompt body both. The generator is taught the whole format, and it receives a names-only inventory of your project — context files it may bind, and the agents and sub-agents that already exist — so it references real files and carves out territory that doesn't overlap an existing agent's job. The new file opens straight into the builder, prefilled — review the description and prompt, add grants if you want them, and Save registers it in one motion.
Polish on a sub-agent tab scopes itself to the prompt body only — the frontmatter is never sent to the model or altered. The action list adapts too: alongside Fix English & Punctuation you'll find Tighten the Prompt (imperative voice, no redundancy — every token of the body ships on every call, so shorter is cheaper) and Harden Against Invention (adds the answer-only-from-reference guardrails that keep a lorekeeper from inventing lore). Both preserve %%MACRO%% tokens exactly.
Size and Cost Warnings
Because whole-file context is re-sent on every call, Studio surfaces cost before you pay it, in three places — all warn-never-block:
- At registration — a sub-agent whose prompt plus context files exceeds the size threshold (default ~24k estimated tokens, adjustable in Settings) logs a warning and shows a one-time toast: "eve.lorekeeper sends a ~40k-token file with every call."
- While authoring — the builder's context-file rows show per-file token estimates inline, red when they cross the threshold.
- Per call — with
debug: true, the Agent Log records every number you need to tune cost. The summary entry for each invocation shows the expanded prompt size (chars and estimated tokens), the history tier, the model lane and the actual model it resolved to, the answer cap, the tool-round count, total tokens (marked unmetered when you triggered the call yourself), and duration. When grants are in play, each inner round additionally logs its own entry — which tools the model requested, that round's tokens, finish reason, and duration — because every round is a separately-priced model call, and a per-invocation total can hide which round is eating the budget. Granted-tool executions log too, so the full chain is auditable.
Security and Trust
Sub-agents are strictly lower-risk than custom JS agents. No code executes — there is nothing to sandbox. There is no network access beyond brokered model calls to your own configured endpoint, with your key, under your caps. There is no model escalation, and no cross-agent access beyond declared grants — everything a sub-agent can see and touch is visible in the config and in the builder. For these reasons, characters that ship only sub-agents (no .js agents) do not trigger the unverified agent code banner.
Two behaviors deserve plain statement rather than a warning dialog. First, history: full re-sends your whole conversation to your configured API on every call — nothing new in kind, but multiplied. Second, a shared store grant means the sub-agent can write state the narrator reads. That's a feature — a lens contributing back to shared knowledge — but it's a deliberate one, and it's why the builder makes the shared/private distinction explicit rather than defaulting everything to shared.
Sub-Agents in the Players
Every player runs sub-agents with full parity: frontmatter parsing, macros, history tiers, grants, private instances, the inner loop, and the token caps all come from the same shared code Studio uses, so the runtimes cannot drift. A character whose narrator depends on find_unused_baby_name works identically wherever it runs.
Packaging is automatic: .subagent.md files, the sub-agent engine, and default .agent.json files (including private-instance defaults) all flow through the existing shareable export with no extra steps. The differences in a player are the ones you'd expect from this chapter: there is no @ command UI (model-path only), and the token caps come from the player user's own settings — never from anything shipped inside the character package.