Docs/Gage the AI Harness/Authoring an Atlas Agent
GageReference

Authoring an Atlas Agent

How to write an agent definition for Atlas: the manifest format, the tool list, delegation, and where the file has to live to be picked up.

Updated 29 Oct 2026Timebase Docs

Overview#

This article covers the agents that ship inside Atlas: how one is defined, how the runtime loads it, and how to author your own.

It is the developer view: the manifest file format, how the runtime resolves it, and how to add a new agent. For the end-user view of what the six shipped agents do and how they hand work to each other, see Atlas AI Agents - this chapter does not restate that roster.

The runtime that parses and runs an agent definition is shared across Timebase products. Atlas is one host of that runtime; it contributes nine tool providers, a Pulse-proxied model transport, and the six shipped agent files. Everything under "The manifest format" through "Delegation" below is the shared runtime and applies to any host that calls AddTimebaseAgent(). "Authoring surface and permissions" and "Deployment reality" are Atlas-specific: the routes, the role gate, and the on-disk paths are Atlas's own wiring.


1. The manifest format#

An agent definition is one Markdown file: YAML frontmatter, then the system prompt as the Markdown body. The parser is AgentParser.ParseMetadataFromContent, which uses IgnoreUnmatchedProperties() on the YAML deserializer - an unrecognized key in the frontmatter is dropped, not rejected. There is no schema validation beyond the eight fields below and the mcpServers transport checks in section 3.

Complete frontmatter field list#

Field YAML type Default when omitted Consumed at runtime?
name string filename without .md Yes - the registry index key and the identifier a DelegateTask call must match.
description string falls back to name Yes - shown in the agent list (AgentInfoResponse.Description) and inlined verbatim into every other agent's DelegateTask tool description so the parent model can pick a target. Empty descriptions are rejected on save.
model string "default" Yes - keys the chat client (see section 3).
icon string "bot" Display only - returned in the agent responses for the interface avatar.
color string (hex) "#31AEE4" Display only, same as icon.
displayOrder integer 0 Yes - sorts the agent within its section of the Start an agent picker, ascending; agents sharing a value fall back to name order, and negatives are valid. Never orders across the User-defined / Built-in split.
tools list of strings null (every registered provider, unscoped) Yes - resolved by ToolScopeResolver.Resolve (see section 2).
maxToolRounds integer null, which means 40 Yes - caps the model-and-tool rounds per message. See below.
mcpServers list of objects [] Yes - see section 4.

is the record these deserialize into; FilePath, Body, and IsUserDefined are stamped by the registry/parser, never present in the file itself.

name, description and model are commonly described as required. In practice name and model both have working fallbacks, and only an empty description is rejected, and only when updating an agent rather than creating one - there is no check on Description at all beyond the request model requiring the field to be present.

maxToolRounds#

maxToolRounds caps how many model-call-then-tool-call rounds one message may take. It is parsed from the frontmatter, stored on the manifest, round-tripped through every authoring endpoint, editable on the Agents page, and enforced: a run that reaches the cap ends with a visible notice and the tool call it was about to make is not invoked. A manifest that leaves it out runs under AgentManifest.DefaultMaxToolRounds, which is 40, the same ceiling every agent ran under before the cap was wired. A delegated sub-agent that hits its own cap stops there and returns what it had.

Set it deliberately. Every round re-sends the whole conversation prefix, so the shipped agents cap themselves below the default (gage at 30, the specialists between 8 and 25) and tell the model to batch its reads. (Earlier Beta builds parsed the field and did nothing with it; that is no longer the case.)


2. tools: - accepted values and resolution#

The sixteen names tools: accepts in an Atlas#

Ten are Atlas-specific, registered by AddAtlasAgentTools(); six are generic runtime tools registered by AddTimebaseAgent() and shipped to every host:

Name Read/write Requires pulse:administrator?
OntologyToolProvider read No
OntologyWriteToolProvider write Yes
DomainToolProvider read (list/inspect domains and libraries) No
DomainLifecycleToolProvider write (create/compile/import) Yes
InstanceMapToolProvider read No
InstanceMapWriteToolProvider write Yes
LibraryToolProvider read No
LibraryWriteToolProvider write Yes
CypherToolProvider read (executes Cypher against the compiled world) No
BranchToolProvider read (ListBranches, UseBranch: pick the conversation's branch when the page has none) No
SkillToolProvider read (finds/loads skills: ListSkills, GetSkill) No - but see the note below
SkillWriteToolProvider write (CreateSkill, UpdateSkill) Yes
AgentToolProvider read (ListAgents, GetAgent, ListToolProviders) No
AgentWriteToolProvider write (CreateAgent, UpdateAgent) Yes
BashToolProvider write (shell) Yes in Atlas - see below
FileSystemToolProvider write (file read/write/glob) Yes in Atlas - see below

Each write/lifecycle provider declares its own RequiredRole in code (public string? RequiredRole => AdminRole;, where AdminRole = MeMapper.AdminRole = "pulse:administrator",). BashToolProvider and FileSystemToolProvider carry no role of their own - they are generic, host-agnostic tools - but Atlas's wraps them in a RoleRestrictedToolProvider at registration time via AgentOptions.BuiltInToolsRequiredRole = MeMapper.AdminRole. The effect is identical either way: a non-administrator caller never sees these eight providers' tools, regardless of what an agent's tools: list names.

How resolution works#

ToolScopeResolver.Resolve runs on every AgentFactory.CreateAgentAsync call:

  1. Every registered IToolProvider is filtered by RequiredRole against the caller's roles first - a provider the caller does not hold the role for is dropped before any spec matching happens, so an agent never even learns the tool existed. An anonymous caller (no IAgentExecutionContext.ActingUser) is never filtered - this is what keeps a bare single-operator deployment with Pulse:Auth:Enabled=false fully open.
  2. tools: omitted, or an empty list: every tool from every role-permitted provider.
  3. "ProviderName": every tool from that one provider, matched case-insensitively.
  4. "ProviderName:ToolName": one specific tool.
  5. "mcp:{server}" or "mcp:{server}:{tool}": tools from a declared external MCP server (see section 4) -.

What happens when a name does not resolve#

Nothing is reported. The entry is dropped. An unrecognized "ProviderName" simply finds no matching provider and contributes zero tools; an unrecognized "ProviderName:ToolName" behaves the same way; an unknown MCP server or tool name resolves to an empty tool list rather than an error. There is no exception, no log line visible to the agent author, and no refusal to start the agent - the agent simply runs with fewer tools than its tools: list implies. If every entry in the list fails to resolve, the agent starts with zero tools (not "every tool" - that fallback only applies when the whole field is omitted or the list is empty, per point 2 above).

This is a materially different failure mode from a system that refuses to launch an agent whose tools: list resolves to nothing. If an agent behaves as though it has no tools, check every spec in its tools: list for a typo before assuming a permissions problem.

The live tool catalogue#

GET /app/v1/agent/tools lists every currently registered provider and the tools within it, filtered by the SAME FilterByRequiredRole check ToolScopeResolver.Resolve applies - so the Agents page's tool picker never offers a spec the caller could not actually use. This is the authoritative source for what names are valid in a given deployment; the table above is what ships by default.

SkillToolProvider is "mandatory" in the UI only, not in the runtime#

The Agents page's create/edit/duplicate panels hard-lock SkillToolProvider on in the tool picker

  • MANDATORY_AGENT_TOOL_PROVIDERS = ["SkillToolProvider"], consumed by ToolPicker's mandatoryProviderNames prop to disable its checkbox. That is a client-side convention only. has no special case for SkillToolProvider anywhere in its resolution logic - if you hand-author a manifest (or edit one through the Raw-text save path, section 6) with a tools: list that omits SkillToolProvider, the server accepts it and the agent loses the ability to find or load any skill. The UI lock only protects authors who never leave the form.

3. Model selection#

What model: accepts#

Atlas is wired as a Gage-proxied host: AddAtlasAgentGage() replaces the runtime's default KeyedServiceAgentModelResolver with GageModelRegistry, which implements both IAgentModelResolver and IAgentModelCatalog. Because of this, an Atlas deployment's model catalog is always managed (IAgentModelCatalog.IsManaged => true,), and only two kinds of value are ever accepted in model::

  • the reserved sentinel "default" (GageModelRegistry.DefaultModelSentinel,), or
  • a literal handle in the exact form providerId/logicalModelId (GageCatalogModel.Handle,) that Pulse's Gage catalog currently reports.

There is no alias vocabulary such as sonnet, opus or haiku in this system. All six shipped agents declare model: default.

Validation happens server-side, independent of the UI, via GageModelRegistry.ValidateForCreate, called from CreateAgent, UpdateAgent, and the raw-text save path:

  • zero models currently pinned from Pulse: creation/update is refused outright, even for "default";
  • "default": accepted whenever at least one model is pinned;
  • anything else: must exactly match a Handle in the pinned catalog, or the save is rejected with "Model '<handle>' is not in the Gage catalog pinned from Pulse. Pick the reserved word 'default' or one of the handles Pulse currently reports.".

How Pulse:Gage model discovery interacts with a per-agent choice#

At startup (and on every live Gage-enabled toggle), GageModelRegistry.PinAsync fetches Pulse's full model catalog once (GET {ProxyEndpoint}/api/gage/models, using the app's own client-credentials token) and holds it as atomically-swappable state - no further Pulse round trip happens per chat turn. Resolution at chat time (GageModelRegistry.ResolveChatClient,) follows a fixed three-step precedence:

  1. The operator's Pulse:Gage:Agents map (GageAgentOptions.Agents,) - an agent-name to handle-or-"default" override, keyed case-insensitively - always wins, over even a hand-authored manifest's own model: value. This is the one deployment-level knob that retargets any agent, built-in or custom, without touching its file.
  2. Else, the manifest's own model: value, unless it is the "default" sentinel.
  3. Else, the installation's default model - the one marked in Pulse's provider editor, or the alphabetically-first pinned handle.

If a manifest names a handle that later drops out of Pulse's catalog (a model disabled after the agent was saved), the file is not re-validated - only a save is. The next chat turn against that agent reports Agent '<name>' names Gage model '<handle>', which is not in the catalog pinned from Pulse on the chat stream, so the host keeps running and the message reaches you in the panel.


4. mcpServers: - outbound MCP client configuration#

An agent can declare external MCP servers it connects to as a client - distinct from Atlas's own inbound MCP server that exposes every IToolProvider at /mcp (AddAtlasMcpServer(), out of this chapter's scope).

Shape#

mcpServers:
  - name: timebase-historian
    transport: http
    endpoint: http://historian:4511/mcp
    toolTimeoutSeconds: 30
    auth: pulse
tools:
  - mcp:timebase-historian
  - LibraryToolProvider

transport: stdio servers use command / args / env instead of endpoint:

mcpServers:
  - name: everything
    transport: stdio
    command: npx
    args: ["-y", "@modelcontextprotocol/server-everything"]
    env:
      DEBUG: "1"

Parsing and validation live in AgentParser.ParseMcpServers, which throws InvalidOperationException - caught by the raw-text save path and turned into a field-scoped 400 - for:

  • a missing name or missing transport;
  • transport: http with no endpoint;
  • transport: stdio with no command;
  • any transport other than http or stdio;
  • auth: set to anything other than pulse, or auth: pulse on a stdio server - outbound auth only applies to transport: http.

toolTimeoutSeconds#

Every MCP tool invocation is wrapped in a timeout: the global default is 60 seconds (AgentResilienceOptions.DefaultToolCallTimeoutSeconds, configurable via Pulse:Gage:Resilience:ToolCallTimeoutSeconds). A server entry's own toolTimeoutSeconds overrides the global default for calls to that server (AgentResilienceOptions.ResolveToolCallTimeout,); 0 disables the timeout for that server entirely, letting the call run unbounded. A timeout or failure never throws into the model - it comes back as [tool '<name>' failed] <cause> text so the model can reason about and retry the failure (per).

auth: pulse#

Setting auth: pulse on an http server stamps every outbound request to it with Atlas's own client-credentials bearer token (McpServerConfig.PulseAuthScheme = "pulse", wired via AddGageMcpAuth(),). It is opt-in per server on purpose: the token carries Atlas's own audiences and must never be sent to a third-party MCP server the manifest author does not control.

The Docker service-name rule#

When the target MCP server runs in another container on the same Compose network, endpoint must use that container's Docker service name (http://historian:4511/mcp), never a bridge IP (http://172.22.0.2:4511/mcp) - bridge IPs are assigned dynamically and differ per machine and per network recreation, so a hardcoded one works only by accident. Use localhost only for a manifest connecting back to Atlas's own /mcp endpoint on its own container's listener port.

One client per unique server name, shared across all manifests#

McpClientRegistry maintains one live connection per unique server name declared across every agent manifest in the deployment, not one per agent. McpClientInitializerHostedService connects them in the background without blocking host startup and retries indefinitely on a constant backoff (AgentResilienceOptions.McpReconnectBackoffSeconds, default 5s). The tools each server exposes are cached under the synthetic mcp provider (McpClientToolProvider.Name => "mcp",) with wire names mcp__{server}__{tool}, which is what the mcp:{server}:{tool} spec in tools: resolves against.

mcpServers: cannot be edited from the structured form#

UpdateAgentRequest and AgentFrontmatterWriter.ApplyEdit have no mcpServers parameter at all. The Agents page's Form tab can view an existing mcpServers: block (it survives an unrelated field edit unchanged, by the writer's own "leave what the caller didn't mention alone" contract,) but cannot add or change one. The only ways to declare or edit mcpServers: are: hand-editing the .md file on disk, or using the Raw tab's full-file text save (ApplyRawTextUpdate,), if the deployment's build of the Agents page exposes it.


5. Delegation: DelegateTask#

Every agent that runs inside a chat session gets a DelegateTask tool, appended after its own tools: scoping is resolved - a manifest cannot narrow it away, and it is present even for an agent whose tools: list is empty or names nothing else.

How it is attached#

MafAgentRuntime.ProcessMessageAsync builds delegation tools only when the chat request carries both a session id and an instance id (_sessionId/_instanceId in the request parameters - hasDelegation = !string.IsNullOrEmpty(sessionId) && !string.IsNullOrEmpty(instanceId),). These are the coordinates the Atlas chat panel always sends for a session-scoped conversation; a bare programmatic call to POST /app/v1/agent/chat that omits them gets no DelegateTask at all. When present, DelegationToolFactory.CreateDelegationTools builds one tool listing every OTHER agent currently in the registry (a.Name != parentAgentId,) with each one's description inlined - this is why a vague description field actively harms routing. AgentFactory.CreateAgentAsync appends this tool list after ToolScopeResolver.Resolve runs, so it rides on top of, not through, the manifest's own tool scoping.

Sub-agents cannot re-delegate#

When DelegateTask fires, DelegationToolFactory.ExecuteDelegationAsync builds the sub-agent directly through context.AgentFactory.CreateAgentAsync(manifest, context.Parameters, ct: ct) - note the absence of an additionalTools argument. It never routes back through MafAgentRuntime.ProcessMessageAsync, which is the only place delegation tools are ever constructed. The sub-agent's tool set is therefore exactly ToolScopeResolver.Resolve(subManifest.Tools, ...) with nothing appended - it structurally cannot receive a DelegateTask of its own. This is not a configured depth limit that could be raised; it is a consequence of which method builds the sub-agent.

The caller's role still gates writes#

IAgentExecutionContext is populated once per HTTP request by AgentActingUserFilter from the authenticated principal's identity and role claims, and is injected into AgentFactory as a singleton captured at construction. ExecuteDelegationAsync calls that SAME AgentFactory instance to build the sub-agent, so the sub-agent's ToolScopeResolver.FilterByRequiredRole check runs against the identical role set the parent agent's own tool resolution used. A non-administrator whose own agent had its write tools dropped cannot recover them by delegating to builder or any other write-capable agent - the same pulse:administrator gate drops those tools for the sub-agent too.


6. Authoring surface and permissions#

The Agents page lists only agents where IsUserDefined is true - the eight built-ins never appear there, even though they are fully usable from chat and the "Start an agent" picker. parseAgentsDeepLink resolves a URL of the exact shape /agents/{name} to the agent name to pre-select; it explicitly rejects /agents and /agents/ (no segment, or an empty one) and any path with more than one segment after the prefix.

The routes#

Standard surface, any authenticated user: GET /app/v1/agent/status, GET /app/v1/agent/agents, GET /app/v1/agent/models, POST /app/v1/agent/chat, GET /app/v1/agent/threads, DELETE /app/v1/agent/threads/{id}, GET /app/v1/agent/threads/{id}/messages. A deprecated, undocumented /api/agent/* alias mirrors every one of these.

Authoring surface, pulse:administrator only:

Route Handler
POST /app/v1/agent/agents CreateAgent
GET /app/v1/agent/agents/{name} GetAgentDetail
PATCH /app/v1/agent/agents/{name} UpdateAgent
DELETE /app/v1/agent/agents/{name} DeleteAgent
POST /app/v1/agent/agents/{name}/duplicate DuplicateAgent
GET /app/v1/agent/tools GetToolCatalog

AtlasAgentEndpoints.MapAgent applies the split only when Pulse:Auth:Enabled is true: standard routes require AuthExtensions.AuthPolicies.UiAccess, authoring routes require UiAdministratorAccess. With authentication disabled, Atlas's DisabledAuthProvider would make either named policy 403 every request, so the whole surface - authoring included - is mapped open instead. There is no anonymous-but-read-only middle ground: with auth off, anyone can create or delete a custom agent.

Built-ins are refused on every authoring route#

GetAgentDetail, UpdateAgent, DeleteAgent, and DuplicateAgent each check manifest.IsUserDefined and return 403 Forbidden with an explanatory message otherwise, for example: 'atlas' is a built-in agent and cannot be updated through this endpoint. CreateAgent and DuplicateAgent additionally refuse a name that collides with a built-in - checked against every configured agent directory, not just the user-defined one (FindAgentNameCollision,), returning: '<name>' is a built-in agent name. A shipped agent already uses this name - built-in agents cannot be overridden; choose a different name. A name is validated against ^[a-z0-9][a-z0-9-]*$ before any of this runs.


7. Deployment reality#

Where the file lands#

Two directories are configured, in this order, via AgentOptions.AgentDirectories:

  1. Built-in - {AppContext.BaseDirectory}/agents. The six shipped .md files are placed here when Atlas is built, not copied by hand.
  2. User-defined - {configPath}/agents, always the last entry, which is the convention AgentRegistry and the create-agent endpoint both key off (AgentOptions.AgentDirectories doc comment, AgentRegistry.ScanDirectories,). The store path resolves from the Config setting. On Windows with no explicit value it defaults to %ProgramData%\Flow Software\Timebase\Atlas\Config, so the user agents directory is ...\Config\agents. In Docker the platform Compose file sets Config=/atlas/config, so the directory is /atlas/config/agents.

AgentRegistry scans both directories at construction and, if a name appears in more than one, the last-scanned directory wins in the shared index (_index[manifest.Name] = ...,) - which, given the configured order, is the user directory. The authoring API prevents this collision from ever being created through normal use (FindAgentNameCollision, above), but a file dropped directly onto the server's user directory with a built-in's exact name is not checked at scan time, and would shadow the built-in in the registry. Use the authoring API rather than dropping files in by hand.

Reload() vs. a restart#

Every successful write through the authoring API - CreateAgent, UpdateAgent, DeleteAgent, DuplicateAgent - calls registry.Reload(), which clears the whole index and rescans both directories. An edit made on the Agents page is therefore live on the agent's very next chat turn - no restart.

Nothing watches the filesystem. A .md file added or edited by hand (dropped onto the server, or edited with a text editor) is only picked up the next time ScanDirectories runs, which happens at AgentRegistry construction - i.e. Atlas process startup - or via an explicit Reload() call from the authoring API. A hand-edited file therefore needs a full service restart to take effect; the shipped states this plainly ("discovered on next Atlas startup - no rebuild required" - no code rebuild, but the running process must still restart to rescan).

Agents are held outside version control#

The {configPath}/agents/ and {configPath}/skills/ directories are excluded from the model's Git repository. Atlas seeds /agents/ and /skills/ into that repository's .gitignore on every boot and untracks either directory from the index if an earlier version committed it, leaving the working tree untouched. The registries read both directories straight off disk, which keeps prompt edits out of the model history.

Keep your own copy of any prompt worth restoring. An agent you create or edit has no history in Atlas, so overwriting its system prompt through the Agents page, or deleting it, cannot be undone from within the product.


8. Worked example: create, scope, invoke, verify#

This creates a minimal read-only agent scoped to two providers, using only the Agents page - no file system access to the server required.

1. Create#

Sidebar → Agents group → Agents → New agent (requires pulse:administrator, section 6). Fill in:

  • Name: graph-explainer (must match ^[a-z0-9][a-z0-9-]*$).
  • Description: Answers read-only questions about a domain's structure by inspecting the ontology and querying the compiled graph. Never writes. (This is what another agent's DelegateTask call sees, section 5 - be specific.)
  • Model: leave as default, unless the deployment's Pulse Gage catalog is empty, in which case creation is refused until an administrator pins at least one model in Pulse (section 3).
  • Tool access: switch on Restrict tool access, then check only OntologyToolProvider and CypherToolProvider. SkillToolProvider is not offered because it is locked on (section 2): Atlas writes it into the file for you. Everything else - including OntologyWriteToolProvider, BashToolProvider, FileSystemToolProvider - stays unchecked.

The agent editor's Form tab for graph-explainer, scrolled to Tool access: Restrict tool access switched on and the provider list with CypherToolProvider ticked and every write provider clearThe agent editor's Form tab for graph-explainer, scrolled to Tool access: Restrict tool access switched on and the provider list with CypherToolProvider ticked and every write provider clear

  • Max tool rounds: leave blank for the default of 40, or set a lower cap (section 1).
  • System prompt, on the Instructions tab: something like: "You answer questions about this domain's ontology and its compiled instance graph. You inspect types and relationships with your ontology tools and run Cypher with your query tool. You never propose or make a write - if the user asks for a change, say that is out of scope for you and suggest builder or the general gage assistant instead."

The agent editor's Instructions tab: a code editor holding the system prompt as markdownThe agent editor's Instructions tab: a code editor holding the system prompt as markdown

The Raw tab shows the manifest the form is building, frontmatter and body, and can be edited directly:

The agent editor's Raw tab: the manifest with name, description, model, icon, color and a tools list of SkillToolProvider, OntologyToolProvider and CypherToolProvider above the system promptThe agent editor's Raw tab: the manifest with name, description, model, icon, color and a tools list of SkillToolProvider, OntologyToolProvider and CypherToolProvider above the system prompt

Select Create. This calls POST /app/v1/agent/agents (CreateAgent,), writes {configPath}/agents/graph-explainer.md, and calls registry.Reload() - the agent is live immediately, with no restart (section 7).

2. Confirm the resulting file's shape#

The written file looks like:

---
name: graph-explainer
description: >
  Answers read-only questions about a domain's structure by inspecting the
  ontology and querying the compiled graph. Never writes.
model: default
icon: bot
color: "#31AEE4"
tools:
  - OntologyToolProvider
  - CypherToolProvider
---

You answer questions about this domain's ontology and its compiled instance graph. ...

Note SkillToolProvider is absent from tools: even though the UI showed it checked and locked - CreateAgentRequest only writes what the caller actually sent, and the create panel's own mandatoryProviderNames convention (section 2) only prevents unchecking it in the picker; if this particular save path did not include it in the submitted list, the file will not have it, and the agent gets no skill-discovery tools. Open the agent again on the page and confirm SkillToolProvider shows checked before relying on it - or read the file directly if you have server access.

3. Verify the tool scope actually took effect#

GET /app/v1/agent/tools (section 2) returns the full catalogue filtered by your own role; confirm OntologyToolProvider and CypherToolProvider appear and that nothing your agent was not granted does. Then, in the Ask Gage panel, the + button lists graph-explainer under User-defined; start it and ask it something the ontology tools can answer, e.g. "which entity types derive from WorkUnit?" - and then ask it to create something. It should refuse the write per its own system prompt; separately, even if the prompt did not refuse, OntologyWriteToolProvider was never granted, so it has no tool that could carry the write out.

graph-explainer answering 'Which entity types derive from WorkUnit, and how many instances does each have?': GetOntologyNode, ListOntologyNodes and ExecuteCypherQuery tool blocks, then a table of StorageUnit, Unit, WorkCell and Filler with their instance countsgraph-explainer answering 'Which entity types derive from WorkUnit, and how many instances does each have?': GetOntologyNode, ListOntologyNodes and ExecuteCypherQuery tool blocks, then a table of StorageUnit, Unit, WorkCell and Filler with their instance counts

4. Verify delegation reaches it#

From the general gage assistant (or any other agent) in the same chat session, ask something that names the new agent directly, e.g. "ask graph-explainer what feeds Tank 3." If the parent agent calls DelegateTask(agent: "graph-explainer", task: "..."), you will see delegation_start / delegation_end events in the stream and the sub-agent's answer folded into the parent's response. Confirm the sub-agent could not itself delegate further by asking it, directly, to hand a task to builder - it has no DelegateTask tool at all (section 5), so it can only decline in its own words, never actually dispatch.

5. Clean up#

Delete on the Agents page calls DELETE /app/v1/agent/agents/graph-explainer, which removes the file and reloads the registry. Existing chat threads that used the agent are left alone - a console tab still open on it hits the ordinary "agent not found" handling on its next message.


esc
↑↓ navigate · ↵ open · ⌘K anywhere