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.
- package/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/agent.d.ts +110 -0
- package/dist/agent.js +897 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.js +9 -0
- package/dist/background/manager.d.ts +32 -0
- package/dist/background/manager.js +108 -0
- package/dist/background/tools.d.ts +5 -0
- package/dist/background/tools.js +98 -0
- package/dist/background/worker.d.ts +19 -0
- package/dist/background/worker.js +30 -0
- package/dist/commands/builtins.d.ts +2 -0
- package/dist/commands/builtins.js +306 -0
- package/dist/commands/index.d.ts +21 -0
- package/dist/commands/index.js +56 -0
- package/dist/commands/types.d.ts +110 -0
- package/dist/commands/types.js +5 -0
- package/dist/compact.d.ts +22 -0
- package/dist/compact.js +67 -0
- package/dist/fs/dexie.d.ts +57 -0
- package/dist/fs/dexie.js +243 -0
- package/dist/fs/index.d.ts +4 -0
- package/dist/fs/index.js +13 -0
- package/dist/fs/linuxTree.d.ts +11 -0
- package/dist/fs/linuxTree.js +43 -0
- package/dist/fs/opfs.d.ts +23 -0
- package/dist/fs/opfs.js +112 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/llm/anthropic.d.ts +24 -0
- package/dist/llm/anthropic.js +280 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/inlineTools.d.ts +11 -0
- package/dist/llm/inlineTools.js +72 -0
- package/dist/llm/openai.d.ts +29 -0
- package/dist/llm/openai.js +224 -0
- package/dist/llm/responses.d.ts +18 -0
- package/dist/llm/responses.js +256 -0
- package/dist/mcp/client.d.ts +20 -0
- package/dist/mcp/client.js +156 -0
- package/dist/mcp/index.d.ts +24 -0
- package/dist/mcp/index.js +157 -0
- package/dist/mcp/proxy.d.ts +3 -0
- package/dist/mcp/proxy.js +25 -0
- package/dist/mcp/sdkServer.d.ts +21 -0
- package/dist/mcp/sdkServer.js +28 -0
- package/dist/mcp/types.d.ts +92 -0
- package/dist/mcp/types.js +5 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -0
- package/dist/memory/render.d.ts +7 -0
- package/dist/memory/render.js +46 -0
- package/dist/memory/store.d.ts +20 -0
- package/dist/memory/store.js +79 -0
- package/dist/memory/tools.d.ts +5 -0
- package/dist/memory/tools.js +95 -0
- package/dist/memory/types.d.ts +15 -0
- package/dist/memory/types.js +4 -0
- package/dist/permissions/dangerous.d.ts +4 -0
- package/dist/permissions/dangerous.js +24 -0
- package/dist/permissions/gate.d.ts +21 -0
- package/dist/permissions/gate.js +66 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +6 -0
- package/dist/permissions/match.d.ts +19 -0
- package/dist/permissions/match.js +104 -0
- package/dist/permissions/planMode.d.ts +3 -0
- package/dist/permissions/planMode.js +33 -0
- package/dist/permissions/types.d.ts +19 -0
- package/dist/permissions/types.js +2 -0
- package/dist/persist.d.ts +15 -0
- package/dist/persist.js +58 -0
- package/dist/prompt.d.ts +6 -0
- package/dist/prompt.js +34 -0
- package/dist/query.d.ts +105 -0
- package/dist/query.js +115 -0
- package/dist/queue.d.ts +23 -0
- package/dist/queue.js +43 -0
- package/dist/sandbox/cloudflare.d.ts +48 -0
- package/dist/sandbox/cloudflare.js +124 -0
- package/dist/sandbox/daytona.d.ts +48 -0
- package/dist/sandbox/daytona.js +79 -0
- package/dist/sandbox/e2b.d.ts +54 -0
- package/dist/sandbox/e2b.js +87 -0
- package/dist/sandbox/index.d.ts +8 -0
- package/dist/sandbox/index.js +19 -0
- package/dist/sandbox/local.d.ts +51 -0
- package/dist/sandbox/local.js +155 -0
- package/dist/sandbox/types.d.ts +18 -0
- package/dist/sandbox/types.js +27 -0
- package/dist/sandbox/util.d.ts +15 -0
- package/dist/sandbox/util.js +100 -0
- package/dist/sandbox/vercel.d.ts +48 -0
- package/dist/sandbox/vercel.js +130 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +6 -0
- package/dist/session/store.d.ts +28 -0
- package/dist/session/store.js +122 -0
- package/dist/session/types.d.ts +22 -0
- package/dist/session/types.js +2 -0
- package/dist/settings/index.d.ts +3 -0
- package/dist/settings/index.js +3 -0
- package/dist/settings/load.d.ts +20 -0
- package/dist/settings/load.js +36 -0
- package/dist/settings/merge.d.ts +13 -0
- package/dist/settings/merge.js +65 -0
- package/dist/settings/types.d.ts +17 -0
- package/dist/settings/types.js +3 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +5 -0
- package/dist/skills/load.d.ts +23 -0
- package/dist/skills/load.js +54 -0
- package/dist/skills/parse.d.ts +7 -0
- package/dist/skills/parse.js +40 -0
- package/dist/skills/tool.d.ts +2 -0
- package/dist/skills/tool.js +39 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/skills/types.js +4 -0
- package/dist/team/dispatch.d.ts +2 -0
- package/dist/team/dispatch.js +41 -0
- package/dist/team/index.d.ts +9 -0
- package/dist/team/index.js +11 -0
- package/dist/team/mailbox.d.ts +24 -0
- package/dist/team/mailbox.js +33 -0
- package/dist/team/prompt.d.ts +1 -0
- package/dist/team/prompt.js +12 -0
- package/dist/team/runner.d.ts +20 -0
- package/dist/team/runner.js +45 -0
- package/dist/team/taskBoard.d.ts +41 -0
- package/dist/team/taskBoard.js +73 -0
- package/dist/team/tools.d.ts +7 -0
- package/dist/team/tools.js +190 -0
- package/dist/tools/bash.d.ts +2 -0
- package/dist/tools/bash.js +45 -0
- package/dist/tools/config.d.ts +2 -0
- package/dist/tools/config.js +44 -0
- package/dist/tools/define.d.ts +18 -0
- package/dist/tools/define.js +21 -0
- package/dist/tools/delete_file.d.ts +2 -0
- package/dist/tools/delete_file.js +33 -0
- package/dist/tools/edit_file.d.ts +2 -0
- package/dist/tools/edit_file.js +93 -0
- package/dist/tools/fileTypes.d.ts +32 -0
- package/dist/tools/fileTypes.js +166 -0
- package/dist/tools/glob.d.ts +2 -0
- package/dist/tools/glob.js +53 -0
- package/dist/tools/grep.d.ts +2 -0
- package/dist/tools/grep.js +110 -0
- package/dist/tools/imageProcessor.d.ts +15 -0
- package/dist/tools/imageProcessor.js +83 -0
- package/dist/tools/index.d.ts +28 -0
- package/dist/tools/index.js +45 -0
- package/dist/tools/list_files.d.ts +2 -0
- package/dist/tools/list_files.js +42 -0
- package/dist/tools/multi_edit.d.ts +2 -0
- package/dist/tools/multi_edit.js +112 -0
- package/dist/tools/notebook_edit.d.ts +2 -0
- package/dist/tools/notebook_edit.js +118 -0
- package/dist/tools/plan_mode.d.ts +4 -0
- package/dist/tools/plan_mode.js +44 -0
- package/dist/tools/read_file.d.ts +2 -0
- package/dist/tools/read_file.js +193 -0
- package/dist/tools/task.d.ts +2 -0
- package/dist/tools/task.js +77 -0
- package/dist/tools/todo_write.d.ts +2 -0
- package/dist/tools/todo_write.js +104 -0
- package/dist/tools/tool_search.d.ts +2 -0
- package/dist/tools/tool_search.js +49 -0
- package/dist/tools/types.d.ts +82 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/walk.d.ts +29 -0
- package/dist/tools/walk.js +82 -0
- package/dist/tools/web_fetch.d.ts +2 -0
- package/dist/tools/web_fetch.js +76 -0
- package/dist/tools/web_search.d.ts +22 -0
- package/dist/tools/web_search.js +195 -0
- package/dist/tools/write_file.d.ts +2 -0
- package/dist/tools/write_file.js +39 -0
- package/dist/types/index.d.ts +363 -0
- package/dist/types/index.js +9 -0
- package/dist/util/ids.d.ts +3 -0
- package/dist/util/ids.js +22 -0
- package/dist/util/paths.d.ts +16 -0
- package/dist/util/paths.js +72 -0
- package/dist/util/pricing.d.ts +15 -0
- package/dist/util/pricing.js +81 -0
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.js +2 -0
- package/dist/workspace/memory.d.ts +28 -0
- package/dist/workspace/memory.js +97 -0
- package/dist/workspace/webcontainer.d.ts +65 -0
- package/dist/workspace/webcontainer.js +156 -0
- 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
|
+
}
|