@zaly/agent 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent-BaD2kax9.mjs +68 -0
- package/dist/agent-BaD2kax9.mjs.map +1 -0
- package/dist/agent-GDLdDc2w.mjs +902 -0
- package/dist/agent-GDLdDc2w.mjs.map +1 -0
- package/dist/bash-QNMTQ1PD.mjs +142 -0
- package/dist/bash-QNMTQ1PD.mjs.map +1 -0
- package/dist/compactions-3AjPqjeZ.mjs +190 -0
- package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
- package/dist/edit-Qxl4w1C2.mjs +170 -0
- package/dist/edit-Qxl4w1C2.mjs.map +1 -0
- package/dist/env-UUFv5Mye.mjs +30 -0
- package/dist/env-UUFv5Mye.mjs.map +1 -0
- package/dist/fetch-D8cmmA3E.mjs +111 -0
- package/dist/fetch-D8cmmA3E.mjs.map +1 -0
- package/dist/find-Bah9SKWX.mjs +87 -0
- package/dist/find-Bah9SKWX.mjs.map +1 -0
- package/dist/grep-B2Ax7ewy.mjs +244 -0
- package/dist/grep-B2Ax7ewy.mjs.map +1 -0
- package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
- package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
- package/dist/index.d.mts +1605 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +948 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonl-Dw_VBp46.mjs +134 -0
- package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
- package/dist/manager-D-lWBTHG.mjs +271 -0
- package/dist/manager-D-lWBTHG.mjs.map +1 -0
- package/dist/markdown-Bv4J8cg3.mjs +53 -0
- package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
- package/dist/masker-Bv9KjkDF.mjs +396 -0
- package/dist/masker-Bv9KjkDF.mjs.map +1 -0
- package/dist/model-_3ZA2xqm.mjs +27 -0
- package/dist/model-_3ZA2xqm.mjs.map +1 -0
- package/dist/notify-DpG_m9iP.mjs +96 -0
- package/dist/notify-DpG_m9iP.mjs.map +1 -0
- package/dist/read-Djsx2NZl.mjs +205 -0
- package/dist/read-Djsx2NZl.mjs.map +1 -0
- package/dist/registry-CFmR5G3J.mjs +564 -0
- package/dist/registry-CFmR5G3J.mjs.map +1 -0
- package/dist/search-CRw247ES.mjs +121 -0
- package/dist/search-CRw247ES.mjs.map +1 -0
- package/dist/session/claude.d.mts +59 -0
- package/dist/session/claude.d.mts.map +1 -0
- package/dist/session/claude.mjs +455 -0
- package/dist/session/claude.mjs.map +1 -0
- package/dist/session-CUUdWFC5.mjs +450 -0
- package/dist/session-CUUdWFC5.mjs.map +1 -0
- package/dist/session-Ds5nJz5A.d.mts +346 -0
- package/dist/session-Ds5nJz5A.d.mts.map +1 -0
- package/dist/session.d.mts +37 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +106 -0
- package/dist/session.mjs.map +1 -0
- package/dist/subagent-baqzjRMc.mjs +136 -0
- package/dist/subagent-baqzjRMc.mjs.map +1 -0
- package/dist/swarm-BJ3gQA1Z.mjs +194 -0
- package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
- package/dist/swarm-BuFx_xeY.mjs +109 -0
- package/dist/swarm-BuFx_xeY.mjs.map +1 -0
- package/dist/tasks-D8rARHZ1.mjs +65 -0
- package/dist/tasks-D8rARHZ1.mjs.map +1 -0
- package/dist/tasks-WmC270X2.mjs +615 -0
- package/dist/tasks-WmC270X2.mjs.map +1 -0
- package/dist/tools-DW1dbcCz.mjs +695 -0
- package/dist/tools-DW1dbcCz.mjs.map +1 -0
- package/dist/truncate-BSMXgiXj.mjs +102 -0
- package/dist/truncate-BSMXgiXj.mjs.map +1 -0
- package/dist/uuid-ByXP5SO5.mjs +59 -0
- package/dist/uuid-ByXP5SO5.mjs.map +1 -0
- package/dist/wakeup-BYqTnfSp.mjs +29 -0
- package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
- package/dist/write-D3L-92vj.mjs +89 -0
- package/dist/write-D3L-92vj.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagent-baqzjRMc.mjs","names":[],"sources":["../src/tools/subagent.ts"],"sourcesContent":["import type { MetaPart, Streamable, TextPart, ToolContext, ToolResult } from \"@zaly/ai\"\n\nimport { AiError, defineTool, stringifyContent } from \"@zaly/ai\"\nimport { tmpdir } from \"node:os\"\nimport { join } from \"pathe\"\nimport { Type } from \"typebox\"\nimport { uuidv7 } from \"../utils/uuid.ts\"\n\n/**\n * Spawn a subagent — a fresh `Agent` instance running on the same model\n * with the same permissions, given a focused task and a curated tool\n * list (the parent's, minus the subagent tool itself when the child\n * would otherwise hit `maxDepth`).\n *\n * Returns a Streamable so the harness's existing grace-window machinery\n * promotes long-running subagents to background tasks: the parent gets a\n * placeholder, the model sees task-list / heartbeat updates, and the\n * final assistant message lands as a `task-done` system inject when the\n * child stops naturally.\n *\n * Persistence: each subagent gets its own JSONL session file under\n * `tmpdir`, keyed by id. The final `<subagent>` MetaPart references the\n * path so the parent can re-load the full conversation if it needs more\n * than the surface answer.\n *\n * Auth: the spawned `Agent` shares the parent's `PermissionManager`\n * instance — same workspaces, same rules. (We don't deep-copy yet; if a\n * subagent legitimately needs a tighter scope, that's a future option.)\n */\n\nexport type SubagentMeta = {\n id: string\n depth: number\n durationMs?: number\n sessionPath: string\n /** Lifecycle state — visible to the model in the `<subagent>` MetaPart\n * so it can tell a partial running snapshot apart from a final\n * answer. `running` snapshots accompany text that's still streaming;\n * `done` snapshots carry the final assistant message. */\n status: \"running\" | \"done\"\n /** Stop reason when the child has finished — `\"natural\"` for a clean\n * finish, anything else surfaces in the parent's `<subagent>` block\n * so the model can see *why* the answer might be incomplete. */\n stop?: string\n /** Aggregate token usage across the child's whole run. */\n usage?: { input: number; output: number }\n}\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const subagentTool = defineTool({\n name: \"subagent\",\n desc:\n \"Spawn a focused subagent with the same model and permissions. Use \" +\n \"for tasks that benefit from a clean context window: deep code \" +\n \"exploration, multi-step research, or anything where dragging the \" +\n \"intermediate steps through your own context would be expensive. The \" +\n \"subagent's final answer comes back as the result; full transcript is \" +\n \"available at the returned `sessionPath` if you need more detail.\",\n parallel: true,\n params: Type.Object({\n description: Type.String({\n description:\n \"Short description of what the subagent should do. Shown to the \" +\n \"user in the TUI. Keep under ~10 words.\",\n }),\n prompt: Type.String({\n description:\n \"System prompt for the subagent — defines its role and constraints. \" +\n \"The subagent does NOT inherit your system prompt; spell out what \" +\n \"it needs to know.\",\n }),\n task: Type.String({\n description:\n \"The actual task the subagent should perform, as if you were sending \" +\n \"it as a user message. Be specific about the deliverable.\",\n }),\n }),\n\n async call(args, ctx: ToolContext<SubagentMeta>): Promise<Streamable> {\n const parent = ctx.agent\n if (!parent) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message:\n \"subagent requires an Agent reference on the context (set up by the agent harness).\",\n })\n }\n\n const id = uuidv7()\n const sessionPath = join(tmpdir(), `zaly-subagent-${id}.jsonl`)\n const startedAt = Date.now()\n const { Session } = await import(\"../session/session.ts\")\n\n // `parent.child(...)` handles all the inheritance — cwd, model,\n // permissions, depth + 1, tool list (incl. the loaded skill tool),\n // and the `subagent`-tool filtering at the depth cap.\n const child = await parent.child({\n prompt: [args.prompt],\n session: await Session.load({ path: sessionPath }),\n })\n const depth = child.depth\n\n // Cursor over the child's running text so `poll()` returns only\n // what's new since the last call (mirrors the bash-tool pattern).\n let textBuffer = \"\"\n let cursor = 0\n let finalResult: ToolResult | undefined\n let stopReason: string | undefined\n\n child.on(\"stream-event\", ({ event }) => {\n if (event.type === \"text-delta\" && typeof event.delta === \"string\") {\n textBuffer += event.delta\n }\n })\n child.on(\"stop\", ({ kind }) => {\n stopReason = kind\n })\n\n child.send({ content: args.task, role: \"user\" })\n const runDone = child.run().catch((error: unknown) => {\n stopReason = \"error\"\n throw error\n })\n\n const buildMeta = (running: boolean): SubagentMeta => ({\n depth,\n durationMs: Date.now() - startedAt,\n id,\n sessionPath,\n status: running ? \"running\" : \"done\",\n stop: stopReason,\n usage: { input: child.totalUsage.input, output: child.totalUsage.output },\n })\n\n /** Pull the last assistant text out of the child's session — the\n * user-visible \"answer.\" Falls back to whatever's accumulated in the\n * text buffer if the assistant hasn't committed a message yet. */\n const finalText = (): string => {\n for (let i = child.messages.length - 1; i >= 0; i--) {\n const m = child.messages[i]\n if (m.role !== \"assistant\") continue\n // Assistant content shape excludes Attachment, so it's compatible\n // with `stringifyContent` (which accepts plain Content).\n const text = stringifyContent(m.content as never).trim()\n if (text !== \"\") return text\n }\n return textBuffer.trim()\n }\n\n const buildResult = (running: boolean): ToolResult & { running: boolean } => {\n if (!running && finalResult) return { ...finalResult, running: false }\n\n const meta = buildMeta(running)\n // Live snapshot: emit the meta + only the *new* text since last poll\n // (advancing the cursor), matching the bash tool's increment-poll\n // semantic. The full text is reachable via the session file.\n const slice = textBuffer.slice(cursor)\n cursor = textBuffer.length\n\n // The generic \"still running, more is coming\" notice lives one\n // layer up in `Tasks.#buildPart` (added as a `<task>` MetaPart\n // trailer). Keep this snapshot focused on subagent-specific data\n // — the `<subagent>` envelope + the latest text slice.\n const parts: (MetaPart | TextPart)[] = [{ data: meta, tag: \"subagent\", type: \"meta\" }]\n if (running) {\n if (slice !== \"\") parts.push({ text: slice, type: \"text\" })\n } else {\n const answer = finalText()\n if (answer !== \"\") parts.push({ text: answer, type: \"text\" })\n }\n return { content: parts, isError: false, running }\n }\n\n return {\n abort: () => {\n child.stop()\n },\n done: runDone.then(\n async () => {\n // Flush + close the JSONL writer so the file is fully on disk\n // by the time the parent reads `sessionPath`.\n await child.session.close()\n finalResult = buildResult(false)\n },\n async (error: unknown) => {\n // Surface the error as a subagent result with isError: true.\n // Don't reject the `done` promise — the harness's contract is\n // \"completion is a final snapshot, not a throw.\"\n stopReason ??= \"error\"\n await child.session.close().catch(() => undefined)\n const meta = buildMeta(false)\n const message = error instanceof Error ? error.message : String(error)\n finalResult = {\n content: [\n { data: meta, tag: \"subagent\", type: \"meta\" },\n { text: `subagent failed: ${message}`, type: \"text\" },\n ],\n isError: true,\n }\n }\n ),\n hasNew: () => textBuffer.length > cursor,\n poll: () => buildResult(child.status !== \"idle\" && child.status !== \"paused\"),\n }\n },\n})\n\ndeclare module \"@zaly/ai\" {}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//#region src/swarm.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tree of cooperating agents. One `Swarm` instance per agent tree —
|
|
4
|
+
* the orchestrator (root), every subagent it spawns, every subagent
|
|
5
|
+
* THEY spawn, all live in the same swarm and address each other by
|
|
6
|
+
* `name`.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Register / look up agents in the tree.
|
|
10
|
+
* - Spawn subagents under a parent (delegates inheritance to
|
|
11
|
+
* `parent.child(...)`; the swarm just records the metadata and
|
|
12
|
+
* routes the initial task message).
|
|
13
|
+
* - Route messages between agents (`send(from, to, content)`
|
|
14
|
+
* injects a `<agent>` system message into the receiver carrying
|
|
15
|
+
* the sender's identity).
|
|
16
|
+
* - Tear-down (`stop(id)` aborts the agent and removes the entry).
|
|
17
|
+
*
|
|
18
|
+
* Not yet wired into `Tasks` or the tool catalog — that lands as a
|
|
19
|
+
* follow-up. This module is the data structure + lifecycle.
|
|
20
|
+
*/
|
|
21
|
+
var Swarm = class {
|
|
22
|
+
#entries = /* @__PURE__ */ new Map();
|
|
23
|
+
/** Reverse index `Agent → SwarmEntry`. WeakMap so an unregistered
|
|
24
|
+
* agent's entry is GC'd if the Agent ever goes away on its own;
|
|
25
|
+
* also gives O(1) lookup for `find()` and per-agent attach
|
|
26
|
+
* idempotency without scanning `#entries`. */
|
|
27
|
+
#byAgent = /* @__PURE__ */ new WeakMap();
|
|
28
|
+
/** Look up an entry by id. */
|
|
29
|
+
get(id) {
|
|
30
|
+
return this.#entries.get(id);
|
|
31
|
+
}
|
|
32
|
+
/** Reverse lookup: find an entry by Agent instance. Returns
|
|
33
|
+
* `undefined` for agents that haven't been registered. */
|
|
34
|
+
find(agent) {
|
|
35
|
+
return this.#byAgent.get(agent);
|
|
36
|
+
}
|
|
37
|
+
/** All registered entries (root + every subagent). */
|
|
38
|
+
get entries() {
|
|
39
|
+
return [...this.#entries.values()];
|
|
40
|
+
}
|
|
41
|
+
/** Direct children of the given agent (one level only). Walks the
|
|
42
|
+
* agent-side parent pointer (`agent.parent`) so the swarm stays
|
|
43
|
+
* free of duplicate parent state. */
|
|
44
|
+
children(parent) {
|
|
45
|
+
return [...this.#entries.values()].filter((e) => e.agent.parent === parent);
|
|
46
|
+
}
|
|
47
|
+
/** Register an existing Agent in the swarm. Use this to label the
|
|
48
|
+
* root (the orchestrator that wasn't spawned by `spawn`) with a
|
|
49
|
+
* name + description so other swarm members can address it.
|
|
50
|
+
*
|
|
51
|
+
* The `name` doubles as the entry's id — the swarm resolves
|
|
52
|
+
* collisions by auto-suffixing (`reviewer`, `reviewer-2`, ...).
|
|
53
|
+
* Read the returned `entry.name` to find the actual assigned
|
|
54
|
+
* identity if you care which suffix landed.
|
|
55
|
+
*
|
|
56
|
+
* Idempotent on the same `Agent` instance: re-attaching returns
|
|
57
|
+
* the existing entry, ignoring the new opts. To re-name an agent,
|
|
58
|
+
* `stop()` first then re-attach.
|
|
59
|
+
*
|
|
60
|
+
* Reserved: `name` of `"user"` and the empty string both throw —
|
|
61
|
+
* the former conflicts with the human-sender identity in `<agent>`
|
|
62
|
+
* meta; the latter is a programming error. */
|
|
63
|
+
attach(agent, opts) {
|
|
64
|
+
if (opts.name === "") throw new Error("Swarm.attach: `name` must be non-empty");
|
|
65
|
+
if (opts.name === "user") throw new Error("Swarm.attach: `name` \"user\" is reserved");
|
|
66
|
+
const existing = this.#byAgent.get(agent);
|
|
67
|
+
if (existing) return existing;
|
|
68
|
+
const id = this.#resolveName(opts.name);
|
|
69
|
+
const entry = {
|
|
70
|
+
agent,
|
|
71
|
+
desc: opts.desc,
|
|
72
|
+
id,
|
|
73
|
+
name: id,
|
|
74
|
+
startedAt: Date.now()
|
|
75
|
+
};
|
|
76
|
+
this.#entries.set(id, entry);
|
|
77
|
+
this.#byAgent.set(agent, entry);
|
|
78
|
+
this.#wireForwarding(entry);
|
|
79
|
+
return entry;
|
|
80
|
+
}
|
|
81
|
+
/** Resolve a name to an unused id. Plain `name` if free; otherwise
|
|
82
|
+
* `name-2`, `name-3`, … — next-available, not highest-ever-seen,
|
|
83
|
+
* so a `stop()` frees the name for re-use. */
|
|
84
|
+
#resolveName(name) {
|
|
85
|
+
if (!this.#entries.has(name)) return name;
|
|
86
|
+
let n = 2;
|
|
87
|
+
while (this.#entries.has(`${name}-${n}`)) n++;
|
|
88
|
+
return `${name}-${n}`;
|
|
89
|
+
}
|
|
90
|
+
/** Spawn a new subagent under `parent`. Inheritance (cwd, model,
|
|
91
|
+
* permissions, effective tools) is delegated to `parent.child(...)`;
|
|
92
|
+
* the swarm then attaches the child and (optionally) seeds it with
|
|
93
|
+
* `opts.task` as its first user message. */
|
|
94
|
+
async spawn(parent, opts) {
|
|
95
|
+
const child = await parent.child({ prompt: [opts.prompt] });
|
|
96
|
+
const entry = this.attach(child, {
|
|
97
|
+
desc: opts.desc,
|
|
98
|
+
name: opts.name
|
|
99
|
+
});
|
|
100
|
+
if (opts.task !== void 0 && opts.task !== "") child.send({
|
|
101
|
+
content: opts.task,
|
|
102
|
+
role: "user"
|
|
103
|
+
});
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
/** Send a message from one agent to another. The receiver gets a
|
|
107
|
+
* message tagged `<agent>` with sender metadata (`name` + `id`) so
|
|
108
|
+
* it can parse who's speaking and respond by addressing them back.
|
|
109
|
+
*
|
|
110
|
+
* Role:
|
|
111
|
+
* - `role: "user"` when the sender is the receiver's parent (or
|
|
112
|
+
* the literal `"user"`) — from the receiver's perspective the
|
|
113
|
+
* sender IS its user.
|
|
114
|
+
* - `role: "system"` otherwise (child → parent reports, sibling
|
|
115
|
+
* messages, unregistered senders). */
|
|
116
|
+
send(from, to, content) {
|
|
117
|
+
const meta = this.#senderMeta(from);
|
|
118
|
+
const message = from === "user" || from instanceof Object && to.parent === from ? {
|
|
119
|
+
content,
|
|
120
|
+
role: "user"
|
|
121
|
+
} : {
|
|
122
|
+
content: [meta, {
|
|
123
|
+
text: content,
|
|
124
|
+
type: "text"
|
|
125
|
+
}],
|
|
126
|
+
role: "system"
|
|
127
|
+
};
|
|
128
|
+
to.send(message);
|
|
129
|
+
}
|
|
130
|
+
/** Stop a registered subagent. Aborts its in-flight run and removes
|
|
131
|
+
* the entry. The `Agent` itself is NOT disposed — call
|
|
132
|
+
* `agent.dispose()` separately if you need its resources released. */
|
|
133
|
+
stop(id) {
|
|
134
|
+
const entry = this.#entries.get(id);
|
|
135
|
+
if (!entry) return;
|
|
136
|
+
entry.agent.stop();
|
|
137
|
+
this.#entries.delete(id);
|
|
138
|
+
this.#byAgent.delete(entry.agent);
|
|
139
|
+
}
|
|
140
|
+
/** Stop every registered agent in the swarm. */
|
|
141
|
+
stopAll() {
|
|
142
|
+
const ids = [...this.#entries.keys()];
|
|
143
|
+
for (const id of ids) this.stop(id);
|
|
144
|
+
}
|
|
145
|
+
/** Wire an entry's agent so its outward assistant messages — text
|
|
146
|
+
* parts only, after a natural step-end — auto-forward to the
|
|
147
|
+
* parent via `send(child, parent, …)`. No-op for entries without
|
|
148
|
+
* a parent (the root has nowhere to report).
|
|
149
|
+
*
|
|
150
|
+
* Hooked on `step-end` rather than `stop` so each natural
|
|
151
|
+
* checkpoint reports — important once interactive subagents land
|
|
152
|
+
* (a child can have many natural step-ends across its lifetime). */
|
|
153
|
+
#wireForwarding(entry) {
|
|
154
|
+
const { agent } = entry;
|
|
155
|
+
const parent = agent.parent;
|
|
156
|
+
if (!parent) return;
|
|
157
|
+
agent.on("step-end", ({ outcome }) => {
|
|
158
|
+
if (outcome !== "natural") return;
|
|
159
|
+
const last = agent.messages.at(-1);
|
|
160
|
+
if (last?.role !== "assistant") return;
|
|
161
|
+
const text = assistantText(last.content);
|
|
162
|
+
if (text === "") return;
|
|
163
|
+
this.send(agent, parent, text);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
#senderMeta(from) {
|
|
167
|
+
if (from === "user") return {
|
|
168
|
+
data: { from: "user" },
|
|
169
|
+
tag: "agent",
|
|
170
|
+
type: "meta"
|
|
171
|
+
};
|
|
172
|
+
const entry = this.find(from);
|
|
173
|
+
return {
|
|
174
|
+
data: {
|
|
175
|
+
from: entry?.name ?? "agent",
|
|
176
|
+
id: entry?.id
|
|
177
|
+
},
|
|
178
|
+
tag: "agent",
|
|
179
|
+
type: "meta"
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/** Extract just the visible text from an assistant message — text
|
|
184
|
+
* parts only, no reasoning, no tool calls. The forwarding path uses
|
|
185
|
+
* this so a child's outward report carries the answer without leaking
|
|
186
|
+
* its private working trajectory (thinking, intermediate tool calls). */
|
|
187
|
+
function assistantText(content) {
|
|
188
|
+
if (typeof content === "string") return content.trim();
|
|
189
|
+
return content.filter((p) => p.type === "text").map((p) => p.text).join("\n").trim();
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
export { Swarm };
|
|
193
|
+
|
|
194
|
+
//# sourceMappingURL=swarm-BJ3gQA1Z.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swarm-BJ3gQA1Z.mjs","names":["#entries","#byAgent","#resolveName","#wireForwarding","#senderMeta"],"sources":["../src/swarm.ts"],"sourcesContent":["import type { Message, MetaPart } from \"@zaly/ai\"\nimport type { Agent } from \"./agent.ts\"\n\n/** A registered agent in the swarm — a node in the agent tree. The\n * `name` IS the id: agents address each other by name, so there's no\n * point in maintaining a separate uuid the model would have to\n * remember. Collisions get an auto-suffix (`reviewer`, `reviewer-2`,\n * `reviewer-3`, …) resolved at `attach` time; reading `entry.name`\n * after attach tells the caller which suffix landed. The parent\n * relationship lives on `agent.parent` — single source of truth. */\nexport interface SwarmEntry {\n /** Unique identity within the swarm. Same string as `name` (kept as\n * a separate field for code that wants to be explicit about \"this\n * is the lookup key vs. the displayable name\"). */\n id: string\n /** Resolved name (with collision suffix if needed). What other\n * agents type when addressing this one. */\n name: string\n desc: string\n agent: Agent\n /** Wallclock at registration. */\n startedAt: number\n}\n\n/** Options for spawning a new subagent via `Swarm.spawn`. */\nexport interface SpawnOptions {\n /** Human-readable identity for inter-agent addressing — e.g.\n * `\"reviewer\"`, `\"researcher\"`. Other agents reference the spawned\n * agent by this name in `<agent>` meta tags. */\n name: string\n /** What this agent is doing (one short line). Surfaced in\n * `task_list` / `<agent>` headers so the orchestrator can keep\n * track of who's working on what. */\n desc: string\n /** System prompt for the child. Spelled out from scratch — the child\n * does NOT inherit the parent's prompt. */\n prompt: string\n /** Initial task for the child. When set, the swarm sends it as the\n * child's first user message right after spawn. Omit to spawn an\n * agent that idles waiting for the first `send`. */\n task?: string\n}\n\n/**\n * Tree of cooperating agents. One `Swarm` instance per agent tree —\n * the orchestrator (root), every subagent it spawns, every subagent\n * THEY spawn, all live in the same swarm and address each other by\n * `name`.\n *\n * Responsibilities:\n * - Register / look up agents in the tree.\n * - Spawn subagents under a parent (delegates inheritance to\n * `parent.child(...)`; the swarm just records the metadata and\n * routes the initial task message).\n * - Route messages between agents (`send(from, to, content)`\n * injects a `<agent>` system message into the receiver carrying\n * the sender's identity).\n * - Tear-down (`stop(id)` aborts the agent and removes the entry).\n *\n * Not yet wired into `Tasks` or the tool catalog — that lands as a\n * follow-up. This module is the data structure + lifecycle.\n */\nexport class Swarm {\n readonly #entries = new Map<string, SwarmEntry>()\n /** Reverse index `Agent → SwarmEntry`. WeakMap so an unregistered\n * agent's entry is GC'd if the Agent ever goes away on its own;\n * also gives O(1) lookup for `find()` and per-agent attach\n * idempotency without scanning `#entries`. */\n readonly #byAgent = new WeakMap<Agent, SwarmEntry>()\n\n // ── Lookup ───────────────────────────────────────────────────────────\n\n /** Look up an entry by id. */\n get(id: string): SwarmEntry | undefined {\n return this.#entries.get(id)\n }\n\n /** Reverse lookup: find an entry by Agent instance. Returns\n * `undefined` for agents that haven't been registered. */\n find(agent: Agent): SwarmEntry | undefined {\n return this.#byAgent.get(agent)\n }\n\n /** All registered entries (root + every subagent). */\n get entries(): readonly SwarmEntry[] {\n return [...this.#entries.values()]\n }\n\n /** Direct children of the given agent (one level only). Walks the\n * agent-side parent pointer (`agent.parent`) so the swarm stays\n * free of duplicate parent state. */\n children(parent: Agent): SwarmEntry[] {\n return [...this.#entries.values()].filter((e) => e.agent.parent === parent)\n }\n\n // ── Lifecycle ────────────────────────────────────────────────────────\n\n /** Register an existing Agent in the swarm. Use this to label the\n * root (the orchestrator that wasn't spawned by `spawn`) with a\n * name + description so other swarm members can address it.\n *\n * The `name` doubles as the entry's id — the swarm resolves\n * collisions by auto-suffixing (`reviewer`, `reviewer-2`, ...).\n * Read the returned `entry.name` to find the actual assigned\n * identity if you care which suffix landed.\n *\n * Idempotent on the same `Agent` instance: re-attaching returns\n * the existing entry, ignoring the new opts. To re-name an agent,\n * `stop()` first then re-attach.\n *\n * Reserved: `name` of `\"user\"` and the empty string both throw —\n * the former conflicts with the human-sender identity in `<agent>`\n * meta; the latter is a programming error. */\n attach(agent: Agent, opts: { name: string; desc: string }): SwarmEntry {\n if (opts.name === \"\") throw new Error(\"Swarm.attach: `name` must be non-empty\")\n if (opts.name === \"user\") throw new Error('Swarm.attach: `name` \"user\" is reserved')\n const existing = this.#byAgent.get(agent)\n if (existing) return existing\n const id = this.#resolveName(opts.name)\n const entry: SwarmEntry = {\n agent,\n desc: opts.desc,\n id,\n name: id,\n startedAt: Date.now(),\n }\n this.#entries.set(id, entry)\n this.#byAgent.set(agent, entry)\n this.#wireForwarding(entry)\n return entry\n }\n\n /** Resolve a name to an unused id. Plain `name` if free; otherwise\n * `name-2`, `name-3`, … — next-available, not highest-ever-seen,\n * so a `stop()` frees the name for re-use. */\n #resolveName(name: string): string {\n if (!this.#entries.has(name)) return name\n let n = 2\n while (this.#entries.has(`${name}-${n}`)) n++\n return `${name}-${n}`\n }\n\n /** Spawn a new subagent under `parent`. Inheritance (cwd, model,\n * permissions, effective tools) is delegated to `parent.child(...)`;\n * the swarm then attaches the child and (optionally) seeds it with\n * `opts.task` as its first user message. */\n async spawn(parent: Agent, opts: SpawnOptions): Promise<SwarmEntry> {\n const child = await parent.child({ prompt: [opts.prompt] })\n const entry = this.attach(child, { desc: opts.desc, name: opts.name })\n if (opts.task !== undefined && opts.task !== \"\") {\n child.send({ content: opts.task, role: \"user\" })\n }\n return entry\n }\n\n /** Send a message from one agent to another. The receiver gets a\n * message tagged `<agent>` with sender metadata (`name` + `id`) so\n * it can parse who's speaking and respond by addressing them back.\n *\n * Role:\n * - `role: \"user\"` when the sender is the receiver's parent (or\n * the literal `\"user\"`) — from the receiver's perspective the\n * sender IS its user.\n * - `role: \"system\"` otherwise (child → parent reports, sibling\n * messages, unregistered senders). */\n send(from: Agent | \"user\", to: Agent, content: string): void {\n const meta = this.#senderMeta(from)\n const isUser = from === \"user\" || (from instanceof Object && to.parent === from)\n const message: Message<\"user\" | \"system\"> = isUser\n ? { content, role: \"user\" }\n : { content: [meta, { text: content, type: \"text\" }], role: \"system\" }\n to.send(message)\n }\n\n /** Stop a registered subagent. Aborts its in-flight run and removes\n * the entry. The `Agent` itself is NOT disposed — call\n * `agent.dispose()` separately if you need its resources released. */\n stop(id: string): void {\n const entry = this.#entries.get(id)\n if (!entry) return\n entry.agent.stop()\n this.#entries.delete(id)\n this.#byAgent.delete(entry.agent)\n }\n\n /** Stop every registered agent in the swarm. */\n stopAll(): void {\n // Snapshot ids first — `stop()` mutates `#entries`, and modifying a\n // Map mid-iteration is unsafe.\n const ids = [...this.#entries.keys()]\n for (const id of ids) this.stop(id)\n }\n\n // ── Internals ────────────────────────────────────────────────────────\n\n /** Wire an entry's agent so its outward assistant messages — text\n * parts only, after a natural step-end — auto-forward to the\n * parent via `send(child, parent, …)`. No-op for entries without\n * a parent (the root has nowhere to report).\n *\n * Hooked on `step-end` rather than `stop` so each natural\n * checkpoint reports — important once interactive subagents land\n * (a child can have many natural step-ends across its lifetime). */\n #wireForwarding(entry: SwarmEntry): void {\n const { agent } = entry\n const parent = agent.parent\n if (!parent) return\n agent.on(\"step-end\", ({ outcome }) => {\n if (outcome !== \"natural\") return\n const last = agent.messages.at(-1)\n if (last?.role !== \"assistant\") return\n const text = assistantText(last.content)\n if (text === \"\") return\n this.send(agent, parent, text)\n })\n }\n\n #senderMeta(from: Agent | \"user\"): MetaPart {\n if (from === \"user\") {\n return { data: { from: \"user\" }, tag: \"agent\", type: \"meta\" }\n }\n const entry = this.find(from)\n return {\n data: {\n from: entry?.name ?? \"agent\",\n id: entry?.id,\n },\n tag: \"agent\",\n type: \"meta\",\n }\n }\n}\n\n/** Extract just the visible text from an assistant message — text\n * parts only, no reasoning, no tool calls. The forwarding path uses\n * this so a child's outward report carries the answer without leaking\n * its private working trajectory (thinking, intermediate tool calls). */\nfunction assistantText(content: Message<\"assistant\">[\"content\"]): string {\n if (typeof content === \"string\") return content.trim()\n return content\n .filter((p) => p.type === \"text\")\n .map((p) => p.text)\n .join(\"\\n\")\n .trim()\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AiError, defineTool } from "@zaly/ai";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
//#region src/tools/swarm.ts
|
|
4
|
+
/**
|
|
5
|
+
* Swarm tools — `agent_spawn` and `agent_send`.
|
|
6
|
+
*
|
|
7
|
+
* Both require `ctx.agent` (the running agent, set by the harness)
|
|
8
|
+
* and `ctx.swarm` (the swarm registry, set when the agent was
|
|
9
|
+
* constructed with one). They throw `MISSING_TOOL_CONTEXT` cleanly
|
|
10
|
+
* otherwise so the model sees a useful error rather than a silent
|
|
11
|
+
* no-op.
|
|
12
|
+
*
|
|
13
|
+
* Lifecycle: `agent_spawn` returns immediately with a small ack — no
|
|
14
|
+
* streamable, no grace race. The spawned subagent runs in the
|
|
15
|
+
* background; its outward messages flow back to the parent through
|
|
16
|
+
* the swarm's auto-forward (a `<agent>` system message lands in the
|
|
17
|
+
* parent's queue at each natural step-end). The orchestrator decides
|
|
18
|
+
* what to do next when it sees those.
|
|
19
|
+
*
|
|
20
|
+
* `agent_send` is the back-channel: the orchestrator addresses a
|
|
21
|
+
* subagent by name and the message lands as a `role: "user"` inject
|
|
22
|
+
* (the orchestrator IS the user from the child's perspective).
|
|
23
|
+
*/
|
|
24
|
+
const agentSpawnTool = defineTool({
|
|
25
|
+
name: "agent_spawn",
|
|
26
|
+
desc: "Spawn a subagent under your supervision. The new agent inherits your model, permissions, and tools (cwd, skill catalog, etc.). It runs in the background; its outward messages arrive in your queue as system messages tagged `<agent>` whenever it reaches a natural stop. Use `agent_send` to follow up.",
|
|
27
|
+
parallel: true,
|
|
28
|
+
params: Type.Object({
|
|
29
|
+
name: Type.String({
|
|
30
|
+
description: "Short identifier for inter-agent addressing — e.g. `\"reviewer\"`, `\"researcher\"`. Other agents reference this subagent by name. The swarm auto-suffixes (`reviewer-2`, `reviewer-3`, ...) on collision, so feel free to reuse role names.",
|
|
31
|
+
minLength: 1
|
|
32
|
+
}),
|
|
33
|
+
desc: Type.String({ description: "One-line description of what this subagent is doing. Surfaces in agent listings so you (and humans observing) can keep track." }),
|
|
34
|
+
prompt: Type.String({ description: "System prompt for the subagent — defines its role and constraints. Spell out what it needs to know; the subagent does NOT inherit your prompt." }),
|
|
35
|
+
task: Type.Optional(Type.String({ description: "Initial task for the subagent, sent as its first user message. Omit to spawn an idle subagent that will do nothing until you `agent_send` to it." }))
|
|
36
|
+
}),
|
|
37
|
+
async call(args, ctx) {
|
|
38
|
+
if (!ctx.agent) throw new AiError({
|
|
39
|
+
code: "MISSING_TOOL_CONTEXT",
|
|
40
|
+
message: "agent_spawn requires an Agent reference on the context (set up by the agent harness)."
|
|
41
|
+
});
|
|
42
|
+
const swarm = await ctx.swarm?.();
|
|
43
|
+
if (!swarm) throw new AiError({
|
|
44
|
+
code: "MISSING_TOOL_CONTEXT",
|
|
45
|
+
message: "agent_spawn requires a Swarm registry. Construct the root agent with `Agent.load({ swarm: new Swarm(), ... })` to enable subagent orchestration."
|
|
46
|
+
});
|
|
47
|
+
const entry = await swarm.spawn(ctx.agent, {
|
|
48
|
+
desc: args.desc,
|
|
49
|
+
name: args.name,
|
|
50
|
+
prompt: args.prompt,
|
|
51
|
+
task: args.task
|
|
52
|
+
});
|
|
53
|
+
return [{
|
|
54
|
+
data: {
|
|
55
|
+
desc: entry.desc,
|
|
56
|
+
name: entry.name
|
|
57
|
+
},
|
|
58
|
+
tag: "agent",
|
|
59
|
+
type: "meta"
|
|
60
|
+
}, {
|
|
61
|
+
text: `spawned subagent "${entry.name}".`,
|
|
62
|
+
type: "text"
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const agentSendTool = defineTool({
|
|
67
|
+
name: "agent_send",
|
|
68
|
+
desc: "Send a message to a subagent by name. The receiver sees it as a user message and runs (resuming if idle). Their reply will arrive in your queue as a system message at their next natural stop. Use this to steer a subagent mid-task or to follow up on a report.",
|
|
69
|
+
parallel: true,
|
|
70
|
+
params: Type.Object({
|
|
71
|
+
to: Type.String({
|
|
72
|
+
description: "Name of the recipient subagent (the `name` it was given when spawned, including any auto-suffix). Must be a subagent in your swarm.",
|
|
73
|
+
minLength: 1
|
|
74
|
+
}),
|
|
75
|
+
content: Type.String({
|
|
76
|
+
description: "Message body — what you want the subagent to do or know.",
|
|
77
|
+
minLength: 1
|
|
78
|
+
})
|
|
79
|
+
}),
|
|
80
|
+
async call(args, ctx) {
|
|
81
|
+
if (!ctx.agent) throw new AiError({
|
|
82
|
+
code: "MISSING_TOOL_CONTEXT",
|
|
83
|
+
message: "agent_send requires an Agent reference on the context."
|
|
84
|
+
});
|
|
85
|
+
const swarm = await ctx.swarm?.();
|
|
86
|
+
if (!swarm) throw new AiError({
|
|
87
|
+
code: "MISSING_TOOL_CONTEXT",
|
|
88
|
+
message: "agent_send requires a Swarm registry on the context."
|
|
89
|
+
});
|
|
90
|
+
const target = swarm.get(args.to);
|
|
91
|
+
if (!target) {
|
|
92
|
+
const known = swarm.entries.map((e) => e.name).filter((n) => n !== swarm.find(ctx.agent)?.name);
|
|
93
|
+
throw new AiError({
|
|
94
|
+
code: "UNKNOWN_AGENT",
|
|
95
|
+
data: {
|
|
96
|
+
available: known,
|
|
97
|
+
to: args.to
|
|
98
|
+
},
|
|
99
|
+
message: known.length === 0 ? `no agent named "${args.to}". No subagents are currently in the swarm.` : `no agent named "${args.to}". Available: ${known.join(", ")}.`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
swarm.send(ctx.agent, target.agent, args.content);
|
|
103
|
+
return `sent to ${target.name}.`;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
//#endregion
|
|
107
|
+
export { agentSendTool, agentSpawnTool };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=swarm-BuFx_xeY.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swarm-BuFx_xeY.mjs","names":[],"sources":["../src/tools/swarm.ts"],"sourcesContent":["import type { MetaPart, TextPart } from \"@zaly/ai\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\n\n/**\n * Swarm tools — `agent_spawn` and `agent_send`.\n *\n * Both require `ctx.agent` (the running agent, set by the harness)\n * and `ctx.swarm` (the swarm registry, set when the agent was\n * constructed with one). They throw `MISSING_TOOL_CONTEXT` cleanly\n * otherwise so the model sees a useful error rather than a silent\n * no-op.\n *\n * Lifecycle: `agent_spawn` returns immediately with a small ack — no\n * streamable, no grace race. The spawned subagent runs in the\n * background; its outward messages flow back to the parent through\n * the swarm's auto-forward (a `<agent>` system message lands in the\n * parent's queue at each natural step-end). The orchestrator decides\n * what to do next when it sees those.\n *\n * `agent_send` is the back-channel: the orchestrator addresses a\n * subagent by name and the message lands as a `role: \"user\"` inject\n * (the orchestrator IS the user from the child's perspective).\n */\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const agentSpawnTool = defineTool({\n name: \"agent_spawn\",\n desc:\n \"Spawn a subagent under your supervision. The new agent inherits \" +\n \"your model, permissions, and tools (cwd, skill catalog, etc.). \" +\n \"It runs in the background; its outward messages arrive in your \" +\n \"queue as system messages tagged `<agent>` whenever it reaches a \" +\n \"natural stop. Use `agent_send` to follow up.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n name: Type.String({\n description:\n 'Short identifier for inter-agent addressing — e.g. `\"reviewer\"`, ' +\n '`\"researcher\"`. Other agents reference this subagent by name. ' +\n \"The swarm auto-suffixes (`reviewer-2`, `reviewer-3`, ...) on \" +\n \"collision, so feel free to reuse role names.\",\n minLength: 1,\n }),\n desc: Type.String({\n description:\n \"One-line description of what this subagent is doing. Surfaces \" +\n \"in agent listings so you (and humans observing) can keep track.\",\n }),\n prompt: Type.String({\n description:\n \"System prompt for the subagent — defines its role and \" +\n \"constraints. Spell out what it needs to know; the subagent \" +\n \"does NOT inherit your prompt.\",\n }),\n task: Type.Optional(\n Type.String({\n description:\n \"Initial task for the subagent, sent as its first user \" +\n \"message. Omit to spawn an idle subagent that will do \" +\n \"nothing until you `agent_send` to it.\",\n })\n ),\n }),\n\n async call(args, ctx): Promise<(MetaPart | TextPart)[]> {\n if (!ctx.agent) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message:\n \"agent_spawn requires an Agent reference on the context (set up by the agent harness).\",\n })\n }\n const swarm = await ctx.swarm?.()\n if (!swarm) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message:\n \"agent_spawn requires a Swarm registry. Construct the root agent with \" +\n \"`Agent.load({ swarm: new Swarm(), ... })` to enable subagent orchestration.\",\n })\n }\n const entry = await swarm.spawn(ctx.agent, {\n desc: args.desc,\n name: args.name,\n prompt: args.prompt,\n task: args.task,\n })\n return [\n {\n data: { desc: entry.desc, name: entry.name },\n tag: \"agent\",\n type: \"meta\",\n },\n { text: `spawned subagent \"${entry.name}\".`, type: \"text\" },\n ]\n },\n})\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const agentSendTool = defineTool({\n name: \"agent_send\",\n desc:\n \"Send a message to a subagent by name. The receiver sees it as a \" +\n \"user message and runs (resuming if idle). Their reply will arrive \" +\n \"in your queue as a system message at their next natural stop. \" +\n \"Use this to steer a subagent mid-task or to follow up on a report.\",\n parallel: true,\n // oxlint-disable-next-line sort-keys -- semantic param order\n params: Type.Object({\n to: Type.String({\n description:\n \"Name of the recipient subagent (the `name` it was given when \" +\n \"spawned, including any auto-suffix). Must be a subagent in \" +\n \"your swarm.\",\n minLength: 1,\n }),\n content: Type.String({\n description: \"Message body — what you want the subagent to do or know.\",\n minLength: 1,\n }),\n }),\n\n async call(args, ctx): Promise<string> {\n if (!ctx.agent) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message: \"agent_send requires an Agent reference on the context.\",\n })\n }\n const swarm = await ctx.swarm?.()\n if (!swarm) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message: \"agent_send requires a Swarm registry on the context.\",\n })\n }\n const target = swarm.get(args.to)\n if (!target) {\n const known = swarm.entries\n .map((e) => e.name)\n .filter((n) => n !== swarm.find(ctx.agent!)?.name)\n throw new AiError({\n code: \"UNKNOWN_AGENT\",\n data: { available: known, to: args.to },\n message:\n known.length === 0\n ? `no agent named \"${args.to}\". No subagents are currently in the swarm.`\n : `no agent named \"${args.to}\". Available: ${known.join(\", \")}.`,\n })\n }\n swarm.send(ctx.agent, target.agent, args.content)\n return `sent to ${target.name}.`\n },\n})\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,iBAAiB,WAAW;CACvC,MAAM;CACN,MACE;CAKF,UAAU;CAEV,QAAQ,KAAK,OAAO;EAClB,MAAM,KAAK,OAAO;GAChB,aACE;GAIF,WAAW;EACb,CAAC;EACD,MAAM,KAAK,OAAO,EAChB,aACE,gIAEJ,CAAC;EACD,QAAQ,KAAK,OAAO,EAClB,aACE,iJAGJ,CAAC;EACD,MAAM,KAAK,SACT,KAAK,OAAO,EACV,aACE,mJAGJ,CAAC,CACH;CACF,CAAC;CAED,MAAM,KAAK,MAAM,KAAuC;EACtD,IAAI,CAAC,IAAI,OACP,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SACE;EACJ,CAAC;EAEH,MAAM,QAAQ,MAAM,IAAI,QAAQ;EAChC,IAAI,CAAC,OACH,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SACE;EAEJ,CAAC;EAEH,MAAM,QAAQ,MAAM,MAAM,MAAM,IAAI,OAAO;GACzC,MAAM,KAAK;GACX,MAAM,KAAK;GACX,QAAQ,KAAK;GACb,MAAM,KAAK;EACb,CAAC;EACD,OAAO,CACL;GACE,MAAM;IAAE,MAAM,MAAM;IAAM,MAAM,MAAM;GAAK;GAC3C,KAAK;GACL,MAAM;EACR,GACA;GAAE,MAAM,qBAAqB,MAAM,KAAK;GAAK,MAAM;EAAO,CAC5D;CACF;AACF,CAAC;AAGD,MAAa,gBAAgB,WAAW;CACtC,MAAM;CACN,MACE;CAIF,UAAU;CAEV,QAAQ,KAAK,OAAO;EAClB,IAAI,KAAK,OAAO;GACd,aACE;GAGF,WAAW;EACb,CAAC;EACD,SAAS,KAAK,OAAO;GACnB,aAAa;GACb,WAAW;EACb,CAAC;CACH,CAAC;CAED,MAAM,KAAK,MAAM,KAAsB;EACrC,IAAI,CAAC,IAAI,OACP,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SAAS;EACX,CAAC;EAEH,MAAM,QAAQ,MAAM,IAAI,QAAQ;EAChC,IAAI,CAAC,OACH,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,SAAS;EACX,CAAC;EAEH,MAAM,SAAS,MAAM,IAAI,KAAK,EAAE;EAChC,IAAI,CAAC,QAAQ;GACX,MAAM,QAAQ,MAAM,QACjB,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,QAAQ,MAAM,MAAM,MAAM,KAAK,IAAI,KAAM,CAAC,EAAE,IAAI;GACnD,MAAM,IAAI,QAAQ;IAChB,MAAM;IACN,MAAM;KAAE,WAAW;KAAO,IAAI,KAAK;IAAG;IACtC,SACE,MAAM,WAAW,IACb,mBAAmB,KAAK,GAAG,+CAC3B,mBAAmB,KAAK,GAAG,gBAAgB,MAAM,KAAK,IAAI,EAAE;GACpE,CAAC;EACH;EACA,MAAM,KAAK,IAAI,OAAO,OAAO,OAAO,KAAK,OAAO;EAChD,OAAO,WAAW,OAAO,KAAK;CAChC;AACF,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { r as taskInfoPart } from "./tasks-WmC270X2.mjs";
|
|
2
|
+
import { AiError, defineTool } from "@zaly/ai";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
//#region src/tools/tasks.ts
|
|
5
|
+
/**
|
|
6
|
+
* Generic task management surface for the model.
|
|
7
|
+
*
|
|
8
|
+
* - `task_list`: enumerate running and recently-finished tasks.
|
|
9
|
+
* - `task_stop`: abort a running task. Idempotent.
|
|
10
|
+
*
|
|
11
|
+
* "Wait for a task" is intentionally absent — heartbeats keep the loop
|
|
12
|
+
* alive while tasks run, and `task-done` injects the final result as a
|
|
13
|
+
* system message automatically. If the model wants explicit polling
|
|
14
|
+
* cadence on something specific, it schedules a `wakeup` instead.
|
|
15
|
+
*
|
|
16
|
+
* Permission gating, when wired up, is the harness's concern; these
|
|
17
|
+
* tools are intentionally thin so the policy lives one layer up.
|
|
18
|
+
*/
|
|
19
|
+
const taskListTool = defineTool({
|
|
20
|
+
name: "task_list",
|
|
21
|
+
desc: "List active and recently-finished tasks (running shells, subagents, any other long-running work the model has kicked off). Use this when you've lost track of an id, or to check which background work is still in flight.",
|
|
22
|
+
parallel: true,
|
|
23
|
+
params: Type.Object({ includeFinished: Type.Optional(Type.Boolean({
|
|
24
|
+
default: false,
|
|
25
|
+
description: "When true, also include tasks that have already completed in this session."
|
|
26
|
+
})) }),
|
|
27
|
+
call(args, ctx) {
|
|
28
|
+
return taskInfoPart(requireTasks(ctx).info().filter((task) => (args.includeFinished ?? false) || task.status !== "done"));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const taskPollTool = defineTool({
|
|
32
|
+
name: "task_poll",
|
|
33
|
+
desc: "Read the latest incremental output from a running task. Returns only what's arrived since the previous `task_poll` (or since the task's running placeholder was first surfaced). Use when a heartbeat flagged a task with `*new*`, or to peek at progress on a long command (build, test run, tail -f). Errors if the task is already done (its result was injected as a system message) or has no streaming output.",
|
|
34
|
+
parallel: true,
|
|
35
|
+
params: Type.Object({ id: Type.String({ description: "Task id to poll." }) }),
|
|
36
|
+
call(args, ctx) {
|
|
37
|
+
return requireTasks(ctx).pollOutput(args.id).content;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
const taskStopTool = defineTool({
|
|
41
|
+
name: "task_stop",
|
|
42
|
+
desc: "Stop a running task. Aborts the underlying work (e.g. SIGTERM → SIGKILL for bash); pending tasks chained behind a stopped task get auto-cancelled. Idempotent — safe to call on an already-finished task.",
|
|
43
|
+
params: Type.Object({ id: Type.String({ description: "Task id to stop." }) }),
|
|
44
|
+
call(args, ctx) {
|
|
45
|
+
const tasks = requireTasks(ctx);
|
|
46
|
+
if (!tasks.get(args.id)) throw new AiError({
|
|
47
|
+
code: "NOT_FOUND",
|
|
48
|
+
data: { id: args.id },
|
|
49
|
+
message: `no task with id "${args.id}"`
|
|
50
|
+
});
|
|
51
|
+
tasks.abort(args.id, "stopped via task_stop");
|
|
52
|
+
return `task ${args.id} stopped`;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
function requireTasks(ctx) {
|
|
56
|
+
if (!ctx.tasks) throw new AiError({
|
|
57
|
+
code: "MISSING_TOOL_CONTEXT",
|
|
58
|
+
message: "task tools require a Tasks registry on the context (set up by the agent harness)."
|
|
59
|
+
});
|
|
60
|
+
return ctx.tasks;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { taskListTool, taskPollTool, taskStopTool };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=tasks-D8rARHZ1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-D8rARHZ1.mjs","names":[],"sources":["../src/tools/tasks.ts"],"sourcesContent":["import type { ToolContext } from \"@zaly/ai\"\nimport type { Tasks } from \"../tasks.ts\"\n\nimport { defineTool, AiError } from \"@zaly/ai\"\nimport { Type } from \"typebox\"\nimport { taskInfoPart } from \"../tasks.ts\"\n\n/**\n * Generic task management surface for the model.\n *\n * - `task_list`: enumerate running and recently-finished tasks.\n * - `task_stop`: abort a running task. Idempotent.\n *\n * \"Wait for a task\" is intentionally absent — heartbeats keep the loop\n * alive while tasks run, and `task-done` injects the final result as a\n * system message automatically. If the model wants explicit polling\n * cadence on something specific, it schedules a `wakeup` instead.\n *\n * Permission gating, when wired up, is the harness's concern; these\n * tools are intentionally thin so the policy lives one layer up.\n */\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const taskListTool = defineTool({\n name: \"task_list\",\n desc:\n \"List active and recently-finished tasks (running shells, subagents, \" +\n \"any other long-running work the model has kicked off). Use this when \" +\n \"you've lost track of an id, or to check which background work is \" +\n \"still in flight.\",\n parallel: true,\n params: Type.Object({\n includeFinished: Type.Optional(\n Type.Boolean({\n default: false,\n description: \"When true, also include tasks that have already completed in this session.\",\n })\n ),\n }),\n\n call(args, ctx) {\n const tasks = requireTasks(ctx)\n // Routes through `taskInfoPart` so the `result` strip + line-per-task\n // JSON format stays consistent with heartbeat output. Done tasks\n // surface only their identity / timing, not their (potentially huge)\n // captured body.\n const info = tasks\n .info()\n .filter((task) => (args.includeFinished ?? false) || task.status !== \"done\")\n return taskInfoPart(info)\n },\n})\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const taskPollTool = defineTool({\n name: \"task_poll\",\n desc:\n \"Read the latest incremental output from a running task. Returns \" +\n \"only what's arrived since the previous `task_poll` (or since the \" +\n \"task's running placeholder was first surfaced). Use when a heartbeat \" +\n \"flagged a task with `*new*`, or to peek at progress on a long \" +\n \"command (build, test run, tail -f). Errors if the task is already \" +\n \"done (its result was injected as a system message) or has no \" +\n \"streaming output.\",\n parallel: true,\n params: Type.Object({\n id: Type.String({ description: \"Task id to poll.\" }),\n }),\n\n call(args, ctx) {\n const tasks = requireTasks(ctx)\n const snap = tasks.pollOutput(args.id)\n // The snapshot's `content` already carries any streamable-emitted\n // MetaParts (bash's `<shell>`, etc.), so the model sees status +\n // incremental output in one go. Returning content directly avoids\n // the JSON-stringify path normalize would take on a ToolResult shape.\n return snap.content\n },\n})\n\n// oxlint-disable-next-line sort-keys -- semantic field order: name, desc, params, call\nexport const taskStopTool = defineTool({\n name: \"task_stop\",\n desc:\n \"Stop a running task. Aborts the underlying work (e.g. SIGTERM \" +\n \"→ SIGKILL for bash); pending tasks chained behind a stopped task get \" +\n \"auto-cancelled. Idempotent — safe to call on an already-finished task.\",\n params: Type.Object({\n id: Type.String({ description: \"Task id to stop.\" }),\n }),\n\n call(args, ctx) {\n const tasks = requireTasks(ctx)\n const task = tasks.get(args.id)\n if (!task) {\n throw new AiError({\n code: \"NOT_FOUND\",\n data: { id: args.id },\n message: `no task with id \"${args.id}\"`,\n })\n }\n tasks.abort(args.id, \"stopped via task_stop\")\n return `task ${args.id} stopped`\n },\n})\n\n// ── Internals ──────────────────────────────────────────────────────────\n\nfunction requireTasks(ctx: ToolContext): Tasks {\n if (!ctx.tasks) {\n throw new AiError({\n code: \"MISSING_TOOL_CONTEXT\",\n message: \"task tools require a Tasks registry on the context (set up by the agent harness).\",\n })\n }\n return ctx.tasks\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuBA,MAAa,eAAe,WAAW;CACrC,MAAM;CACN,MACE;CAIF,UAAU;CACV,QAAQ,KAAK,OAAO,EAClB,iBAAiB,KAAK,SACpB,KAAK,QAAQ;EACX,SAAS;EACT,aAAa;CACf,CAAC,CACH,EACF,CAAC;CAED,KAAK,MAAM,KAAK;EASd,OAAO,aARO,aAAa,GAKV,CAAC,CACf,KAAK,CAAC,CACN,QAAQ,UAAU,KAAK,mBAAmB,UAAU,KAAK,WAAW,MAChD,CAAC;CAC1B;AACF,CAAC;AAGD,MAAa,eAAe,WAAW;CACrC,MAAM;CACN,MACE;CAOF,UAAU;CACV,QAAQ,KAAK,OAAO,EAClB,IAAI,KAAK,OAAO,EAAE,aAAa,mBAAmB,CAAC,EACrD,CAAC;CAED,KAAK,MAAM,KAAK;EAOd,OANc,aAAa,GACV,CAAC,CAAC,WAAW,KAAK,EAKzB,CAAC,CAAC;CACd;AACF,CAAC;AAGD,MAAa,eAAe,WAAW;CACrC,MAAM;CACN,MACE;CAGF,QAAQ,KAAK,OAAO,EAClB,IAAI,KAAK,OAAO,EAAE,aAAa,mBAAmB,CAAC,EACrD,CAAC;CAED,KAAK,MAAM,KAAK;EACd,MAAM,QAAQ,aAAa,GAAG;EAE9B,IAAI,CADS,MAAM,IAAI,KAAK,EACpB,GACN,MAAM,IAAI,QAAQ;GAChB,MAAM;GACN,MAAM,EAAE,IAAI,KAAK,GAAG;GACpB,SAAS,oBAAoB,KAAK,GAAG;EACvC,CAAC;EAEH,MAAM,MAAM,KAAK,IAAI,uBAAuB;EAC5C,OAAO,QAAQ,KAAK,GAAG;CACzB;AACF,CAAC;AAID,SAAS,aAAa,KAAyB;CAC7C,IAAI,CAAC,IAAI,OACP,MAAM,IAAI,QAAQ;EAChB,MAAM;EACN,SAAS;CACX,CAAC;CAEH,OAAO,IAAI;AACb"}
|