anyclaude-sdk 0.1.0

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.
Files changed (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/dist/agent.d.ts +110 -0
  4. package/dist/agent.js +897 -0
  5. package/dist/background/index.d.ts +3 -0
  6. package/dist/background/index.js +9 -0
  7. package/dist/background/manager.d.ts +32 -0
  8. package/dist/background/manager.js +108 -0
  9. package/dist/background/tools.d.ts +5 -0
  10. package/dist/background/tools.js +98 -0
  11. package/dist/background/worker.d.ts +19 -0
  12. package/dist/background/worker.js +30 -0
  13. package/dist/commands/builtins.d.ts +2 -0
  14. package/dist/commands/builtins.js +306 -0
  15. package/dist/commands/index.d.ts +21 -0
  16. package/dist/commands/index.js +56 -0
  17. package/dist/commands/types.d.ts +110 -0
  18. package/dist/commands/types.js +5 -0
  19. package/dist/compact.d.ts +22 -0
  20. package/dist/compact.js +67 -0
  21. package/dist/fs/dexie.d.ts +57 -0
  22. package/dist/fs/dexie.js +243 -0
  23. package/dist/fs/index.d.ts +4 -0
  24. package/dist/fs/index.js +13 -0
  25. package/dist/fs/linuxTree.d.ts +11 -0
  26. package/dist/fs/linuxTree.js +43 -0
  27. package/dist/fs/opfs.d.ts +23 -0
  28. package/dist/fs/opfs.js +112 -0
  29. package/dist/index.d.ts +26 -0
  30. package/dist/index.js +29 -0
  31. package/dist/llm/anthropic.d.ts +24 -0
  32. package/dist/llm/anthropic.js +280 -0
  33. package/dist/llm/index.d.ts +3 -0
  34. package/dist/llm/index.js +3 -0
  35. package/dist/llm/inlineTools.d.ts +11 -0
  36. package/dist/llm/inlineTools.js +72 -0
  37. package/dist/llm/openai.d.ts +29 -0
  38. package/dist/llm/openai.js +224 -0
  39. package/dist/llm/responses.d.ts +18 -0
  40. package/dist/llm/responses.js +256 -0
  41. package/dist/mcp/client.d.ts +20 -0
  42. package/dist/mcp/client.js +156 -0
  43. package/dist/mcp/index.d.ts +24 -0
  44. package/dist/mcp/index.js +157 -0
  45. package/dist/mcp/proxy.d.ts +3 -0
  46. package/dist/mcp/proxy.js +25 -0
  47. package/dist/mcp/sdkServer.d.ts +21 -0
  48. package/dist/mcp/sdkServer.js +28 -0
  49. package/dist/mcp/types.d.ts +92 -0
  50. package/dist/mcp/types.js +5 -0
  51. package/dist/memory/index.d.ts +4 -0
  52. package/dist/memory/index.js +5 -0
  53. package/dist/memory/render.d.ts +7 -0
  54. package/dist/memory/render.js +46 -0
  55. package/dist/memory/store.d.ts +20 -0
  56. package/dist/memory/store.js +79 -0
  57. package/dist/memory/tools.d.ts +5 -0
  58. package/dist/memory/tools.js +95 -0
  59. package/dist/memory/types.d.ts +15 -0
  60. package/dist/memory/types.js +4 -0
  61. package/dist/permissions/dangerous.d.ts +4 -0
  62. package/dist/permissions/dangerous.js +24 -0
  63. package/dist/permissions/gate.d.ts +21 -0
  64. package/dist/permissions/gate.js +66 -0
  65. package/dist/permissions/index.d.ts +5 -0
  66. package/dist/permissions/index.js +6 -0
  67. package/dist/permissions/match.d.ts +19 -0
  68. package/dist/permissions/match.js +104 -0
  69. package/dist/permissions/planMode.d.ts +3 -0
  70. package/dist/permissions/planMode.js +33 -0
  71. package/dist/permissions/types.d.ts +19 -0
  72. package/dist/permissions/types.js +2 -0
  73. package/dist/persist.d.ts +15 -0
  74. package/dist/persist.js +58 -0
  75. package/dist/prompt.d.ts +6 -0
  76. package/dist/prompt.js +34 -0
  77. package/dist/query.d.ts +105 -0
  78. package/dist/query.js +115 -0
  79. package/dist/queue.d.ts +23 -0
  80. package/dist/queue.js +43 -0
  81. package/dist/sandbox/cloudflare.d.ts +48 -0
  82. package/dist/sandbox/cloudflare.js +124 -0
  83. package/dist/sandbox/daytona.d.ts +48 -0
  84. package/dist/sandbox/daytona.js +79 -0
  85. package/dist/sandbox/e2b.d.ts +54 -0
  86. package/dist/sandbox/e2b.js +87 -0
  87. package/dist/sandbox/index.d.ts +8 -0
  88. package/dist/sandbox/index.js +19 -0
  89. package/dist/sandbox/local.d.ts +51 -0
  90. package/dist/sandbox/local.js +155 -0
  91. package/dist/sandbox/types.d.ts +18 -0
  92. package/dist/sandbox/types.js +27 -0
  93. package/dist/sandbox/util.d.ts +15 -0
  94. package/dist/sandbox/util.js +100 -0
  95. package/dist/sandbox/vercel.d.ts +48 -0
  96. package/dist/sandbox/vercel.js +130 -0
  97. package/dist/session/index.d.ts +2 -0
  98. package/dist/session/index.js +6 -0
  99. package/dist/session/store.d.ts +28 -0
  100. package/dist/session/store.js +122 -0
  101. package/dist/session/types.d.ts +22 -0
  102. package/dist/session/types.js +2 -0
  103. package/dist/settings/index.d.ts +3 -0
  104. package/dist/settings/index.js +3 -0
  105. package/dist/settings/load.d.ts +20 -0
  106. package/dist/settings/load.js +36 -0
  107. package/dist/settings/merge.d.ts +13 -0
  108. package/dist/settings/merge.js +65 -0
  109. package/dist/settings/types.d.ts +17 -0
  110. package/dist/settings/types.js +3 -0
  111. package/dist/skills/index.d.ts +4 -0
  112. package/dist/skills/index.js +5 -0
  113. package/dist/skills/load.d.ts +23 -0
  114. package/dist/skills/load.js +54 -0
  115. package/dist/skills/parse.d.ts +7 -0
  116. package/dist/skills/parse.js +40 -0
  117. package/dist/skills/tool.d.ts +2 -0
  118. package/dist/skills/tool.js +39 -0
  119. package/dist/skills/types.d.ts +10 -0
  120. package/dist/skills/types.js +4 -0
  121. package/dist/team/dispatch.d.ts +2 -0
  122. package/dist/team/dispatch.js +41 -0
  123. package/dist/team/index.d.ts +9 -0
  124. package/dist/team/index.js +11 -0
  125. package/dist/team/mailbox.d.ts +24 -0
  126. package/dist/team/mailbox.js +33 -0
  127. package/dist/team/prompt.d.ts +1 -0
  128. package/dist/team/prompt.js +12 -0
  129. package/dist/team/runner.d.ts +20 -0
  130. package/dist/team/runner.js +45 -0
  131. package/dist/team/taskBoard.d.ts +41 -0
  132. package/dist/team/taskBoard.js +73 -0
  133. package/dist/team/tools.d.ts +7 -0
  134. package/dist/team/tools.js +190 -0
  135. package/dist/tools/bash.d.ts +2 -0
  136. package/dist/tools/bash.js +45 -0
  137. package/dist/tools/config.d.ts +2 -0
  138. package/dist/tools/config.js +44 -0
  139. package/dist/tools/define.d.ts +18 -0
  140. package/dist/tools/define.js +21 -0
  141. package/dist/tools/delete_file.d.ts +2 -0
  142. package/dist/tools/delete_file.js +33 -0
  143. package/dist/tools/edit_file.d.ts +2 -0
  144. package/dist/tools/edit_file.js +93 -0
  145. package/dist/tools/fileTypes.d.ts +32 -0
  146. package/dist/tools/fileTypes.js +166 -0
  147. package/dist/tools/glob.d.ts +2 -0
  148. package/dist/tools/glob.js +53 -0
  149. package/dist/tools/grep.d.ts +2 -0
  150. package/dist/tools/grep.js +110 -0
  151. package/dist/tools/imageProcessor.d.ts +15 -0
  152. package/dist/tools/imageProcessor.js +83 -0
  153. package/dist/tools/index.d.ts +28 -0
  154. package/dist/tools/index.js +45 -0
  155. package/dist/tools/list_files.d.ts +2 -0
  156. package/dist/tools/list_files.js +42 -0
  157. package/dist/tools/multi_edit.d.ts +2 -0
  158. package/dist/tools/multi_edit.js +112 -0
  159. package/dist/tools/notebook_edit.d.ts +2 -0
  160. package/dist/tools/notebook_edit.js +118 -0
  161. package/dist/tools/plan_mode.d.ts +4 -0
  162. package/dist/tools/plan_mode.js +44 -0
  163. package/dist/tools/read_file.d.ts +2 -0
  164. package/dist/tools/read_file.js +193 -0
  165. package/dist/tools/task.d.ts +2 -0
  166. package/dist/tools/task.js +77 -0
  167. package/dist/tools/todo_write.d.ts +2 -0
  168. package/dist/tools/todo_write.js +104 -0
  169. package/dist/tools/tool_search.d.ts +2 -0
  170. package/dist/tools/tool_search.js +49 -0
  171. package/dist/tools/types.d.ts +82 -0
  172. package/dist/tools/types.js +1 -0
  173. package/dist/tools/walk.d.ts +29 -0
  174. package/dist/tools/walk.js +82 -0
  175. package/dist/tools/web_fetch.d.ts +2 -0
  176. package/dist/tools/web_fetch.js +76 -0
  177. package/dist/tools/web_search.d.ts +22 -0
  178. package/dist/tools/web_search.js +195 -0
  179. package/dist/tools/write_file.d.ts +2 -0
  180. package/dist/tools/write_file.js +39 -0
  181. package/dist/types/index.d.ts +363 -0
  182. package/dist/types/index.js +9 -0
  183. package/dist/util/ids.d.ts +3 -0
  184. package/dist/util/ids.js +22 -0
  185. package/dist/util/paths.d.ts +16 -0
  186. package/dist/util/paths.js +72 -0
  187. package/dist/util/pricing.d.ts +15 -0
  188. package/dist/util/pricing.js +81 -0
  189. package/dist/workspace/index.d.ts +2 -0
  190. package/dist/workspace/index.js +2 -0
  191. package/dist/workspace/memory.d.ts +28 -0
  192. package/dist/workspace/memory.js +97 -0
  193. package/dist/workspace/webcontainer.d.ts +65 -0
  194. package/dist/workspace/webcontainer.js +156 -0
  195. package/package.json +78 -0
package/dist/agent.js ADDED
@@ -0,0 +1,897 @@
1
+ // Agent loop engine — the multi-turn tool loop that powers query().
2
+ //
3
+ // Mirrors the Claude Code QueryEngine pattern:
4
+ // 1. Accumulate messages
5
+ // 2. Call the LLM with tools
6
+ // 3. Extract tool calls from the response
7
+ // 4. Run permission gate + PreToolUse hooks
8
+ // 5. Execute each tool against the workspace
9
+ // 6. Run PostToolUse hooks; append results to the message history
10
+ // 7. Repeat until no tool calls or max turns reached
11
+ import { ALL_CLAUDE_CODE_TOOLS, toolByName, toolDefs } from './tools/index.js';
12
+ import { task as taskTool } from './tools/task.js';
13
+ import { loadMcpServers } from './mcp/index.js';
14
+ import { runSlashCommand } from './commands/index.js';
15
+ import { BackgroundTaskManager, BACKGROUND_TOOLS } from './background/index.js';
16
+ import { Mailbox, TaskBoard, TEAM_TOOLS, TEAM_DISPATCH_TOOLS, coordinatorPrompt } from './team/index.js';
17
+ import { MEMORY_TOOLS } from './memory/index.js';
18
+ import { PLAN_MODE_TOOLS } from './tools/plan_mode.js';
19
+ import { rulesToCanUseTool, ruleSetFromStrings, applyPermissionUpdate, isReadOnlyTool, } from './permissions/index.js';
20
+ import { loadSettings, settingsToPermissionRuleSet } from './settings/index.js';
21
+ import { loadSkillsFromFs, skillsToCommands, skill as skillTool } from './skills/index.js';
22
+ import { defaultSystemPrompt, defaultSubagentPrompt } from './prompt.js';
23
+ import { DEFAULT_MAX_RESULT_CHARS, maybePersistLargeResult } from './persist.js';
24
+ import { computeCostUSD, contextWindowFor } from './util/pricing.js';
25
+ import { estimateTokens, summarizeHistory } from './compact.js';
26
+ import { uuid } from './util/ids.js';
27
+ /** Wrap a single text prompt into the async-iterable form runAgent expects. */
28
+ async function* singleUserPrompt(text) {
29
+ yield {
30
+ type: 'user',
31
+ message: { role: 'user', content: text },
32
+ parent_tool_use_id: null,
33
+ timestamp: new Date().toISOString(),
34
+ };
35
+ }
36
+ /** File-mutating tools whose success fires a FileChanged hook. */
37
+ const MUTATING_FILE_TOOLS = new Set([
38
+ 'write_file',
39
+ 'edit_file',
40
+ 'multi_edit',
41
+ 'delete_file',
42
+ 'notebook_edit',
43
+ ]);
44
+ /** A minimal pushable async queue: yields pushed items until closed. */
45
+ function createPushQueue() {
46
+ const items = [];
47
+ let resolveNext = null;
48
+ let closed = false;
49
+ return {
50
+ push(v) {
51
+ if (resolveNext) {
52
+ resolveNext({ value: v, done: false });
53
+ resolveNext = null;
54
+ }
55
+ else
56
+ items.push(v);
57
+ },
58
+ close() {
59
+ closed = true;
60
+ if (resolveNext) {
61
+ resolveNext({ value: undefined, done: true });
62
+ resolveNext = null;
63
+ }
64
+ },
65
+ [Symbol.asyncIterator]() {
66
+ return {
67
+ next: () => {
68
+ if (items.length)
69
+ return Promise.resolve({ value: items.shift(), done: false });
70
+ if (closed)
71
+ return Promise.resolve({ value: undefined, done: true });
72
+ return new Promise((res) => (resolveNext = res));
73
+ },
74
+ };
75
+ },
76
+ };
77
+ }
78
+ const emptyUsage = () => ({ input_tokens: 0, output_tokens: 0 });
79
+ function addUsageInto(target, b) {
80
+ if (!b)
81
+ return;
82
+ target.input_tokens += b.input_tokens || 0;
83
+ target.output_tokens += b.output_tokens || 0;
84
+ target.cache_read_input_tokens =
85
+ (target.cache_read_input_tokens || 0) + (b.cache_read_input_tokens || 0);
86
+ target.cache_creation_input_tokens =
87
+ (target.cache_creation_input_tokens || 0) + (b.cache_creation_input_tokens || 0);
88
+ }
89
+ function toolUseBlocks(calls) {
90
+ return calls.map((c) => ({
91
+ type: 'tool_use',
92
+ id: c.id,
93
+ name: c.function.name,
94
+ input: safeParse(c.function.arguments),
95
+ }));
96
+ }
97
+ function safeParse(json) {
98
+ if (!json || !json.trim())
99
+ return {};
100
+ try {
101
+ const v = JSON.parse(json);
102
+ return v && typeof v === 'object' ? v : { value: v };
103
+ }
104
+ catch {
105
+ return { _raw: json };
106
+ }
107
+ }
108
+ function resultToText(content) {
109
+ if (typeof content === 'string')
110
+ return content;
111
+ return content
112
+ .map((b) => {
113
+ if (b.type === 'text')
114
+ return b.text;
115
+ if (b.type === 'image')
116
+ return '[image]';
117
+ if (b.type === 'document')
118
+ return `[document${b.title ? ': ' + b.title : ''}]`;
119
+ return `[${b.type}]`;
120
+ })
121
+ .join('\n');
122
+ }
123
+ /** Keep only text/image/document blocks for a tool_result payload. */
124
+ function toToolResultContent(content) {
125
+ if (typeof content === 'string')
126
+ return content;
127
+ return content.filter((b) => b.type === 'text' || b.type === 'image' || b.type === 'document');
128
+ }
129
+ function selectTools(tools, allow, deny) {
130
+ let out = tools;
131
+ if (allow?.length)
132
+ out = out.filter((t) => allow.includes(t.def.function.name));
133
+ if (deny?.length)
134
+ out = out.filter((t) => !deny.includes(t.def.function.name));
135
+ return out;
136
+ }
137
+ /**
138
+ * The core agent loop. Yields faithful SDKMessages as the conversation
139
+ * progresses: an init system message, assistant turns, synthetic user turns
140
+ * carrying tool_results, and a final result message per user prompt.
141
+ */
142
+ export async function* runAgent(options) {
143
+ const { prompt, workspace, llm, abortController, hooks, limits, } = options;
144
+ // Prefer the workspace's own cwd (e.g. LocalSandbox/WebContainer) so the
145
+ // system prompt and tool path resolution match the real filesystem root.
146
+ const workspaceCwd = workspace.cwd;
147
+ const cwd = options.cwd ?? workspaceCwd ?? '/home/projects';
148
+ const sessionId = options.sessionId ?? uuid();
149
+ const signal = abortController?.signal;
150
+ // Load .claude/settings.json (project/local cascade) when requested; explicit
151
+ // options always win over settings.
152
+ let settings = {};
153
+ if (options.settings === true)
154
+ settings = await loadSettings(workspace, { cwd });
155
+ else if (options.settings && typeof options.settings === 'object')
156
+ settings = options.settings;
157
+ const model = options.model ?? settings.model;
158
+ const maxTurns = options.maxTurns ?? settings.maxTurns ?? 50;
159
+ const permissionMode = options.permissionMode ?? settings.permissionMode ?? 'bypassPermissions';
160
+ const persistLargeResults = options.persistLargeResults !== false;
161
+ const maxToolResultChars = options.maxToolResultChars ?? DEFAULT_MAX_RESULT_CHARS;
162
+ const emitPartial = options.includePartialMessages === true;
163
+ // `tools` replaces the builtin set; `extraTools` is ADDED to it (so custom
164
+ // tools augment the defaults). Then allow/deny filtering narrows the result.
165
+ const baseTools = selectTools([...(options.tools ?? ALL_CLAUDE_CODE_TOOLS), ...(options.extraTools ?? [])], options.allowedTools ?? settings.allowedTools, options.disallowedTools ?? settings.disallowedTools);
166
+ // Skills: load .claude/skills/*.md (or use a provided array) → slash commands + registry.
167
+ let skills = [];
168
+ if (options.skills === true)
169
+ skills = await loadSkillsFromFs(workspace);
170
+ else if (Array.isArray(options.skills))
171
+ skills = options.skills;
172
+ // Permission gate: explicit canUseTool wins; else build one from rules
173
+ // (options.permissionRules merged with settings rules).
174
+ const settingsRules = settingsToPermissionRuleSet(settings);
175
+ const ruleSet = ruleSetFromStrings({
176
+ allow: [...(options.permissionRules?.allow ?? []), ...settingsRules.allow],
177
+ deny: [...(options.permissionRules?.deny ?? []), ...settingsRules.deny],
178
+ ask: [...(options.permissionRules?.ask ?? []), ...settingsRules.ask],
179
+ });
180
+ const hasRules = ruleSet.allow.length + ruleSet.deny.length + ruleSet.ask.length > 0;
181
+ const ruleBased = !options.canUseTool && hasRules;
182
+ let activeRuleSet = ruleSet;
183
+ const buildGate = () => rulesToCanUseTool(activeRuleSet, {
184
+ mode: permissionMode,
185
+ onAsk: options.onPermissionAsk,
186
+ flagDangerous: true,
187
+ });
188
+ let canUseTool = options.canUseTool ?? (hasRules ? buildGate() : undefined);
189
+ const planMode = { active: permissionMode === 'plan' };
190
+ // Sub-agents: register the `task` tool when agents are configured and we have
191
+ // nesting budget left (prevents runaway recursion).
192
+ const agents = options.agents;
193
+ const depth = options.subagentDepth ?? 0;
194
+ const maxDepth = options.maxSubagentDepth ?? 2;
195
+ const subagentsEnabled = !!agents && depth < maxDepth;
196
+ // Background tasks: a manager + the management tools, when enabled.
197
+ // A background manager may be injected so tasks persist across turns (the TUI
198
+ // shares one for the whole session); otherwise one is created when enabled.
199
+ const backgroundEnabled = options.background === true || !!options.backgroundManager;
200
+ const background = backgroundEnabled
201
+ ? options.backgroundManager ?? new BackgroundTaskManager()
202
+ : undefined;
203
+ const messageQueue = options.messageQueue;
204
+ // Teammates: a shared Mailbox + TaskBoard (reused from the parent when this
205
+ // is a sub-agent) + team tools + coordinator prompt.
206
+ const teamEnabled = options.team === true;
207
+ const mailbox = teamEnabled ? options.mailbox ?? new Mailbox() : undefined;
208
+ const board = teamEnabled ? options.board ?? new TaskBoard() : undefined;
209
+ const agentName = options.agentName ?? 'coordinator';
210
+ let localTools = subagentsEnabled && !baseTools.some((t) => t.def.function.name === 'task')
211
+ ? [...baseTools, taskTool]
212
+ : baseTools;
213
+ if (backgroundEnabled) {
214
+ const present = new Set(localTools.map((t) => t.def.function.name));
215
+ localTools = [...localTools, ...BACKGROUND_TOOLS.filter((t) => !present.has(t.def.function.name))];
216
+ }
217
+ if (teamEnabled) {
218
+ const present = new Set(localTools.map((t) => t.def.function.name));
219
+ const teamSet = subagentsEnabled ? [...TEAM_TOOLS, ...TEAM_DISPATCH_TOOLS] : TEAM_TOOLS;
220
+ localTools = [...localTools, ...teamSet.filter((t) => !present.has(t.def.function.name))];
221
+ }
222
+ const memory = options.memory;
223
+ if (memory) {
224
+ const present = new Set(localTools.map((t) => t.def.function.name));
225
+ localTools = [...localTools, ...MEMORY_TOOLS.filter((t) => !present.has(t.def.function.name))];
226
+ }
227
+ // Skill tool (when skills are available) + plan-mode tools (always, so the
228
+ // agent can enter/exit plan mode on demand).
229
+ {
230
+ const present = new Set(localTools.map((t) => t.def.function.name));
231
+ const extra = [...PLAN_MODE_TOOLS];
232
+ if (skills.length)
233
+ extra.push(skillTool);
234
+ localTools = [...localTools, ...extra.filter((t) => !present.has(t.def.function.name))];
235
+ }
236
+ // Load MCP server tools (HTTP/SSE/in-process) and merge them in. Never throws;
237
+ // failed servers contribute no tools and surface in mcp_servers status.
238
+ let mcpStatuses = [];
239
+ let tools = localTools;
240
+ if (options.mcpServers && Object.keys(options.mcpServers).length) {
241
+ const loaded = await loadMcpServers(options.mcpServers, {
242
+ signal,
243
+ proxy: options.mcpProxy,
244
+ });
245
+ tools = [...localTools, ...loaded.tools];
246
+ mcpStatuses = loaded.statuses.map((s) => ({ name: s.name, status: s.status }));
247
+ }
248
+ const defs = toolDefs(tools);
249
+ const byName = toolByName(tools);
250
+ let system = options.systemPrompt != null ? options.systemPrompt : defaultSystemPrompt(cwd);
251
+ if (teamEnabled)
252
+ system += '\n\n' + coordinatorPrompt();
253
+ if (memory) {
254
+ const mem = await memory.render();
255
+ if (mem)
256
+ system += '\n\n' + mem;
257
+ }
258
+ if (options.appendSystemPrompt)
259
+ system += '\n\n' + options.appendSystemPrompt;
260
+ const history = [{ role: 'system', content: system }];
261
+ const store = { todos: [] };
262
+ const ctx = {
263
+ fs: workspace,
264
+ exec: workspace,
265
+ cwd,
266
+ readFiles: new Set(),
267
+ signal,
268
+ store,
269
+ limits,
270
+ background,
271
+ mailbox,
272
+ board,
273
+ agentName,
274
+ toolIndex: defs.map((d) => ({ name: d.function.name, description: d.function.description })),
275
+ memory,
276
+ skills,
277
+ planMode,
278
+ };
279
+ const skillCommands = skillsToCommands(skills);
280
+ const allCommands = [...(options.commands ?? []), ...skillCommands];
281
+ // Wire sub-agent spawning. Each call runs a fresh, isolated runAgent to
282
+ // completion and returns only its final text.
283
+ if (subagentsEnabled) {
284
+ ctx.runSubagent = async ({ prompt: subPrompt, agentType, signal: subSignal, onProgress }) => {
285
+ const def = agentType ? agents?.[agentType] : undefined;
286
+ const subSystem = def?.prompt ?? defaultSubagentPrompt(cwd);
287
+ const subTools = def?.tools
288
+ ? baseTools.filter((t) => def.tools.includes(t.def.function.name))
289
+ : baseTools;
290
+ await runHooks('SubagentStart', {
291
+ hook_event_name: 'SubagentStart',
292
+ agent_type: agentType || 'general-purpose',
293
+ });
294
+ let finalText = '';
295
+ let isError = false;
296
+ // Own controller so the caller's signal (e.g. a background task's stop)
297
+ // AND the parent's abort both cancel this sub-agent.
298
+ const childController = new AbortController();
299
+ const onAbort = () => childController.abort();
300
+ abortController?.signal.addEventListener('abort', onAbort);
301
+ subSignal?.addEventListener('abort', onAbort);
302
+ if (abortController?.signal.aborted || subSignal?.aborted)
303
+ childController.abort();
304
+ const child = runAgent({
305
+ prompt: singleUserPrompt(subPrompt),
306
+ workspace,
307
+ llm,
308
+ tools: subTools,
309
+ model: def?.model ?? model,
310
+ systemPrompt: subSystem,
311
+ maxTurns,
312
+ cwd,
313
+ abortController: childController,
314
+ canUseTool,
315
+ permissionMode,
316
+ hooks,
317
+ limits,
318
+ persistLargeResults,
319
+ maxToolResultChars,
320
+ agents,
321
+ subagentDepth: depth + 1,
322
+ maxSubagentDepth: maxDepth,
323
+ // Share the same mailbox + board so workers and the coordinator
324
+ // collaborate on one set of tasks/messages.
325
+ team: teamEnabled,
326
+ mailbox,
327
+ board,
328
+ agentName: agentType || 'worker',
329
+ memory,
330
+ skills,
331
+ });
332
+ for await (const m of child) {
333
+ if (m.type === 'assistant') {
334
+ const t = m.message.content
335
+ .filter((b) => b.type === 'text')
336
+ .map((b) => b.text)
337
+ .join('\n');
338
+ if (t) {
339
+ finalText = t;
340
+ onProgress?.(t);
341
+ }
342
+ for (const b of m.message.content) {
343
+ if (b.type === 'tool_use')
344
+ onProgress?.(`[${b.name}]`);
345
+ }
346
+ }
347
+ else if (m.type === 'result') {
348
+ if (m.subtype !== 'success')
349
+ isError = true;
350
+ if ('result' in m && m.result)
351
+ finalText = m.result;
352
+ }
353
+ }
354
+ abortController?.signal.removeEventListener('abort', onAbort);
355
+ subSignal?.removeEventListener('abort', onAbort);
356
+ await runHooks('SubagentStop', {
357
+ hook_event_name: 'SubagentStop',
358
+ agent_type: agentType || 'general-purpose',
359
+ last_assistant_message: finalText,
360
+ });
361
+ return { text: finalText, isError };
362
+ };
363
+ }
364
+ async function runHooks(event, input) {
365
+ const cbs = hooks?.[event];
366
+ if (!cbs?.length)
367
+ return [];
368
+ const out = [];
369
+ for (const cb of cbs) {
370
+ try {
371
+ out.push(await cb(input, { signal }));
372
+ }
373
+ catch (err) {
374
+ out.push({
375
+ systemMessage: `Hook ${event} error: ${err instanceof Error ? err.message : String(err)}`,
376
+ });
377
+ }
378
+ }
379
+ return out;
380
+ }
381
+ // Init system message.
382
+ yield {
383
+ type: 'system',
384
+ subtype: 'init',
385
+ apiKeySource: 'none',
386
+ cwd,
387
+ tools: defs.map((d) => d.function.name),
388
+ mcp_servers: mcpStatuses,
389
+ model: model ?? 'unknown',
390
+ permissionMode,
391
+ slash_commands: [],
392
+ output_style: 'default',
393
+ skills: [],
394
+ agents: agents ? Object.keys(agents) : undefined,
395
+ uuid: uuid(),
396
+ session_id: sessionId,
397
+ };
398
+ await runHooks('SessionStart', {
399
+ hook_event_name: 'SessionStart',
400
+ source: agentName === 'coordinator' ? 'startup' : 'subagent',
401
+ cwd,
402
+ model: model ?? 'unknown',
403
+ });
404
+ const startedAt = Date.now();
405
+ const sessionUsage = emptyUsage();
406
+ // Resume: seed the transcript from a prior session before the first turn.
407
+ if (options.resume && options.sessionStore) {
408
+ const prior = await options.sessionStore.load(sessionId);
409
+ if (prior && prior.length) {
410
+ // Replace everything after our system message with the stored transcript
411
+ // (which already includes its own system message at index 0).
412
+ history.splice(0, history.length, ...prior);
413
+ }
414
+ }
415
+ for await (const userMsg of prompt) {
416
+ if (signal?.aborted)
417
+ break;
418
+ const content = userMsg.message.content;
419
+ // Slash-command interception: a string user turn beginning with '/'.
420
+ if (typeof content === 'string' && content.trim().startsWith('/')) {
421
+ const outcome = await runSlashCommand(content, {
422
+ history,
423
+ tools: defs.map((d) => ({
424
+ name: d.function.name,
425
+ description: d.function.description,
426
+ })),
427
+ model,
428
+ cwd,
429
+ usage: sessionUsage,
430
+ store,
431
+ signal,
432
+ llm,
433
+ commands: allCommands,
434
+ sessionId,
435
+ sessionStore: options.sessionStore,
436
+ readFiles: ctx.readFiles,
437
+ agents,
438
+ mcpServers: mcpStatuses,
439
+ permissionMode,
440
+ background,
441
+ board,
442
+ exec: (command) => workspace.exec(command),
443
+ fs: { readFile: (p) => workspace.readFile(p) },
444
+ memory,
445
+ });
446
+ if (outcome) {
447
+ if (outcome.compacted) {
448
+ await runHooks('PreCompact', { hook_event_name: 'PreCompact', trigger: 'manual' });
449
+ }
450
+ if (outcome.newHistory)
451
+ history.splice(0, history.length, ...outcome.newHistory);
452
+ if (outcome.compacted) {
453
+ yield {
454
+ type: 'system',
455
+ subtype: 'compact_boundary',
456
+ compact_metadata: { trigger: 'manual', pre_tokens: 0 },
457
+ uuid: uuid(),
458
+ session_id: sessionId,
459
+ };
460
+ await runHooks('PostCompact', { hook_event_name: 'PostCompact', trigger: 'manual' });
461
+ }
462
+ if (outcome.systemText) {
463
+ yield {
464
+ type: 'system',
465
+ subtype: 'local_command_output',
466
+ content: outcome.systemText,
467
+ uuid: uuid(),
468
+ session_id: sessionId,
469
+ };
470
+ }
471
+ if (outcome.expandedPrompt != null) {
472
+ history.push({ role: 'user', content: outcome.expandedPrompt });
473
+ }
474
+ else {
475
+ continue; // command handled; no LLM turn
476
+ }
477
+ }
478
+ else {
479
+ history.push({ role: 'user', content }); // unknown command → normal prompt
480
+ }
481
+ }
482
+ else {
483
+ history.push({ role: 'user', content });
484
+ const pre = await runHooks('UserPromptSubmit', {
485
+ hook_event_name: 'UserPromptSubmit',
486
+ prompt: typeof content === 'string' ? content : '',
487
+ });
488
+ const extra = pre.map((o) => (o && o.additionalContext) || '').filter(Boolean).join('\n');
489
+ if (extra)
490
+ history.push({ role: 'user', content: extra });
491
+ }
492
+ let turns = 0;
493
+ let lastText = '';
494
+ let resultModel = model ?? 'unknown';
495
+ const usageTotal = emptyUsage();
496
+ let apiMs = 0;
497
+ let hitMaxTurns = false;
498
+ let errored = null;
499
+ const denials = [];
500
+ let autoCompactCount = 0;
501
+ while (true) {
502
+ if (signal?.aborted)
503
+ break;
504
+ if (turns >= maxTurns) {
505
+ hitMaxTurns = true;
506
+ break;
507
+ }
508
+ turns++;
509
+ // Message queue: deliver one interjected user message per turn boundary.
510
+ // (Messages enqueued via options.messageQueue while this loop runs.)
511
+ if (messageQueue && messageQueue.size > 0) {
512
+ const queued = messageQueue.shift();
513
+ if (queued) {
514
+ history.push({ role: 'user', content: queued.content });
515
+ yield {
516
+ type: 'user',
517
+ message: { role: 'user', content: queued.content },
518
+ parent_tool_use_id: null,
519
+ timestamp: new Date().toISOString(),
520
+ uuid: uuid(),
521
+ session_id: sessionId,
522
+ };
523
+ }
524
+ }
525
+ // Auto-compaction: summarize when the transcript nears the context limit.
526
+ // Circuit-breaker: stop after 3 compactions (avoids a summarize loop).
527
+ if (options.autoCompact && autoCompactCount < 3 && history.length > 3) {
528
+ const limit = options.contextLimit ?? (contextWindowFor(resultModel) || 200_000);
529
+ const threshold = (options.compactThreshold ?? 0.8) * limit;
530
+ if (estimateTokens(history) > threshold) {
531
+ await runHooks('PreCompact', { hook_event_name: 'PreCompact', trigger: 'auto' });
532
+ const compacted = await summarizeHistory(history, llm, { model, signal });
533
+ if (compacted) {
534
+ history.splice(0, history.length, ...compacted);
535
+ autoCompactCount++;
536
+ yield {
537
+ type: 'system',
538
+ subtype: 'compact_boundary',
539
+ compact_metadata: { trigger: 'auto', pre_tokens: Math.round(threshold) },
540
+ uuid: uuid(),
541
+ session_id: sessionId,
542
+ };
543
+ await runHooks('PostCompact', { hook_event_name: 'PostCompact', trigger: 'auto' });
544
+ }
545
+ }
546
+ }
547
+ let streamedText = '';
548
+ let captured = [];
549
+ const apiStart = Date.now();
550
+ let result;
551
+ try {
552
+ if (emitPartial) {
553
+ // Stream token deltas to the consumer as stream_event messages while
554
+ // the request is in flight, then await the final result.
555
+ const q = createPushQueue();
556
+ let inToolMarkup = false;
557
+ const sp = llm.streamChat(history, {
558
+ model,
559
+ tools: defs,
560
+ signal,
561
+ onToken: (delta) => {
562
+ streamedText += delta;
563
+ // Stop streaming once inline tool-call markup begins; it would
564
+ // otherwise flood the UI with raw XML / file contents. The cleaned
565
+ // text arrives with the final assistant message.
566
+ if (!inToolMarkup && /<tool_call|<function\s*=/.test(streamedText)) {
567
+ inToolMarkup = true;
568
+ }
569
+ if (inToolMarkup)
570
+ return;
571
+ q.push({
572
+ type: 'stream_event',
573
+ event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: delta } },
574
+ parent_tool_use_id: null,
575
+ uuid: uuid(),
576
+ session_id: sessionId,
577
+ });
578
+ },
579
+ onTool: (calls) => {
580
+ captured = calls;
581
+ },
582
+ });
583
+ sp.then(() => { }, () => { }).finally(() => q.close());
584
+ for await (const ev of q)
585
+ yield ev;
586
+ result = await sp;
587
+ }
588
+ else {
589
+ result = await llm.streamChat(history, {
590
+ model,
591
+ tools: defs,
592
+ signal,
593
+ onToken: (delta) => {
594
+ streamedText += delta;
595
+ },
596
+ onTool: (calls) => {
597
+ captured = calls;
598
+ },
599
+ });
600
+ }
601
+ }
602
+ catch (err) {
603
+ errored = err instanceof Error ? err.message : String(err);
604
+ break;
605
+ }
606
+ apiMs += Date.now() - apiStart;
607
+ const text = result.text || streamedText;
608
+ const calls = result.toolCalls.length ? result.toolCalls : captured;
609
+ lastText = text || lastText;
610
+ resultModel = result.model || resultModel;
611
+ addUsageInto(usageTotal, result.usage);
612
+ addUsageInto(sessionUsage, result.usage);
613
+ const stopReason = calls.length
614
+ ? 'tool_use'
615
+ : result.stopReason ?? 'end_turn';
616
+ const assistantContent = [];
617
+ if (text)
618
+ assistantContent.push({ type: 'text', text });
619
+ assistantContent.push(...toolUseBlocks(calls));
620
+ const apiAssistant = {
621
+ id: 'msg_' + uuid().replace(/-/g, '').slice(0, 24),
622
+ type: 'message',
623
+ role: 'assistant',
624
+ model: resultModel,
625
+ content: assistantContent,
626
+ stop_reason: stopReason,
627
+ stop_sequence: null,
628
+ usage: result.usage ?? emptyUsage(),
629
+ };
630
+ yield {
631
+ type: 'assistant',
632
+ message: apiAssistant,
633
+ parent_tool_use_id: null,
634
+ uuid: uuid(),
635
+ session_id: sessionId,
636
+ };
637
+ history.push({
638
+ role: 'assistant',
639
+ content: text,
640
+ tool_calls: calls.length ? calls : undefined,
641
+ });
642
+ // end_turn — unless the user queued more messages, in which case keep
643
+ // going (the next iteration's boundary injects the next queued message).
644
+ if (!calls.length) {
645
+ if (messageQueue && messageQueue.size > 0)
646
+ continue;
647
+ break;
648
+ }
649
+ // Execute tool calls (permission gate + hooks around each).
650
+ const toolResultBlocks = [];
651
+ const turnMedia = [];
652
+ for (const call of calls) {
653
+ if (signal?.aborted)
654
+ break;
655
+ const name = call.function.name;
656
+ let input = safeParse(call.function.arguments);
657
+ const tool = byName.get(name);
658
+ let content = '';
659
+ let isError = false;
660
+ let extraContext = '';
661
+ if (!tool) {
662
+ content = `Error: unknown tool "${name}"`;
663
+ isError = true;
664
+ }
665
+ else {
666
+ // PreToolUse hooks (may block or inject context).
667
+ const pre = await runHooks('PreToolUse', {
668
+ hook_event_name: 'PreToolUse',
669
+ tool_name: name,
670
+ tool_input: input,
671
+ tool_use_id: call.id,
672
+ });
673
+ const blocked = pre.find((o) => o &&
674
+ (o.decision === 'block' ||
675
+ o.permissionDecision === 'deny' ||
676
+ o.permissionDecision === 'ask'));
677
+ extraContext += pre
678
+ .map((o) => (o && o.additionalContext) || '')
679
+ .filter(Boolean)
680
+ .join('\n');
681
+ // Plan mode: block mutating tools until the agent exits plan mode.
682
+ const planBlocked = planMode.active &&
683
+ name !== 'enter_plan_mode' &&
684
+ name !== 'exit_plan_mode' &&
685
+ !isReadOnlyTool(name, input);
686
+ const denyTool = async (reason) => {
687
+ denials.push({ tool_name: name, tool_use_id: call.id, tool_input: input });
688
+ content = `Permission denied: ${reason}`;
689
+ isError = true;
690
+ await runHooks('PermissionDenied', {
691
+ hook_event_name: 'PermissionDenied',
692
+ tool_name: name,
693
+ tool_input: input,
694
+ tool_use_id: call.id,
695
+ reason,
696
+ });
697
+ };
698
+ if (blocked) {
699
+ await denyTool(blocked.permissionDecisionReason || 'Blocked by PreToolUse hook');
700
+ }
701
+ else if (planBlocked) {
702
+ await denyTool(`Plan mode is active — "${name}" is a mutating tool and is blocked. Investigate with read-only tools, then call exit_plan_mode before making changes.`);
703
+ }
704
+ else {
705
+ // PermissionRequest hooks fire before the gate; they can decide the
706
+ // call outright and/or suggest permission rule updates.
707
+ const preq = await runHooks('PermissionRequest', {
708
+ hook_event_name: 'PermissionRequest',
709
+ tool_name: name,
710
+ tool_input: input,
711
+ tool_use_id: call.id,
712
+ });
713
+ let preApproved = false;
714
+ let preDenied;
715
+ for (const o of preq) {
716
+ if (!o)
717
+ continue;
718
+ if (o.permissionUpdates?.length && ruleBased) {
719
+ for (const u of o.permissionUpdates)
720
+ activeRuleSet = applyPermissionUpdate(activeRuleSet, u);
721
+ canUseTool = buildGate();
722
+ }
723
+ if (o.permissionDecision === 'allow' || o.decision === 'approve')
724
+ preApproved = true;
725
+ if (o.permissionDecision === 'deny' || o.decision === 'block')
726
+ preDenied = o.permissionDecisionReason || 'Denied by PermissionRequest hook';
727
+ if (o.additionalContext)
728
+ extraContext += o.additionalContext + '\n';
729
+ }
730
+ const decision = preDenied
731
+ ? { behavior: 'deny', message: preDenied }
732
+ : preApproved
733
+ ? { behavior: 'allow' }
734
+ : canUseTool
735
+ ? await canUseTool(name, input, { signal, toolUseId: call.id })
736
+ : { behavior: 'allow' };
737
+ if (decision.behavior === 'deny') {
738
+ await denyTool(decision.message);
739
+ if (decision.interrupt)
740
+ abortController?.abort();
741
+ }
742
+ else {
743
+ if ('updatedInput' in decision && decision.updatedInput)
744
+ input = decision.updatedInput;
745
+ try {
746
+ const r = await tool.run(input, ctx);
747
+ content = r.content;
748
+ isError = !!r.isError;
749
+ }
750
+ catch (err) {
751
+ content = `Error executing ${name}: ${err instanceof Error ? err.message : String(err)}`;
752
+ isError = true;
753
+ }
754
+ if (isError) {
755
+ await runHooks('PostToolUseFailure', {
756
+ hook_event_name: 'PostToolUseFailure',
757
+ tool_name: name,
758
+ tool_input: input,
759
+ tool_use_id: call.id,
760
+ error: resultToText(content),
761
+ });
762
+ }
763
+ else if (MUTATING_FILE_TOOLS.has(name) && typeof input.path === 'string') {
764
+ await runHooks('FileChanged', {
765
+ hook_event_name: 'FileChanged',
766
+ file_path: input.path,
767
+ event: name === 'delete_file' ? 'unlink' : 'change',
768
+ });
769
+ }
770
+ // PostToolUse hooks.
771
+ const post = await runHooks('PostToolUse', {
772
+ hook_event_name: 'PostToolUse',
773
+ tool_name: name,
774
+ tool_input: input,
775
+ tool_response: content,
776
+ tool_use_id: call.id,
777
+ });
778
+ extraContext += post
779
+ .map((o) => (o && o.additionalContext) || '')
780
+ .filter(Boolean)
781
+ .join('\n');
782
+ }
783
+ }
784
+ }
785
+ let textOut = resultToText(content) + (extraContext ? '\n' + extraContext : '');
786
+ // Large-output handling: spill oversized text results to a file and
787
+ // replace them with a preview + path the model reads via read_file.
788
+ // Skipped for media results and for tools that opt out (maxResultChars).
789
+ if (persistLargeResults && typeof content === 'string') {
790
+ const threshold = tool?.maxResultChars ?? maxToolResultChars;
791
+ textOut = await maybePersistLargeResult(textOut, call.id, workspace, cwd, threshold);
792
+ }
793
+ // The tool message itself carries text only (OpenAI tool messages can't
794
+ // hold image parts); media is forwarded as a user turn below so it
795
+ // reaches every provider.
796
+ history.push({ role: 'tool', tool_call_id: call.id, content: textOut });
797
+ if (Array.isArray(content)) {
798
+ for (const b of content) {
799
+ if (b.type === 'image' || b.type === 'document')
800
+ turnMedia.push(b);
801
+ }
802
+ }
803
+ toolResultBlocks.push({
804
+ type: 'tool_result',
805
+ tool_use_id: call.id,
806
+ content: typeof content === 'string' ? textOut : toToolResultContent(content),
807
+ is_error: isError || undefined,
808
+ });
809
+ }
810
+ // Forward any image/PDF bytes from this turn's tools to the model as a
811
+ // user turn (provider-agnostic multimodal delivery).
812
+ if (turnMedia.length) {
813
+ history.push({
814
+ role: 'user',
815
+ content: [
816
+ { type: 'text', text: 'Attached file content from the tools above:' },
817
+ ...turnMedia,
818
+ ],
819
+ });
820
+ }
821
+ yield {
822
+ type: 'user',
823
+ message: { role: 'user', content: toolResultBlocks },
824
+ parent_tool_use_id: null,
825
+ isSynthetic: true,
826
+ timestamp: new Date().toISOString(),
827
+ uuid: uuid(),
828
+ session_id: sessionId,
829
+ };
830
+ }
831
+ await runHooks('Stop', {
832
+ hook_event_name: 'Stop',
833
+ last_assistant_message: lastText,
834
+ });
835
+ const durationMs = Date.now() - startedAt;
836
+ const costUSD = computeCostUSD(resultModel, usageTotal);
837
+ const modelUsage = {
838
+ [resultModel]: {
839
+ inputTokens: usageTotal.input_tokens,
840
+ outputTokens: usageTotal.output_tokens,
841
+ cacheReadInputTokens: usageTotal.cache_read_input_tokens ?? 0,
842
+ cacheCreationInputTokens: usageTotal.cache_creation_input_tokens ?? 0,
843
+ webSearchRequests: 0,
844
+ costUSD,
845
+ contextWindow: contextWindowFor(resultModel),
846
+ maxOutputTokens: 0,
847
+ },
848
+ };
849
+ if (errored || hitMaxTurns) {
850
+ yield {
851
+ type: 'result',
852
+ subtype: hitMaxTurns ? 'error_max_turns' : 'error_during_execution',
853
+ duration_ms: durationMs,
854
+ duration_api_ms: apiMs,
855
+ is_error: true,
856
+ num_turns: turns,
857
+ stop_reason: hitMaxTurns ? 'max_turns' : 'error',
858
+ total_cost_usd: costUSD,
859
+ usage: usageTotal,
860
+ modelUsage,
861
+ permission_denials: denials,
862
+ errors: errored ? [errored] : [`Reached max turns (${maxTurns})`],
863
+ uuid: uuid(),
864
+ session_id: sessionId,
865
+ };
866
+ }
867
+ else {
868
+ yield {
869
+ type: 'result',
870
+ subtype: 'success',
871
+ duration_ms: durationMs,
872
+ duration_api_ms: apiMs,
873
+ is_error: false,
874
+ num_turns: turns,
875
+ result: lastText,
876
+ stop_reason: 'end_turn',
877
+ total_cost_usd: costUSD,
878
+ usage: usageTotal,
879
+ modelUsage,
880
+ permission_denials: denials,
881
+ uuid: uuid(),
882
+ session_id: sessionId,
883
+ };
884
+ }
885
+ // Persist the transcript for resume after each completed prompt.
886
+ if (options.sessionStore) {
887
+ try {
888
+ await options.sessionStore.save(sessionId, history, { model });
889
+ }
890
+ catch {
891
+ /* persistence is best-effort */
892
+ }
893
+ }
894
+ }
895
+ // The prompt stream is exhausted — the session is ending.
896
+ await runHooks('SessionEnd', { hook_event_name: 'SessionEnd', reason: 'prompt_input_exit' });
897
+ }