Agents
Each directory under agents/ at the repo root (a sibling of amodal/) defines one agent. A named agent can root a conversation or run as a delegated specialist. The directory name is the agent's identifier.
agents/
├── main/
│ └── AGENT.md
└── compliance-checker/
├── AGENT.md # prompt, required
├── agent.json # configuration, optional
├── agent.ts # configuration with predicates, optional
└── CONTEXT.md # per-turn prompt, optionalA directory without AGENT.md is ignored.
Reserved Names
| Name | Purpose |
|---|---|
default | Implicit conversation root when a chat request omits agent. Its resource declarations apply. |
main | Operating instructions for unscoped chat when no authored default exists. Supports agents/main/AGENT.md or agents/main.md. |
simple | Loaded from agents/simple/AGENT.md or agents/simple.md, but no runtime path reads it. Use a named agent with modelTier. |
Unscoped chat inserts main into the compiled system prompt alongside platform instructions and resource descriptions. A basePrompt in amodal.json replaces the compiled prompt instead.
agents/main/agent.json can opt unscoped chat into file tools and propose_source_change through tools. Its other fields are ignored, so main does not restrict resources. The file is read only when agents/main/AGENT.md exists and is not empty.
Rooting A Conversation
Pass the directory name in the chat request's agent field:
{
"message": "Review transactions TXN-001 through TXN-050.",
"agent": "compliance-checker"
}The root agent's prompt replaces the main operating instructions. Its tools, skills, connections, stores, mcp, and subagents declarations select the available resources. Files under amodal/ still load as repo content, but the session only exposes the names the agent lists. The runtime still supplies chat protocol tools such as stop_execution and ask_choice.
- An unknown
agentname fails the request. It does not fall back to the default chat. - When
agentis omitted, chat usesagents/default/if it exists. Without it, chat is unscoped and exposes every bundle resource. - An end user's autonomous run of
mainusesagents/default/the same way chat does. Operator callers keep the unscoped surface. - A later turn on the same session that names a different
agentre-roots the session. The transcript is kept, and the runtime inserts an[Agent handoff] <from> -> <to>user message that lists the tools the new agent lacks. - The active root and
defaultare excluded from the callable specialist list.
Skill entries use the loaded skill name, not the folder name. The # Skill: heading wins, then frontmatter name. An amodal/skills/craft-greeting/SKILL.md that starts with # Skill: Craft Greeting is referenced as "Craft Greeting". Store entries produce tools named store__<name>__<op>, with hyphens in the store name replaced by underscores.
agent.json
{
"name": "Compliance Checker",
"description": "Check transactions against the supplied compliance policy.",
"tools": ["load_knowledge", "store__research_notes__query"],
"stores": { "research-notes": "read" },
"maxToolCalls": 15,
"modelTier": "advanced"
}AGENT.md holds the prompt. If it has a ## Prompt section, that section is the prompt; otherwise the whole file is.
| Field | Default | Meaning |
|---|---|---|
name | Directory name | Display name. The directory name remains the invocation identifier. |
description | name | Description shown in the specialist list. |
tools | [] | Explicit tool names. An agent gets only the tools it names. |
skills | None | Skills included when the agent roots a session. Each adds its allowedTools. |
connections | None | REST connections for a root session. Native OpenAPI specialists also prepare their own declared connections. |
mcp | None | MCP servers exposed when the agent roots a session. |
subagents | None | Specialists callable from a session rooted in this agent. |
stores | None | Per-store "read" or "rw" access for a root session. read grants get, query, and list; rw adds set and remove. |
maxToolCalls | 10 | Turn limit for a delegated run. One turn may call several tools. Positive integer. |
modelTier | Session model | simple, default, or advanced. See Model Tiers. |
maxDepth, timeout, targetOutputMin, and targetOutputMax are validated but have no runtime effect. State the required output format in the prompt.
List entries accept a name or {"name": "..."}. Store entries accept "read", "rw", or {"mode": "read"}. A conditional key in agent.json is a load error.
Configuration In AGENT.md
Without agent.json or agent.ts, AGENT.md can carry configuration in YAML frontmatter. Use displayName for the display name. The prompt is the body after the frontmatter and must not be empty.
---
displayName: Vendor Lookup
description: Look up vendor records and summarize contract status.
tools: [request]
maxToolCalls: 10
modelTier: simple
---
Query the vendor system for the named vendor. Return the company name,
contract status, source record, and any missing information.A ## Config YAML section is not parsed.
agent.ts
A sibling agent.ts accepts the same fields as agent.json and adds synchronous predicates to resource entries. It needs no Amodal package dependency:
const isManager = (ctx: { claims: Record<string, string> }) => ctx.claims['role'] === 'manager';
const isJoint = (ctx: { context: Record<string, string> }) => ctx.context['mission_type'] === 'joint';
export default {
tools: ['review', 'check_conflicts'],
skills: ['request-review', { name: 'joint-review', conditional: isJoint }],
subagents: ['prescreen', { name: 'adjudicator', conditional: isManager }],
stores: {
requests: 'read',
decisions: { mode: 'rw', conditional: isManager },
},
};List entries accept a name or {name, conditional}. Store entries accept "read", "rw", or {mode, conditional}. Any other key in an entry is a load error.
A predicate decides whether a declared entry is included for this caller. It cannot add an undeclared resource. It must return true to include the entry. A thrown error or missing caller context excludes it.
interface AgentSurfaceContext {
claims: Record<string, string>;
context: Record<string, string>;
scopeId: string;
userId?: string;
orgId?: string;
humanPresent: boolean;
isSubagent: boolean;
agentName: string;
}| Field | Meaning |
|---|---|
claims | Verified JWT claims. Use for authorization decisions. |
context | Merged request context, including client-supplied values. Use to select relevant workflows or resources. |
scopeId | The session's scope identifier. Empty for agent-level state. |
userId, orgId | Verified caller and organization identifiers, when available. |
humanPresent | false for cron, webhook, and channel runs where nobody can answer a confirmation. |
isSubagent | true when the agent runs as a delegated specialist. |
agentName | Name of the session's root agent, including in specialist predicates. |
A client can send {"role": "manager"} in request context. A permission predicate must therefore read ctx.claims.role, not ctx.context.role.
A delegated specialist inherits the session's surface context with isSubagent set to true. A curation predicate that reads page state should allow delegation explicitly:
{name: 'lookup', conditional: (ctx) => ctx.isSubagent || ctx.context.page === 'requests'}Rules:
- Predicates must be synchronous and pure. Do not fetch data or change state in them.
- Excluding a skill also excludes the tools contributed by its
allowedTools, unless another declaration grants them. agent.tsfields override matchingagent.jsonfields and logagent_definition_overrides_json. Keep each field in one file.agent.tsrequires a default export. Named exports are a load error.AGENT.mdis still required.- Conditional entries resolve at session creation and are checked again on each turn. If the resolved set changes, the runtime rebuilds the session's resources and keeps the conversation.
CONTEXT.md
AGENT.md renders once when the session is built, so values in it stay fixed. Put facts that change while the user works in CONTEXT.md, which renders on each turn after the cached prompt prefix.
Both files use Nunjucks and read request context as scope:
{% if scope.request_status %}
Request status: {{ scope.request_status }}.
{% endif %}Send the context with each chat request:
{
"message": "Why has this not been approved?",
"agent": "requests",
"context": {
"mission_id": "m-42",
"request_status": "Pending review"
}
}CONTEXT.mdis read when its agent roots the conversation, includingdefault. It is not read formainor for a delegated specialist.- Keys must start with a letter or underscore and contain at most 64 letters, digits, or underscores. Other keys are dropped.
- Values are strings and inert text. Template syntax inside a value is not executed.
"false"and"0"are truthy. - Empty and whitespace-only values are dropped.
- Values have no length cap. The runtime logs
prompt_template_scope_largeabove 100,000 total characters. - Wrap literal template syntax in
{% raw %}...{% endraw %}.
Prompt text does not enforce permissions. Use a predicate on verified claims to gate a capability, and a tool call for data that must be checked against a server.
Delegation
The root agent calls call_subagent with one or more named tasks:
call_subagent({
calls: [
{
subagent: 'compliance-checker',
task: 'Check transactions TXN-001 through TXN-050 against the compliance policy.',
input: { policy: 'aml-2026' },
},
],
});- Each call accepts at most 12 tasks and runs at most five concurrently.
- A single task returns
{subagent, result}or{subagent, error}. A batch returns{results: [...]}in call order. One failed specialist does not fail the batch. - Nested delegation stops at five levels.
- Specialists run without an interactive approval handler. A tool call that requires confirmation is denied.
- A failing tool returns an error to the specialist's model. A provider failure fails the specialist.
- From 65% of
maxToolCalls, each tool result carries a_budgetnotice with the turns left. Non-object results are wrapped as{result, _budget}.
Context Isolation
A specialist receives its own prompt as the system prompt, the task, the optional input appended as a JSON block, and any attachments a composite tool forwards. It does not receive the parent's prompt, skill bodies, or conversation history. Name every tool it needs, including load_knowledge or load_skill. A declared tool is copied from the parent session's registry, so it must exist there. Declared file tools are the exception: the runtime builds them for the specialist. See Tools for tool names.
Model Tiers
The deployment binds simple and advanced to models through Studio model settings or the MODEL_SIMPLE and MODEL_ADVANCED environment variables. default and an unbound tier use the session model. ctx.callSubagent from a durable tool ignores modelTier and uses the session model.