Threads AI

3. Delegate to a subagent

Hand one job to a second agent that works in its own thread and reports back.

The support agent is doing two jobs: talking to the customer, and digging through orders. Splitting them keeps each one's context short, and lets the digging run on a cheaper model later.

A subagent is an ordinary agent that another agent may start with a task. It gets its own thread and its own log; its final answer comes back to the parent as a tool result.

Two agents

List the agents the parent may start in subagents. The parent's model gets a spawn_agent tool and sees the names in its instructions.

The scripted responses repeat, so both programs below build them with two small helpers: say for a plain answer, call for a tool call.

support.ts
import { agent, scriptedModel, sqlite, tool } from "@threads/core";
import { z } from "zod";

const usage = { input_tokens: 10, output_tokens: 2 };
const say = (text: string) => ({
  content: [{ type: "text", text }],
  stop_reason: "end_turn",
  usage,
});
const call = (id: string, name: string, input: unknown) => ({
  content: [{ type: "tool_use", call_id: id, name, input }],
  stop_reason: "tool_use",
  usage,
});

const lookupOrder = tool({
  name: "lookup_order",
  description: "Look up an order by id.",
  input: z.object({ id: z.string() }),
  effect: "read_only",
  execute: async ({ id }) => `order ${id}: delivered 12 days ago`,
});

const orderDesk = agent({
  name: "order_desk",
  instructions: "Look the order up and answer in one sentence.",
  model: scriptedModel({
    responses: [
      call("o1", "lookup_order", { id: "42" }),
      say("Order 42 was delivered 12 days ago."),
    ],
  }),
  tools: [lookupOrder],
});

const support = agent({
  name: "support",
  instructions: "Send order questions to the order desk, then answer the customer.",
  model: scriptedModel({
    responses: [
      call("s1", "spawn_agent", { agent: "order_desk", prompt: "When was order 42 delivered?" }),
      say("Order 42 arrived 12 days ago, inside the 30-day refund window."),
    ],
  }),
  tools: [lookupOrder], // a subagent only gets tools its parent also has
  subagents: [orderDesk],
});

const result = await support.run("Can I still return order 42?", { store: sqlite(":memory:") });
if (result.status === "completed") console.log(result.output);
else console.log(result.status);

for (const child of await result.thread.children()) {
  console.log(child.child_thread_id, child.status);
}

It prints the customer's answer and then the desk's own thread:

Order 42 arrived 12 days ago, inside the 30-day refund window.
01a0ddb0-9f74-7c34-855c-b90a57b6d0b8 completed

The id changes every run. What matters is that there is one: the order desk's work is a thread of its own, which you can read with timeline(), replay or fork exactly like the parent's.

What a subagent may do

A subagent can only narrow what its parent may do, never widen it. That is why lookupOrder is passed to both agents above: a subagent's tools are filtered to the names its parent also has, so a tool the parent lacks is simply not there.

The same goes for permissions — each call is decided under the subagent's rules and again under its parent's, and the stricter answer wins — and for budgets, which the parent's covers.

spawn_agent also takes background: true, which returns at once and delivers the answer to the parent later. Subagents covers what that does to a run's status.

Next

A subagent answers once and is gone. When you want members that keep working, message each other and report back as they finish, that's a team.

Edit on GitHub

On this page