Delegation

When one agent should hand work to another, the ladder from a plain function call to a full agent-as-tool, how to keep a tree in one trace and one budget, and the depth limit to respect.

The Spikefrost Team30 Jul 20264 min read

Before adding a second agent, ask whether you need one. Most features are one agent. Splitting adds trace surface, latency, and the risk that a parent confidently reports work a child never did.

Three reasons that justify a split:

  • Parallelism — several independent lookups that can run at once.
  • Context isolation — a subtask whose long output would pollute the parent's conversation.
  • A narrower grant — the child needs a tool you don't want the parent to hold, or the parent handles untrusted input and the child touches something sensitive.

"It feels tidier" is not on the list.

The connection ladder

Pick the lowest discretion that works. Every rung up hands more of the decision to a model.

Level Mechanism Use when
L0 A typed function call between facets The default. You know what to call and when
L1 Agent-as-tool The model should choose the moment; a schema fixes the interface
L2 Free-text chat() between agents Rare — you genuinely want open-ended dialogue
L3 Workflow pipelines Multi-stage orchestration with durable steps

Most trees are L0 with one or two L1 edges. If you find yourself at L2, check whether a typed call would do — free text between two models is where fabrication creeps in.

Invariants belong in beforeTurn code, not in prose. If something must be true before every turn, check it in code.

Cross-app work is different in kind: that's a platform job, not a direct call.

Agent-as-tool

import { agentTool } from "@spikefrost/agents";   // NOT the raw SDK path

override tools = {
  plan_trip: agentTool(TripPlannerAgent, {
    description: "Plan a full trip given a destination and dates",
    inputSchema: z.object({
      destination: z.string(),
      nights: z.number(),
    }),
  }),
};

Import from @spikefrost/agents. The platform's agentTool does three things the raw SDK equivalent doesn't:

  1. Links the delegation into one stitched trace, so you can see the child's steps inline.
  2. Makes the child inherit the parent's trace id.
  3. Bills the whole tree as one turn under one budget — a per-tree cap covers children.

Wire a child with the raw import and it renders opaque, bills separately, and escapes the tree's budget cap. Same behavior, different accounting, and you'll find out at the wrong moment.

Where child files go

Kind Path Notes
Facet (one parent only) app/agents/<parent>/agents/<child>.ts No object binding of its own; not publicly reachable
Shared (several parents) app/agents/shared/agents/<child>.ts Imported by each parent

Never copy a shared child. Children resolve by class name, so two files declaring the same class break the build.

Also resist promoting a helper to top level just to share it — top level means a publicly routable agent, which is a different security posture. Import from shared/ instead.

The depth limit

Maximum depth is three: root → facet → one nested facet.

A fourth agent-as-tool level has been observed to silently not start. The failure mode is the dangerous kind: the parent doesn't error, it describes the work as though the child had done it. You get a plausible answer built on nothing.

So: keep trees shallow, and verify any depth-3 delegation in the trace. The rule generalizes — after building any delegation, confirm the child's edge actually appears:

sf api get "/apps/<appId>/agent-trace?agent=<route>&session=<sid>&traceId=<tid>&format=mermaid&detail=1"

A missing edge means the child never ran, whatever the parent claimed. This is the single highest-value check in the whole agent section.

Designing the tree

Write the tree down before you write code — one line per node: its single responsibility, its model, its tool grants, and which edges are code decisions versus model judgment. That document ends up being the sequence diagram you'll later compare against real traces.

A shape that works well:

  • Root: a capable model, holds the conversation, decides what's needed, owns approvals.
  • Facets: cheap models with narrow grants doing one thing each — classify, extract, summarize, look up.

The most common mistake is the inverse: a cheap root that can't route well, delegating to expensive children.

Next

Frequently asked questions

How many agents should a feature have?

Usually one. Split only for genuine parallelism, to isolate context, or to give a narrower permission grant. Most working trees are one to three nodes.

Why must I import agentTool from the platform package?

The platform's agentTool links the child into one stitched trace, makes it inherit the parent's trace id, and bills the whole tree as a single turn under one budget. A raw SDK import renders opaque and budgets separately.

How deep can delegation go?

Three levels — root, facet, one nested facet. A fourth level has been observed to silently not start, after which the parent describes work that never happened. Verify any depth-3 delegation in the trace.

How do I share a child agent between two parents?

Put it in app/agents/shared/agents/ and import it from each parent. Never copy the file — children resolve by class name and duplicates break the build.

What does a delegating tree cost?

It bills as one root-labeled turn covering all children, and a per-tree budget cap applies to the whole thing — provided every child was wired with the platform agentTool.