Skip to content

Commit 023d9da

Browse files
committed
Add sub agents
1 parent cbbf48e commit 023d9da

13 files changed

Lines changed: 591 additions & 12 deletions

README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ const workflow = createAgentWorkflow(
132132
tools: tools.definitions,
133133
});
134134

135+
// Using single-agent (global) helper
135136
await actions.handleModelResult(result);
136137
actions.setLoading(false);
137138
},
@@ -740,6 +741,69 @@ const workflow = createAgentWorkflow(
740741
- **Debugging**: Add debugging information that doesn't appear in AI conversations
741742
- **Integration**: Store data needed for external systems or APIs
742743

744+
### Per‑Agent Message Scoping (Multi‑Agent in One Workflow)
745+
746+
Keep control state global (loading, inputDisabled, statusLine, queue, taskList). Use lightweight agent handles to scope messages and transcripts per agent within a single workflow.
747+
748+
```ts
749+
const planner = actions.createAgent("Planner"); // auto‑generates UUID id
750+
console.log(planner.id); // uuid
751+
console.log(planner.name); // "Planner"
752+
planner.setName("PlanBot"); // updates display name (keeps same id)
753+
planner.say("Starting plan…");
754+
755+
const planResult = await generateText({
756+
model: openai("gpt-4o"),
757+
system: "You are the planning agent.",
758+
messages: planner.transcript(),
759+
tools: tools.definitions,
760+
});
761+
762+
// Assistant and tool messages are tagged to this agent only
763+
await planner.handleModelResults(planResult);
764+
765+
// Another agent in the same workflow
766+
const executor = actions.createAgent("Executor");
767+
const execResult = await generateText({
768+
model: openai("gpt-4o-mini"),
769+
system: "You are the execution agent.",
770+
messages: executor.transcript(),
771+
tools: tools.definitions,
772+
});
773+
await executor.handleModelResults(execResult);
774+
```
775+
776+
Notes:
777+
- `actions.createAgent(name)` returns a handle with `id`, `name`, `setName`, `say`, `addMessage`, `transcript`, `handleModelResults`.
778+
- Messages produced via these methods carry `metadata.agentId = id` and the UI prefixes messages with `[name]` (fallback to `[id]`).
779+
- All other state (loading, inputDisabled, statusLine, queue, taskList) remains global at the workflow level.
780+
781+
#### Message structure and storage
782+
783+
- Storage model: a single workflow‑level message log (`state.messages`). There is no `agentsById` state; messages are attributed to agents via metadata and names are stored in `state.agentNames`.
784+
- Each message can include agent attribution by id only; names are resolved at render time:
785+
786+
```ts
787+
// UI-only messages or model/tool messages with optional agent attribution
788+
type UIMessage =
789+
| { role: "ui"; content: string; metadata?: { agentId?: string } & Record<string, unknown> }
790+
| (ModelMessage & { metadata?: { agentId?: string } & Record<string, unknown> });
791+
792+
// Example stored assistant message
793+
const m: UIMessage = {
794+
role: "assistant",
795+
content: [{ type: "text", text: "Plan complete." }],
796+
metadata: { agentId: "planner" },
797+
};
798+
```
799+
800+
#### Transcript semantics
801+
802+
- Global transcript (`state.transcript`): excludes UI messages and excludes any message with `metadata.agentId` (i.e., sub‑agents are filtered out). Use this for simple single‑agent prompts.
803+
- Per‑agent transcript (`agent.transcript()`): includes only non‑UI messages tagged with that agent’s `id`.
804+
- Tool execution: assistant messages and tool results produced via `agent.handleModelResults(result)` are tagged with that agent. Tool‑synthesized user messages are routed via the normal input path.
805+
- UI display: when `metadata.agentId` is present, the terminal UI prefixes the role header with `[name]` using `state.agentNames[agentId]` (fallback to `[id]`). You can override this by providing `displayConfig.formatRoleHeader` or a custom `agentNameResolver`.
806+
743807
## 📚 Examples
744808

745809
The SDK includes examples:
@@ -912,6 +976,71 @@ const workflow = createAgentWorkflow(
912976
);
913977
```
914978

979+
### Built-in Tools
980+
981+
These tools are available to your agent when you pass `tools.definitions` to your model call. Consider including the brief definitions below in your system prompt so the model knows how to use them.
982+
983+
- **`shell`**
984+
- **What it does**: Execute a shell/CLI command (including `git`) in a given working directory.
985+
- **Parameters**:
986+
- `cmd: string[]` — command and args, e.g. `["git","status"]`
987+
- `workdir: string` — working directory
988+
- `timeout: number` — milliseconds
989+
990+
- **`apply_patch`**
991+
- **What it does**: Apply file edits via a structured `apply_patch` payload for auditable changes.
992+
- **Parameters**:
993+
- `cmd: string[]` — invocation and payload, e.g. `["apply_patch","*** Begin Patch\\n...\\n*** End Patch"]`
994+
- `workdir: string` — working directory
995+
- `timeout: number` — milliseconds
996+
997+
- **`user_select`** (UI mode only)
998+
- **What it does**: Prompt the user to choose from options. The UI also includes a "None of the above (enter custom option)" choice automatically.
999+
- **Parameters**:
1000+
- `message: string` — text shown to the user
1001+
- `options: string[]` — available choices
1002+
- `defaultValue: string` — must match one of `options`; used on timeout
1003+
1004+
Note: In headless mode, only `shell` and `apply_patch` are available; `user_select` is not supported.
1005+
1006+
### Prompt guidance: include tool definitions
1007+
1008+
Add a succinct description of the tools and their parameter shapes in your system prompt so the model reliably uses them. For example:
1009+
1010+
```text
1011+
You can use the following tools. Call them with the exact parameter shapes.
1012+
1013+
- shell: run terminal commands.
1014+
Parameters: { cmd: string[]; workdir: string; timeout: number }
1015+
1016+
- apply_patch: make file edits using a single apply_patch payload wrapped in *** Begin Patch ... *** End Patch.
1017+
Parameters: { cmd: string[]; workdir: string; timeout: number }
1018+
1019+
- user_select (UI only): ask the user to pick from options; a custom option entry is available in the UI.
1020+
Parameters: { message: string; options: string[]; defaultValue: string }
1021+
1022+
Guidelines:
1023+
- Use shell for executing commands; prefer safe/read-only commands where possible.
1024+
- Use apply_patch for all file modifications; do not edit files via shell.
1025+
- Use user_select to disambiguate choices; ensure defaultValue matches one of options.
1026+
```
1027+
1028+
### Sample system prompt
1029+
1030+
```text
1031+
You are an engineering agent assisting inside a terminal UI. You can use tools.
1032+
1033+
Tools:
1034+
- shell(cmd: string[], workdir: string, timeout: number): run terminal commands.
1035+
- apply_patch(cmd: string[], workdir: string, timeout: number): apply file edits via a single *** Begin Patch ... *** End Patch block.
1036+
- user_select(message: string, options: string[], defaultValue: string): prompt the user to choose (UI only).
1037+
1038+
Rules:
1039+
- Prefer safe commands; request approval before risky actions if applicable.
1040+
- All code edits must use apply_patch with a complete patch block. Do not edit files via shell.
1041+
- Use user_select when you need the user to choose among options or confirm a decision.
1042+
```
1043+
9151044
## 📚 Type Definitions
9161045

9171046
Full TypeScript support with type definitions:

examples/gpt5-structure-scan.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { run, createAgentWorkflow } from "../dist/lib.js";
12
import { openai } from "@ai-sdk/openai";
23
import { generateText } from "ai";
3-
import { run, createAgentWorkflow } from "../dist/lib.js";
44

55
// Basic GPT‑5 structure scanning workflow
66
export const workflow = createAgentWorkflow(

src/components/WorkflowSwitcherOverlay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function WorkflowSwitcherOverlay({
7070
...(currentWorkflows.length > 0
7171
? [
7272
{
73-
label: `Close current (${(currentWorkflows.find((w) => w.id === activeWorkflowId)?.displayTitle) || "current"})`,
73+
label: `Close current (${currentWorkflows.find((w) => w.id === activeWorkflowId)?.displayTitle || "current"})`,
7474
value: "__close_current__",
7575
isLoading: false,
7676
},

src/components/chat/hooks/use-tool-execution.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export function useToolExecution(params: {
119119
// Forward to the workflow input path instead of duplicating in transcript
120120
dispatchUserMessage?.(r);
121121
} else if (r) {
122-
toolResponses.push(r);
122+
toolResponses.push(r as ModelMessage);
123123
}
124124
}
125125
}

src/components/chat/hooks/use-workflow-actions.ts

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
toggleNextIncomplete,
1212
toggleTaskAtIndex,
1313
} from "../../../utils/workflow/tasks.js";
14-
import { useCallback, useMemo } from "react";
14+
import { useCallback, useMemo, useRef } from "react";
1515

1616
export function useWorkflowActions(params: {
1717
smartSetState: (
@@ -230,7 +230,14 @@ export function useWorkflowActions(params: {
230230
return syncStateRef.current.taskList || [];
231231
},
232232
get transcript() {
233-
return filterTranscript(syncStateRef.current.messages);
233+
const all = syncStateRef.current.messages || [];
234+
const topLevel = all.filter((m) => {
235+
const meta = (m as UIMessage).metadata as
236+
| { agentId?: string }
237+
| undefined;
238+
return m.role !== "ui" && !meta?.agentId;
239+
});
240+
return filterTranscript(topLevel as Array<UIMessage>);
234241
},
235242
get statusLine() {
236243
return syncStateRef.current.statusLine;
@@ -245,6 +252,102 @@ export function useWorkflowActions(params: {
245252
[syncStateRef],
246253
);
247254

255+
// Agent API
256+
const agentsRef = useRef<Map<string, string>>(new Map());
257+
258+
const makeAgentHandle = useCallback(
259+
(id: string, name: string) => {
260+
const tagUiMessage = (m: UIMessage): UIMessage => ({
261+
...m,
262+
metadata: { ...(m.metadata || {}), agentId: id },
263+
});
264+
265+
const sayForAgent = (text: string, metadata?: MessageMetadata) => {
266+
const message = tagUiMessage({ role: "ui", content: text, metadata });
267+
void smartSetState((prev) => ({
268+
...prev,
269+
messages: [...prev.messages, message],
270+
}));
271+
};
272+
273+
const addMessageForAgent = (message: UIMessage | Array<UIMessage>) => {
274+
const list = Array.isArray(message) ? message : [message];
275+
const tagged = list.map(tagUiMessage);
276+
void smartSetState((prev) => ({
277+
...prev,
278+
messages: [...prev.messages, ...tagged],
279+
}));
280+
};
281+
282+
const transcriptForAgent = (): Array<ModelMessage> => {
283+
const all = syncStateRef.current.messages || [];
284+
const mine = all.filter((m) => {
285+
const meta = (m as UIMessage).metadata as
286+
| { agentId?: string }
287+
| undefined;
288+
return m.role !== "ui" && meta?.agentId === id;
289+
});
290+
return filterTranscript(mine as Array<UIMessage>);
291+
};
292+
293+
const handleModelResultsForAgent = async (
294+
result: ModelResult,
295+
opts?: { abortSignal?: AbortSignal },
296+
): Promise<Array<UIMessage>> => {
297+
const messages = result?.response?.messages ?? [];
298+
addMessageForAgent(messages as unknown as Array<UIMessage>);
299+
const toolResponses = (await handleToolCall(messages, {
300+
...(opts || {}),
301+
})) as Array<ModelMessage>;
302+
addMessageForAgent(toolResponses as unknown as Array<UIMessage>);
303+
return (toolResponses as unknown as Array<UIMessage>) || [];
304+
};
305+
306+
const setName = (newName: string) => {
307+
agentsRef.current.set(id, newName);
308+
void smartSetState((prev) => ({
309+
...prev,
310+
agentNames: { ...(prev.agentNames || {}), [id]: newName },
311+
}));
312+
};
313+
314+
return {
315+
id,
316+
name,
317+
say: sayForAgent,
318+
addMessage: addMessageForAgent,
319+
transcript: transcriptForAgent,
320+
handleModelResults: handleModelResultsForAgent,
321+
setName,
322+
} as const;
323+
},
324+
[handleToolCall, smartSetState, syncStateRef],
325+
);
326+
327+
const createAgent = useCallback(
328+
(name: string) => {
329+
const id = crypto.randomUUID();
330+
agentsRef.current.set(id, name);
331+
void smartSetState((prev) => ({
332+
...prev,
333+
agentNames: { ...(prev.agentNames || {}), [id]: name },
334+
}));
335+
return makeAgentHandle(id, name);
336+
},
337+
[agentsRef, makeAgentHandle, smartSetState],
338+
);
339+
340+
const getAgent = useCallback(
341+
(id: string) => {
342+
const name = agentsRef.current.get(id);
343+
if (!name) {
344+
return undefined;
345+
}
346+
return makeAgentHandle(id, name);
347+
},
348+
[agentsRef, makeAgentHandle],
349+
);
350+
248351
const actions = useMemo(
249352
() => ({
250353
say,
@@ -265,6 +368,8 @@ export function useWorkflowActions(params: {
265368
setInputValue,
266369
truncateFromLastMessage,
267370
handleModelResult,
371+
createAgent,
372+
getAgent,
268373
}),
269374
[
270375
say,
@@ -285,6 +390,8 @@ export function useWorkflowActions(params: {
285390
setInputValue,
286391
truncateFromLastMessage,
287392
handleModelResult,
393+
createAgent,
394+
getAgent,
288395
],
289396
);
290397

src/components/chat/hooks/use-workflow-manager.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
createApplyPatchTool,
2626
createUserSelectTool,
2727
} from "../tools/definitions.js";
28-
import { useEffect, useRef, useState } from "react";
28+
import { useEffect, useRef, useState, useMemo } from "react";
2929

3030
export function useWorkflowManager(params: {
3131
initialApprovalPolicy: ApprovalPolicy;
@@ -73,6 +73,7 @@ export function useWorkflowManager(params: {
7373
taskList: [],
7474
statusLine: undefined,
7575
slots: undefined,
76+
agentNames: {},
7677
approvalPolicy: initialApprovalPolicy,
7778
});
7879

@@ -124,10 +125,18 @@ export function useWorkflowManager(params: {
124125
});
125126

126127
const workflowRef = useRef<Workflow | null>(null);
127-
const [displayConfig, setDisplayConfig] = useState<
128+
const [displayConfigRaw, setDisplayConfig] = useState<
128129
Workflow["displayConfig"] | undefined
129130
>(undefined);
130131

132+
const displayConfig = useMemo(() => {
133+
const resolver = (id: string) =>
134+
(syncRef.current.agentNames && syncRef.current.agentNames[id]) || id;
135+
return displayConfigRaw
136+
? { ...displayConfigRaw, agentNameResolver: resolver }
137+
: { agentNameResolver: resolver };
138+
}, [displayConfigRaw, syncRef]);
139+
131140
useEffect(() => {
132141
// Build the workflow once (and when the factory changes). Keep the instance
133142
// stable during transient UI state changes (e.g. confirmation overlays)

0 commit comments

Comments
 (0)