← Table of Contents
Chapter 8

Sub-Agents

Delegate to focused, prompt-defined assistants — the power of custom agents with no code required.

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-agents and true agents live together

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.

FieldDefaultWhat it does
idfilename stemInstance ID for the shorthand registration form. marcus.subagent.md → id marcus, tool marcus, command @marcus. Must not begin with studio.
descriptionrequiredThe tool description the primary model reads. The triggering contract — the model's entire basis for delegating.
paramssingle request stringJSON Schema for the tool parameters. The default — one free-text request — is right for nearly every sub-agent.
historynoneHow 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.
modelworkerworker or main — never an explicit model ID. See The Model Policy.
maxTokens1024Cap on each answer's length (1–8192). Keep it small — the answer is consumed by the calling model, not read by a person.
maxRounds5Inner tool-round cap (1–10). Only meaningful when the sub-agent has grants.
temperature / topPworker defaultsPer-instance sampling overrides — e.g. temperature: 0.2 for a factual lookup.
debugfalseEnables 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:

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:

MacroExpands 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 historynone.
%%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:

historyThe sub-agent is a…CostUse it for
none (default)Consultant — sees only the request, its context files, and its granted toolsCheapest, most predictableLore lookup, list cross-checks, name generation, format conversion
recent:NObserver — plus the last N conversation messagesModerateScene-aware NPC replies, continuity checks
fullAuditor — the whole conversationExpensiveDeep 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.

Cost note

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:

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.

The @ command UI is Studio-only

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:

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:

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.