kode-sdk 2.7.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 (169) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +74 -0
  3. package/dist/core/agent/breakpoint-manager.d.ts +16 -0
  4. package/dist/core/agent/breakpoint-manager.js +36 -0
  5. package/dist/core/agent/message-queue.d.ts +26 -0
  6. package/dist/core/agent/message-queue.js +47 -0
  7. package/dist/core/agent/permission-manager.d.ts +9 -0
  8. package/dist/core/agent/permission-manager.js +32 -0
  9. package/dist/core/agent/todo-manager.d.ts +26 -0
  10. package/dist/core/agent/todo-manager.js +91 -0
  11. package/dist/core/agent/tool-runner.d.ts +9 -0
  12. package/dist/core/agent/tool-runner.js +45 -0
  13. package/dist/core/agent.d.ts +271 -0
  14. package/dist/core/agent.js +2334 -0
  15. package/dist/core/checkpointer.d.ts +96 -0
  16. package/dist/core/checkpointer.js +57 -0
  17. package/dist/core/checkpointers/file.d.ts +20 -0
  18. package/dist/core/checkpointers/file.js +153 -0
  19. package/dist/core/checkpointers/index.d.ts +3 -0
  20. package/dist/core/checkpointers/index.js +9 -0
  21. package/dist/core/checkpointers/redis.d.ts +35 -0
  22. package/dist/core/checkpointers/redis.js +113 -0
  23. package/dist/core/compression/ai-strategy.d.ts +53 -0
  24. package/dist/core/compression/ai-strategy.js +298 -0
  25. package/dist/core/compression/index.d.ts +12 -0
  26. package/dist/core/compression/index.js +27 -0
  27. package/dist/core/compression/prompts.d.ts +35 -0
  28. package/dist/core/compression/prompts.js +114 -0
  29. package/dist/core/compression/simple-strategy.d.ts +44 -0
  30. package/dist/core/compression/simple-strategy.js +240 -0
  31. package/dist/core/compression/token-estimator.d.ts +42 -0
  32. package/dist/core/compression/token-estimator.js +121 -0
  33. package/dist/core/compression/types.d.ts +140 -0
  34. package/dist/core/compression/types.js +9 -0
  35. package/dist/core/config.d.ts +10 -0
  36. package/dist/core/config.js +2 -0
  37. package/dist/core/context-manager.d.ts +115 -0
  38. package/dist/core/context-manager.js +107 -0
  39. package/dist/core/errors.d.ts +6 -0
  40. package/dist/core/errors.js +17 -0
  41. package/dist/core/events.d.ts +49 -0
  42. package/dist/core/events.js +312 -0
  43. package/dist/core/file-pool.d.ts +43 -0
  44. package/dist/core/file-pool.js +120 -0
  45. package/dist/core/hooks.d.ts +23 -0
  46. package/dist/core/hooks.js +71 -0
  47. package/dist/core/permission-modes.d.ts +31 -0
  48. package/dist/core/permission-modes.js +61 -0
  49. package/dist/core/pool.d.ts +31 -0
  50. package/dist/core/pool.js +87 -0
  51. package/dist/core/room.d.ts +15 -0
  52. package/dist/core/room.js +57 -0
  53. package/dist/core/scheduler.d.ts +33 -0
  54. package/dist/core/scheduler.js +58 -0
  55. package/dist/core/template.d.ts +69 -0
  56. package/dist/core/template.js +35 -0
  57. package/dist/core/time-bridge.d.ts +18 -0
  58. package/dist/core/time-bridge.js +100 -0
  59. package/dist/core/todo.d.ts +34 -0
  60. package/dist/core/todo.js +89 -0
  61. package/dist/core/types.d.ts +380 -0
  62. package/dist/core/types.js +3 -0
  63. package/dist/index.d.ts +51 -0
  64. package/dist/index.js +147 -0
  65. package/dist/infra/provider.d.ts +144 -0
  66. package/dist/infra/provider.js +294 -0
  67. package/dist/infra/sandbox-factory.d.ts +10 -0
  68. package/dist/infra/sandbox-factory.js +21 -0
  69. package/dist/infra/sandbox.d.ts +87 -0
  70. package/dist/infra/sandbox.js +255 -0
  71. package/dist/infra/store.d.ts +154 -0
  72. package/dist/infra/store.js +584 -0
  73. package/dist/skills/index.d.ts +12 -0
  74. package/dist/skills/index.js +36 -0
  75. package/dist/skills/injector.d.ts +29 -0
  76. package/dist/skills/injector.js +96 -0
  77. package/dist/skills/loader.d.ts +59 -0
  78. package/dist/skills/loader.js +215 -0
  79. package/dist/skills/manager.d.ts +85 -0
  80. package/dist/skills/manager.js +221 -0
  81. package/dist/skills/parser.d.ts +40 -0
  82. package/dist/skills/parser.js +107 -0
  83. package/dist/skills/types.d.ts +107 -0
  84. package/dist/skills/types.js +7 -0
  85. package/dist/skills/validator.d.ts +30 -0
  86. package/dist/skills/validator.js +121 -0
  87. package/dist/store.d.ts +1 -0
  88. package/dist/store.js +5 -0
  89. package/dist/tools/bash_kill/index.d.ts +1 -0
  90. package/dist/tools/bash_kill/index.js +35 -0
  91. package/dist/tools/bash_kill/prompt.d.ts +2 -0
  92. package/dist/tools/bash_kill/prompt.js +14 -0
  93. package/dist/tools/bash_logs/index.d.ts +1 -0
  94. package/dist/tools/bash_logs/index.js +40 -0
  95. package/dist/tools/bash_logs/prompt.d.ts +2 -0
  96. package/dist/tools/bash_logs/prompt.js +14 -0
  97. package/dist/tools/bash_run/index.d.ts +16 -0
  98. package/dist/tools/bash_run/index.js +61 -0
  99. package/dist/tools/bash_run/prompt.d.ts +2 -0
  100. package/dist/tools/bash_run/prompt.js +18 -0
  101. package/dist/tools/builtin.d.ts +9 -0
  102. package/dist/tools/builtin.js +27 -0
  103. package/dist/tools/define.d.ts +101 -0
  104. package/dist/tools/define.js +214 -0
  105. package/dist/tools/fs_edit/index.d.ts +1 -0
  106. package/dist/tools/fs_edit/index.js +62 -0
  107. package/dist/tools/fs_edit/prompt.d.ts +2 -0
  108. package/dist/tools/fs_edit/prompt.js +15 -0
  109. package/dist/tools/fs_glob/index.d.ts +1 -0
  110. package/dist/tools/fs_glob/index.js +60 -0
  111. package/dist/tools/fs_glob/prompt.d.ts +2 -0
  112. package/dist/tools/fs_glob/prompt.js +18 -0
  113. package/dist/tools/fs_grep/index.d.ts +1 -0
  114. package/dist/tools/fs_grep/index.js +66 -0
  115. package/dist/tools/fs_grep/prompt.d.ts +2 -0
  116. package/dist/tools/fs_grep/prompt.js +16 -0
  117. package/dist/tools/fs_multi_edit/index.d.ts +1 -0
  118. package/dist/tools/fs_multi_edit/index.js +106 -0
  119. package/dist/tools/fs_multi_edit/prompt.d.ts +2 -0
  120. package/dist/tools/fs_multi_edit/prompt.js +16 -0
  121. package/dist/tools/fs_read/index.d.ts +1 -0
  122. package/dist/tools/fs_read/index.js +40 -0
  123. package/dist/tools/fs_read/prompt.d.ts +2 -0
  124. package/dist/tools/fs_read/prompt.js +16 -0
  125. package/dist/tools/fs_rm/index.d.ts +1 -0
  126. package/dist/tools/fs_rm/index.js +41 -0
  127. package/dist/tools/fs_rm/prompt.d.ts +2 -0
  128. package/dist/tools/fs_rm/prompt.js +14 -0
  129. package/dist/tools/fs_write/index.d.ts +1 -0
  130. package/dist/tools/fs_write/index.js +40 -0
  131. package/dist/tools/fs_write/prompt.d.ts +2 -0
  132. package/dist/tools/fs_write/prompt.js +15 -0
  133. package/dist/tools/index.d.ts +9 -0
  134. package/dist/tools/index.js +56 -0
  135. package/dist/tools/mcp.d.ts +73 -0
  136. package/dist/tools/mcp.js +198 -0
  137. package/dist/tools/registry.d.ts +29 -0
  138. package/dist/tools/registry.js +26 -0
  139. package/dist/tools/skill_activate/index.d.ts +5 -0
  140. package/dist/tools/skill_activate/index.js +63 -0
  141. package/dist/tools/skill_list/index.d.ts +5 -0
  142. package/dist/tools/skill_list/index.js +48 -0
  143. package/dist/tools/skill_resource/index.d.ts +5 -0
  144. package/dist/tools/skill_resource/index.js +82 -0
  145. package/dist/tools/task_run/index.d.ts +7 -0
  146. package/dist/tools/task_run/index.js +60 -0
  147. package/dist/tools/task_run/prompt.d.ts +5 -0
  148. package/dist/tools/task_run/prompt.js +29 -0
  149. package/dist/tools/todo_read/index.d.ts +1 -0
  150. package/dist/tools/todo_read/index.js +29 -0
  151. package/dist/tools/todo_read/prompt.d.ts +2 -0
  152. package/dist/tools/todo_read/prompt.js +18 -0
  153. package/dist/tools/todo_write/index.d.ts +1 -0
  154. package/dist/tools/todo_write/index.js +42 -0
  155. package/dist/tools/todo_write/prompt.d.ts +2 -0
  156. package/dist/tools/todo_write/prompt.js +23 -0
  157. package/dist/tools/tool.d.ts +43 -0
  158. package/dist/tools/tool.js +104 -0
  159. package/dist/tools/toolkit.d.ts +69 -0
  160. package/dist/tools/toolkit.js +98 -0
  161. package/dist/tools/type-inference.d.ts +127 -0
  162. package/dist/tools/type-inference.js +207 -0
  163. package/dist/utils/agent-id.d.ts +1 -0
  164. package/dist/utils/agent-id.js +28 -0
  165. package/dist/utils/session-id.d.ts +21 -0
  166. package/dist/utils/session-id.js +64 -0
  167. package/dist/utils/unicode.d.ts +17 -0
  168. package/dist/utils/unicode.js +62 -0
  169. package/package.json +117 -0
@@ -0,0 +1,2334 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Agent = void 0;
7
+ const events_1 = require("./events");
8
+ const hooks_1 = require("./hooks");
9
+ const scheduler_1 = require("./scheduler");
10
+ const context_manager_1 = require("./context-manager");
11
+ const file_pool_1 = require("./file-pool");
12
+ const ajv_1 = __importDefault(require("ajv"));
13
+ const todo_1 = require("./todo");
14
+ const provider_1 = require("../infra/provider");
15
+ const breakpoint_manager_1 = require("./agent/breakpoint-manager");
16
+ const permission_manager_1 = require("./agent/permission-manager");
17
+ const todo_read_1 = require("../tools/todo_read");
18
+ const todo_write_1 = require("../tools/todo_write");
19
+ const errors_1 = require("./errors");
20
+ const message_queue_1 = require("./agent/message-queue");
21
+ const todo_manager_1 = require("./agent/todo-manager");
22
+ const tool_runner_1 = require("./agent/tool-runner");
23
+ const skills_1 = require("../skills");
24
+ const CONFIG_VERSION = 'v2.7.0';
25
+ class Agent {
26
+ get persistentStore() {
27
+ if (!this.deps.store) {
28
+ throw new Error('Agent persistent store is not configured for this operation.');
29
+ }
30
+ return this.deps.store;
31
+ }
32
+ static requireStore(deps) {
33
+ if (!deps.store) {
34
+ throw new errors_1.ResumeError('CORRUPTED_DATA', 'Agent store is not configured.');
35
+ }
36
+ return deps.store;
37
+ }
38
+ constructor(config, deps, runtime) {
39
+ this.config = config;
40
+ this.deps = deps;
41
+ this.events = new events_1.EventBus();
42
+ this.hooks = new hooks_1.HookManager();
43
+ this.ajv = new ajv_1.default({ allErrors: true, strict: false });
44
+ this.validatorCache = new Map();
45
+ this.invalidToolArgsStreak = 0;
46
+ this.invalidToolArgsLastTool = '';
47
+ this.suppressAutoContinue = false;
48
+ this.nextModelToolsOverride = null;
49
+ this.toolControllers = new Map();
50
+ this.toolAbortNotes = new Map();
51
+ this.tools = new Map();
52
+ this.toolDescriptors = [];
53
+ this.toolDescriptorIndex = new Map();
54
+ this.pendingPermissions = new Map();
55
+ this.messages = [];
56
+ this.state = 'READY';
57
+ this.toolRecords = new Map();
58
+ this.interrupted = false;
59
+ this.processingPromise = null;
60
+ this.lastProcessingStart = 0;
61
+ this.PROCESSING_TIMEOUT = 5 * 60 * 1000; // 5 分钟
62
+ this.processingQueued = false;
63
+ this.stepCount = 0;
64
+ this.lastSfpIndex = -1;
65
+ this.lineage = [];
66
+ Agent.requireStore(this.deps);
67
+ this.template = runtime.template;
68
+ this.model = runtime.model;
69
+ this.sandbox = runtime.sandbox;
70
+ this.sandboxConfig = runtime.sandboxConfig;
71
+ this.permission = runtime.permission;
72
+ this.subagents = runtime.subagents;
73
+ this.exposeThinking = config.exposeThinking ?? runtime.template.runtime?.exposeThinking ?? false;
74
+ this.toolDescriptors = runtime.toolDescriptors;
75
+ for (const descriptor of this.toolDescriptors) {
76
+ this.toolDescriptorIndex.set(descriptor.name, descriptor);
77
+ }
78
+ this.todoConfig = runtime.todoConfig;
79
+ this.permissions = new permission_manager_1.PermissionManager(this.permission, this.toolDescriptorIndex);
80
+ this.scheduler = new scheduler_1.Scheduler({
81
+ onTrigger: (info) => {
82
+ this.events.emitMonitor({
83
+ channel: 'monitor',
84
+ type: 'scheduler_triggered',
85
+ taskId: info.taskId,
86
+ spec: info.spec,
87
+ kind: info.kind,
88
+ triggeredAt: Date.now(),
89
+ });
90
+ },
91
+ });
92
+ const runtimeMeta = { ...(this.template.runtime?.metadata || {}), ...(config.metadata || {}) };
93
+ this.createdAt = new Date().toISOString();
94
+ this.toolTimeoutMs = typeof runtimeMeta.toolTimeoutMs === 'number' ? runtimeMeta.toolTimeoutMs : 60000;
95
+ this.maxToolConcurrency = typeof runtimeMeta.maxToolConcurrency === 'number' ? runtimeMeta.maxToolConcurrency : 3;
96
+ this.toolRunner = new tool_runner_1.ToolRunner(Math.max(1, this.maxToolConcurrency));
97
+ for (const tool of runtime.tools) {
98
+ this.tools.set(tool.name, tool);
99
+ if (tool.hooks) {
100
+ this.hooks.register(tool.hooks, 'toolTune');
101
+ }
102
+ }
103
+ if (this.template.hooks) {
104
+ this.hooks.register(this.template.hooks, 'agent');
105
+ }
106
+ if (config.overrides?.hooks) {
107
+ this.hooks.register(config.overrides.hooks, 'agent');
108
+ }
109
+ this.breakpoints = new breakpoint_manager_1.BreakpointManager((previous, current, entry) => {
110
+ this.events.emitMonitor({
111
+ channel: 'monitor',
112
+ type: 'breakpoint_changed',
113
+ previous,
114
+ current,
115
+ timestamp: entry.timestamp,
116
+ });
117
+ });
118
+ this.breakpoints.set('READY');
119
+ if (runtime.todoConfig?.enabled) {
120
+ this.todoService = new todo_1.TodoService(this.persistentStore, this.agentId);
121
+ }
122
+ this.filePool = new file_pool_1.FilePool(this.sandbox, {
123
+ watch: this.sandboxConfig?.watchFiles !== false,
124
+ onChange: (event) => this.handleExternalFileChange(event.path, event.mtime),
125
+ });
126
+ // 创建 ContextManager,传入模型配置和事件发送器
127
+ const contextEventEmitter = {
128
+ emit: (event) => {
129
+ this.events.emitMonitor({
130
+ channel: 'monitor',
131
+ type: 'context_compression',
132
+ ...event,
133
+ });
134
+ },
135
+ };
136
+ this.contextManager = new context_manager_1.ContextManager(this.persistentStore, this.agentId, {
137
+ ...runtime.context,
138
+ modelConfig: this.model.toConfig(),
139
+ modelFactory: ensureModelFactory(this.deps.modelFactory),
140
+ eventEmitter: contextEventEmitter,
141
+ });
142
+ this.messageQueue = new message_queue_1.MessageQueue({
143
+ wrapReminder: this.wrapReminder.bind(this),
144
+ addMessage: (message, kind) => this.enqueueMessage(message, kind),
145
+ persist: () => this.persistMessages(),
146
+ ensureProcessing: () => this.ensureProcessing(),
147
+ });
148
+ this.todoManager = new todo_manager_1.TodoManager({
149
+ service: this.todoService,
150
+ config: this.todoConfig,
151
+ events: this.events,
152
+ remind: (content, options) => this.remind(content, options),
153
+ });
154
+ this.events.setStore(this.persistentStore, this.agentId);
155
+ // 初始化 Skills Manager
156
+ this.skillsConfig = runtime.skills;
157
+ if (this.skillsConfig) {
158
+ this.skillsManager = new skills_1.SkillsManager(this.skillsConfig, this.sandbox, this.persistentStore, this.agentId);
159
+ }
160
+ // 自动注入工具说明书到系统提示
161
+ this.injectManualIntoSystemPrompt();
162
+ }
163
+ get agentId() {
164
+ return this.config.agentId;
165
+ }
166
+ static async create(config, deps) {
167
+ if (!config.agentId) {
168
+ config.agentId = Agent.generateAgentId();
169
+ }
170
+ const template = deps.templateRegistry.get(config.templateId);
171
+ const sandboxConfig = config.sandbox && 'kind' in config.sandbox
172
+ ? config.sandbox
173
+ : template.sandbox;
174
+ const sandbox = typeof config.sandbox === 'object' && 'exec' in config.sandbox
175
+ ? config.sandbox
176
+ : deps.sandboxFactory.create(sandboxConfig || { kind: 'local', workDir: process.cwd() });
177
+ const model = config.model
178
+ ? config.model
179
+ : config.modelConfig
180
+ ? ensureModelFactory(deps.modelFactory)(config.modelConfig)
181
+ : template.model
182
+ ? ensureModelFactory(deps.modelFactory)({ provider: 'anthropic', model: template.model })
183
+ : ensureModelFactory(deps.modelFactory)({ provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' });
184
+ const resolvedTools = resolveTools(config, template, deps.toolRegistry, deps.templateRegistry);
185
+ const permissionConfig = config.overrides?.permission || template.permission || { mode: 'auto' };
186
+ const normalizedPermission = {
187
+ ...permissionConfig,
188
+ mode: permissionConfig.mode || 'auto',
189
+ };
190
+ const agent = new Agent(config, deps, {
191
+ template,
192
+ model,
193
+ sandbox,
194
+ sandboxConfig,
195
+ tools: resolvedTools.instances,
196
+ toolDescriptors: resolvedTools.descriptors,
197
+ permission: normalizedPermission,
198
+ todoConfig: config.overrides?.todo || template.runtime?.todo,
199
+ subagents: config.overrides?.subagents || template.runtime?.subagents,
200
+ context: config.context || template.runtime?.metadata?.context,
201
+ skills: config.skills,
202
+ });
203
+ await agent.initialize();
204
+ return agent;
205
+ }
206
+ async initialize() {
207
+ await this.todoService?.load();
208
+ const messages = await this.persistentStore.loadMessages(this.agentId);
209
+ this.messages = messages;
210
+ this.lastSfpIndex = this.findLastSfp();
211
+ this.stepCount = messages.filter((m) => m.role === 'user').length;
212
+ const records = await this.persistentStore.loadToolCallRecords(this.agentId);
213
+ this.toolRecords = new Map(records.map((record) => [record.id, this.normalizeToolRecord(record)]));
214
+ if (this.todoService) {
215
+ this.registerTodoTools();
216
+ this.todoManager.handleStartup();
217
+ }
218
+ // 初始化 Skills
219
+ if (this.skillsManager) {
220
+ await this.skillsManager.restoreState();
221
+ const skills = await this.skillsManager.discover();
222
+ if (skills.length > 0) {
223
+ this.injectSkillsIntoSystemPrompt(skills);
224
+ this.events.emitMonitor({
225
+ channel: 'monitor',
226
+ type: 'skill_discovered',
227
+ skills: skills.map((s) => s.name),
228
+ timestamp: Date.now(),
229
+ });
230
+ // 自动激活 Template 配置的核心 Skills
231
+ const templateSkillsConfig = this.template.runtime?.skills;
232
+ if (templateSkillsConfig?.autoActivate && templateSkillsConfig.autoActivate.length > 0) {
233
+ const autoActivated = await this.skillsManager.autoActivateSkills(templateSkillsConfig.autoActivate);
234
+ if (autoActivated.length > 0) {
235
+ // 将自动激活的 Skills 内容注入上下文
236
+ for (const skill of autoActivated) {
237
+ const instructionsXml = skills_1.SkillsInjector.toActivatedXML(skill);
238
+ this.remind(instructionsXml, {
239
+ category: 'general',
240
+ persistent: true,
241
+ label: `skill:${skill.name}`,
242
+ });
243
+ }
244
+ this.events.emitMonitor({
245
+ channel: 'monitor',
246
+ type: 'skill_activated',
247
+ skill: autoActivated.map((s) => s.name).join(', '),
248
+ activatedBy: 'auto',
249
+ timestamp: Date.now(),
250
+ });
251
+ }
252
+ }
253
+ // 注入推荐 Skills 提示
254
+ if (templateSkillsConfig?.recommend && templateSkillsConfig.recommend.length > 0) {
255
+ const recommendedSkills = templateSkillsConfig.recommend
256
+ .filter((name) => this.skillsManager?.get(name) && !this.skillsManager?.isActivated(name));
257
+ if (recommendedSkills.length > 0) {
258
+ const recommendXml = `\n<recommended_skills>\nConsider activating these skills if relevant to your task: ${recommendedSkills.join(', ')}\n</recommended_skills>\n`;
259
+ if (this.template.systemPrompt) {
260
+ this.template.systemPrompt += recommendXml;
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ await this.persistInfo();
267
+ }
268
+ async *chatStream(input, opts) {
269
+ const since = opts?.since ?? this.events.getLastBookmark();
270
+ await this.send(input);
271
+ const subscription = this.events.subscribeProgress({ since, kinds: opts?.kinds });
272
+ for await (const event of subscription) {
273
+ yield event;
274
+ if (event.event.type === 'done') {
275
+ this.lastBookmark = event.bookmark;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ async chat(input, opts) {
281
+ let streamedText = '';
282
+ let bookmark;
283
+ for await (const envelope of this.chatStream(input, opts)) {
284
+ if (envelope.event.type === 'text_chunk') {
285
+ streamedText += envelope.event.delta;
286
+ }
287
+ if (envelope.event.type === 'done') {
288
+ bookmark = envelope.bookmark;
289
+ }
290
+ }
291
+ const pending = Array.from(this.pendingPermissions.keys());
292
+ let finalText = streamedText;
293
+ const lastAssistant = [...this.messages].reverse().find((message) => message.role === 'assistant');
294
+ if (lastAssistant) {
295
+ const combined = lastAssistant.content
296
+ .filter((block) => block.type === 'text')
297
+ .map((block) => block.text)
298
+ .join('\n');
299
+ if (combined.trim().length > 0) {
300
+ finalText = combined;
301
+ }
302
+ }
303
+ return {
304
+ status: pending.length ? 'paused' : 'ok',
305
+ text: finalText,
306
+ last: bookmark,
307
+ permissionIds: pending,
308
+ };
309
+ }
310
+ async complete(input, opts) {
311
+ return this.chat(input, opts);
312
+ }
313
+ async *stream(input, opts) {
314
+ yield* this.chatStream(input, opts);
315
+ }
316
+ async send(text, options) {
317
+ return this.messageQueue.send(text, options);
318
+ }
319
+ schedule() {
320
+ return this.scheduler;
321
+ }
322
+ /**
323
+ * Force the agent to (re)enter the processing loop.
324
+ * Useful for recovering from transient stalls after restarts.
325
+ */
326
+ kick() {
327
+ if (this.state === 'PAUSED')
328
+ return; // waiting for approval
329
+ this.ensureProcessing();
330
+ }
331
+ on(event, handler) {
332
+ if (event === 'permission_required' || event === 'permission_decided') {
333
+ return this.events.onControl(event, handler);
334
+ }
335
+ return this.events.onMonitor(event, handler);
336
+ }
337
+ subscribe(channels, opts) {
338
+ if (!opts || (!opts.since && !opts.kinds)) {
339
+ return this.events.subscribe(channels);
340
+ }
341
+ return this.events.subscribe(channels, { since: opts.since, kinds: opts.kinds });
342
+ }
343
+ getTodos() {
344
+ return this.todoManager.list();
345
+ }
346
+ async setTodos(todos) {
347
+ await this.todoManager.setTodos(todos);
348
+ }
349
+ async updateTodo(todo) {
350
+ await this.todoManager.update(todo);
351
+ }
352
+ async deleteTodo(id) {
353
+ await this.todoManager.remove(id);
354
+ }
355
+ async decide(permissionId, decision, note) {
356
+ const pending = this.pendingPermissions.get(permissionId);
357
+ if (!pending)
358
+ throw new Error(`Permission not pending: ${permissionId}`);
359
+ pending.resolve(decision, note);
360
+ this.pendingPermissions.delete(permissionId);
361
+ this.events.emitControl({
362
+ channel: 'control',
363
+ type: 'permission_decided',
364
+ callId: permissionId,
365
+ decision,
366
+ decidedBy: 'api',
367
+ note,
368
+ });
369
+ if (decision === 'allow') {
370
+ this.setState('WORKING');
371
+ this.setBreakpoint('PRE_TOOL');
372
+ this.ensureProcessing();
373
+ }
374
+ else {
375
+ this.setBreakpoint('POST_TOOL');
376
+ this.setState('READY');
377
+ }
378
+ }
379
+ async interrupt(opts) {
380
+ this.interrupted = true;
381
+ this.toolRunner.clear();
382
+ for (const controller of this.toolControllers.values()) {
383
+ controller.abort();
384
+ }
385
+ this.toolControllers.clear();
386
+ this.toolAbortNotes.clear();
387
+ await this.appendSyntheticToolResults(opts?.note || 'Interrupted by user');
388
+ this.setState('READY');
389
+ this.setBreakpoint('READY');
390
+ }
391
+ /**
392
+ * Abort a single in-flight tool call (TS-aligned: per-tool AbortController).
393
+ * Returns true when an active controller existed and was aborted.
394
+ */
395
+ abortToolCall(callId, opts) {
396
+ const controller = this.toolControllers.get(callId);
397
+ if (!controller)
398
+ return false;
399
+ if (opts?.note)
400
+ this.toolAbortNotes.set(callId, opts.note);
401
+ controller.abort();
402
+ return true;
403
+ }
404
+ async snapshot(label) {
405
+ const id = label || `sfp:${this.lastSfpIndex}`;
406
+ const snapshot = {
407
+ id,
408
+ messages: JSON.parse(JSON.stringify(this.messages)),
409
+ lastSfpIndex: this.lastSfpIndex,
410
+ lastBookmark: this.lastBookmark ?? { seq: -1, timestamp: Date.now() },
411
+ createdAt: new Date().toISOString(),
412
+ metadata: {
413
+ stepCount: this.stepCount,
414
+ },
415
+ };
416
+ await this.persistentStore.saveSnapshot(this.agentId, snapshot);
417
+ return id;
418
+ }
419
+ async fork(sel) {
420
+ const snapshotId = typeof sel === 'string' ? sel : sel?.at ?? (await this.snapshot());
421
+ const snapshot = await this.persistentStore.loadSnapshot(this.agentId, snapshotId);
422
+ if (!snapshot)
423
+ throw new Error(`Snapshot not found: ${snapshotId}`);
424
+ const forkId = `${this.agentId}/fork:${Date.now()}`;
425
+ const forkConfig = {
426
+ ...this.config,
427
+ agentId: forkId,
428
+ };
429
+ const fork = await Agent.create(forkConfig, this.deps);
430
+ fork.messages = JSON.parse(JSON.stringify(snapshot.messages));
431
+ fork.lastSfpIndex = snapshot.lastSfpIndex;
432
+ fork.stepCount = snapshot.metadata?.stepCount ?? fork.messages.filter((m) => m.role === 'user').length;
433
+ fork.lineage = [...this.lineage, this.agentId];
434
+ await fork.persistMessages();
435
+ return fork;
436
+ }
437
+ async status() {
438
+ return {
439
+ agentId: this.agentId,
440
+ state: this.state,
441
+ stepCount: this.stepCount,
442
+ lastSfpIndex: this.lastSfpIndex,
443
+ lastBookmark: this.lastBookmark,
444
+ cursor: this.events.getCursor(),
445
+ breakpoint: this.breakpoints.getCurrent(),
446
+ };
447
+ }
448
+ async info() {
449
+ return {
450
+ agentId: this.agentId,
451
+ templateId: this.template.id,
452
+ createdAt: this.createdAt,
453
+ lineage: this.lineage,
454
+ configVersion: CONFIG_VERSION,
455
+ messageCount: this.messages.length,
456
+ lastSfpIndex: this.lastSfpIndex,
457
+ lastBookmark: this.lastBookmark,
458
+ breakpoint: this.breakpoints.getCurrent(),
459
+ };
460
+ }
461
+ setBreakpoint(state, note) {
462
+ this.breakpoints.set(state, note);
463
+ }
464
+ remind(content, options) {
465
+ this.messageQueue.send(content, { kind: 'reminder', reminder: options });
466
+ this.events.emitMonitor({
467
+ channel: 'monitor',
468
+ type: 'reminder_sent',
469
+ category: options?.category ?? 'general',
470
+ content,
471
+ });
472
+ }
473
+ // ========== Skills 相关方法 ==========
474
+ /**
475
+ * 获取 Skills Manager
476
+ */
477
+ getSkillsManager() {
478
+ return this.skillsManager;
479
+ }
480
+ /**
481
+ * 激活 Skill 并将其完整指令注入上下文
482
+ */
483
+ async activateSkill(name, activatedBy = 'agent') {
484
+ if (!this.skillsManager) {
485
+ throw new Error('Skills not configured for this agent');
486
+ }
487
+ const skill = await this.skillsManager.activate(name, activatedBy);
488
+ // 使用 remind 机制将 Skill 完整指令注入上下文
489
+ const instructionsXml = skills_1.SkillsInjector.toActivatedXML(skill);
490
+ this.remind(instructionsXml, {
491
+ category: 'general',
492
+ priority: 'high',
493
+ skipStandardEnding: true,
494
+ });
495
+ this.events.emitMonitor({
496
+ channel: 'monitor',
497
+ type: 'skill_activated',
498
+ skill: skill.name,
499
+ activatedBy,
500
+ timestamp: Date.now(),
501
+ });
502
+ return skill;
503
+ }
504
+ /**
505
+ * 停用 Skill(并注入一个“取消生效”提醒,覆盖之前的指令)
506
+ */
507
+ async deactivateSkill(name) {
508
+ if (!this.skillsManager) {
509
+ throw new Error('Skills not configured for this agent');
510
+ }
511
+ await this.skillsManager.deactivate(name);
512
+ // We cannot reliably delete previously injected reminders from message history.
513
+ // Instead, inject a newer reminder telling the model to ignore that skill's instructions.
514
+ this.remind(`<skill_deactivated name="${name}">\nThis skill has been deactivated. Ignore its previous instructions unless re-activated.\n</skill_deactivated>`, {
515
+ category: 'general',
516
+ priority: 'high',
517
+ // Wrap as a system-reminder so it stays invisible to users/exports.
518
+ });
519
+ this.events.emitMonitor({
520
+ channel: 'monitor',
521
+ type: 'skill_deactivated',
522
+ name,
523
+ timestamp: Date.now(),
524
+ });
525
+ }
526
+ /**
527
+ * 将 Skills 元数据注入系统提示
528
+ */
529
+ injectSkillsIntoSystemPrompt(skills) {
530
+ if (skills.length === 0)
531
+ return;
532
+ const skillsXml = skills_1.SkillsInjector.toPromptXML(skills);
533
+ if (skillsXml && this.template.systemPrompt) {
534
+ this.template.systemPrompt += skillsXml;
535
+ }
536
+ }
537
+ async spawnSubAgent(templateId, prompt, runtime) {
538
+ if (!this.subagents) {
539
+ throw new Error('Sub-agent configuration not enabled for this agent');
540
+ }
541
+ const remaining = runtime?.depthRemaining ?? this.subagents.depth;
542
+ if (remaining <= 0) {
543
+ throw new Error('Sub-agent recursion limit reached');
544
+ }
545
+ if (this.subagents.templates && !this.subagents.templates.includes(templateId)) {
546
+ throw new Error(`Template ${templateId} not allowed for sub-agent`);
547
+ }
548
+ const subConfig = {
549
+ templateId,
550
+ modelConfig: this.model.toConfig(),
551
+ sandbox: this.sandboxConfig || { kind: 'local', workDir: this.sandbox.workDir },
552
+ exposeThinking: this.exposeThinking,
553
+ metadata: this.config.metadata,
554
+ overrides: {
555
+ permission: this.subagents.overrides?.permission || this.permission,
556
+ todo: this.subagents.overrides?.todo || this.template.runtime?.todo,
557
+ subagents: this.subagents.inheritConfig ? { ...this.subagents, depth: remaining - 1 } : undefined,
558
+ },
559
+ };
560
+ const subAgent = await Agent.create(subConfig, this.deps);
561
+ subAgent.lineage = [...this.lineage, this.agentId];
562
+ const result = await subAgent.complete(prompt);
563
+ return result;
564
+ }
565
+ /**
566
+ * Create and run a sub-agent with a task, without requiring subagents config.
567
+ * This is useful for tools that want to delegate work to specialized agents.
568
+ *
569
+ * @param config.streamEvents - Whether to forward sub-agent events to the parent agent.
570
+ * Defaults to true. Set to false to disable streaming.
571
+ */
572
+ async delegateTask(config) {
573
+ const subAgentConfig = {
574
+ templateId: config.templateId,
575
+ modelConfig: config.model
576
+ ? { provider: 'anthropic', model: config.model }
577
+ : this.model.toConfig(),
578
+ sandbox: this.sandboxConfig || { kind: 'local', workDir: this.sandbox.workDir },
579
+ tools: config.tools,
580
+ metadata: {
581
+ ...this.config.metadata,
582
+ parentAgentId: this.agentId,
583
+ delegatedBy: 'task_tool',
584
+ parentCallId: config.callId,
585
+ },
586
+ };
587
+ const subAgent = await Agent.create(subAgentConfig, this.deps);
588
+ subAgent.lineage = [...this.lineage, this.agentId];
589
+ const subAgentId = subAgent.agentId;
590
+ this.events.emitMonitor({
591
+ channel: 'monitor',
592
+ type: 'subagent.created',
593
+ callId: config.callId,
594
+ agentId: subAgentId,
595
+ templateId: config.templateId,
596
+ parentAgentId: this.agentId,
597
+ timestamp: Date.now(),
598
+ });
599
+ // Subscribe to sub-agent events and forward them to parent agent's EventBus
600
+ // This enables real-time streaming of sub-agent progress to the frontend
601
+ let unsubscribe;
602
+ const shouldStream = config.streamEvents !== false;
603
+ if (shouldStream) {
604
+ // Track accumulated text for text_chunk events
605
+ let accumulatedText = '';
606
+ // Start async iteration in the background
607
+ const iterator = subAgent.subscribe(['progress', 'control'])[Symbol.asyncIterator]();
608
+ let iterating = true;
609
+ const iterateEvents = async () => {
610
+ try {
611
+ while (iterating) {
612
+ const { value: env, done } = await iterator.next();
613
+ if (done || !iterating)
614
+ break;
615
+ const evt = env?.event;
616
+ if (!evt || typeof evt !== 'object')
617
+ continue;
618
+ // Forward text streaming events
619
+ if (evt.type === 'text_chunk') {
620
+ const delta = typeof evt.delta === 'string' ? evt.delta : '';
621
+ if (delta) {
622
+ accumulatedText += delta;
623
+ this.events.emitMonitor({
624
+ channel: 'monitor',
625
+ type: 'subagent.delta',
626
+ subAgentId,
627
+ templateId: config.templateId,
628
+ callId: config.callId,
629
+ delta,
630
+ text: accumulatedText,
631
+ step: evt.step,
632
+ timestamp: Date.now(),
633
+ });
634
+ }
635
+ }
636
+ // Forward thinking events (if exposed)
637
+ if (evt.type === 'think_chunk') {
638
+ const delta = typeof evt.delta === 'string' ? evt.delta : '';
639
+ if (delta) {
640
+ this.events.emitMonitor({
641
+ channel: 'monitor',
642
+ type: 'subagent.thinking',
643
+ subAgentId,
644
+ templateId: config.templateId,
645
+ callId: config.callId,
646
+ delta,
647
+ step: evt.step,
648
+ timestamp: Date.now(),
649
+ });
650
+ }
651
+ }
652
+ // Forward tool start events
653
+ if (evt.type === 'tool:start') {
654
+ const call = evt.call || {};
655
+ this.events.emitMonitor({
656
+ channel: 'monitor',
657
+ type: 'subagent.tool_start',
658
+ subAgentId,
659
+ templateId: config.templateId,
660
+ parentCallId: config.callId,
661
+ toolCallId: call.id,
662
+ toolName: call.name,
663
+ inputPreview: call.inputPreview,
664
+ timestamp: Date.now(),
665
+ });
666
+ }
667
+ // Forward tool end events
668
+ if (evt.type === 'tool:end') {
669
+ const call = evt.call || {};
670
+ this.events.emitMonitor({
671
+ channel: 'monitor',
672
+ type: 'subagent.tool_end',
673
+ subAgentId,
674
+ templateId: config.templateId,
675
+ parentCallId: config.callId,
676
+ toolCallId: call.id,
677
+ toolName: call.name,
678
+ durationMs: call.durationMs,
679
+ isError: call.isError,
680
+ timestamp: Date.now(),
681
+ });
682
+ }
683
+ // Forward permission required events
684
+ if (evt.type === 'permission_required') {
685
+ const call = evt.call || {};
686
+ this.events.emitMonitor({
687
+ channel: 'monitor',
688
+ type: 'subagent.permission_required',
689
+ subAgentId,
690
+ templateId: config.templateId,
691
+ parentCallId: config.callId,
692
+ toolCallId: call.id,
693
+ toolName: call.name,
694
+ timestamp: Date.now(),
695
+ });
696
+ }
697
+ }
698
+ }
699
+ catch {
700
+ // Ignore iteration errors (e.g., when agent completes)
701
+ }
702
+ };
703
+ // Start event iteration in background
704
+ const iterationPromise = iterateEvents();
705
+ unsubscribe = () => {
706
+ iterating = false;
707
+ // Signal iterator to stop
708
+ iterator.return?.();
709
+ };
710
+ }
711
+ try {
712
+ const result = await subAgent.complete(config.prompt);
713
+ return { ...result, agentId: subAgentId };
714
+ }
715
+ finally {
716
+ // Clean up subscription
717
+ if (unsubscribe) {
718
+ unsubscribe();
719
+ }
720
+ }
721
+ }
722
+ static async resume(agentId, config, deps, opts) {
723
+ const store = Agent.requireStore(deps);
724
+ const info = await store.loadInfo(agentId);
725
+ if (!info) {
726
+ throw new errors_1.ResumeError('AGENT_NOT_FOUND', `Agent metadata not found: ${agentId}`);
727
+ }
728
+ const metadata = info.metadata;
729
+ if (!metadata) {
730
+ throw new errors_1.ResumeError('CORRUPTED_DATA', `Agent metadata incomplete for: ${agentId}`);
731
+ }
732
+ const templateId = metadata.templateId;
733
+ let template;
734
+ try {
735
+ template = deps.templateRegistry.get(templateId);
736
+ }
737
+ catch (error) {
738
+ throw new errors_1.ResumeError('TEMPLATE_NOT_FOUND', `Template not registered: ${templateId}`);
739
+ }
740
+ if (config.templateVersion && metadata.templateVersion && config.templateVersion !== metadata.templateVersion) {
741
+ throw new errors_1.ResumeError('TEMPLATE_VERSION_MISMATCH', `Template version mismatch: expected ${config.templateVersion}, got ${metadata.templateVersion}`);
742
+ }
743
+ let sandbox;
744
+ try {
745
+ sandbox = deps.sandboxFactory.create(metadata.sandboxConfig || { kind: 'local', workDir: process.cwd() });
746
+ }
747
+ catch (error) {
748
+ throw new errors_1.ResumeError('SANDBOX_INIT_FAILED', error?.message || 'Failed to create sandbox');
749
+ }
750
+ const model = metadata.modelConfig
751
+ ? ensureModelFactory(deps.modelFactory)(metadata.modelConfig)
752
+ : ensureModelFactory(deps.modelFactory)({ provider: 'anthropic', model: template.model || 'claude-3-5-sonnet-20241022' });
753
+ const toolInstances = metadata.tools.map((descriptor) => {
754
+ try {
755
+ return deps.toolRegistry.create(descriptor.registryId || descriptor.name, descriptor.config);
756
+ }
757
+ catch (error) {
758
+ throw new errors_1.ResumeError('CORRUPTED_DATA', `Failed to restore tool ${descriptor.name}: ${error?.message || error}`);
759
+ }
760
+ });
761
+ const permissionConfig = metadata.permission || template.permission || { mode: 'auto' };
762
+ const normalizedPermission = {
763
+ ...permissionConfig,
764
+ mode: permissionConfig.mode || 'auto',
765
+ };
766
+ const agent = new Agent({ ...config, agentId, templateId: templateId, exposeThinking: config.exposeThinking ?? metadata.exposeThinking }, deps, {
767
+ template,
768
+ model,
769
+ sandbox,
770
+ sandboxConfig: metadata.sandboxConfig,
771
+ tools: toolInstances,
772
+ toolDescriptors: metadata.tools,
773
+ permission: normalizedPermission,
774
+ todoConfig: metadata.todo,
775
+ subagents: metadata.subagents,
776
+ context: metadata.context,
777
+ skills: metadata.skills || config.skills,
778
+ });
779
+ agent.lineage = metadata.lineage || [];
780
+ agent.createdAt = metadata.createdAt || agent.createdAt;
781
+ await agent.initialize();
782
+ if (metadata.breakpoint) {
783
+ agent.breakpoints.reset(metadata.breakpoint);
784
+ }
785
+ let messages;
786
+ try {
787
+ messages = await store.loadMessages(agentId);
788
+ }
789
+ catch (error) {
790
+ throw new errors_1.ResumeError('CORRUPTED_DATA', error?.message || 'Failed to load messages');
791
+ }
792
+ agent.messages = messages;
793
+ agent.lastSfpIndex = agent.findLastSfp();
794
+ agent.stepCount = messages.filter((m) => m.role === 'user').length;
795
+ const toolRecords = await store.loadToolCallRecords(agentId);
796
+ agent.toolRecords = new Map(toolRecords.map((record) => [record.id, agent.normalizeToolRecord(record)]));
797
+ if (opts?.strategy === 'crash') {
798
+ const sealed = await agent.autoSealIncompleteCalls();
799
+ const sealedDangling = await agent.autoSealDanglingToolUses('Sealed missing tool_result after crash-resume; verify potential side effects.');
800
+ agent.recoverFromStaleAwaitingApprovalAfterCrash();
801
+ agent.events.emitMonitor({
802
+ channel: 'monitor',
803
+ type: 'agent_resumed',
804
+ strategy: 'crash',
805
+ sealed: [...sealed, ...sealedDangling],
806
+ });
807
+ }
808
+ else {
809
+ agent.events.emitMonitor({
810
+ channel: 'monitor',
811
+ type: 'agent_resumed',
812
+ strategy: 'manual',
813
+ sealed: [],
814
+ });
815
+ }
816
+ if (opts?.autoRun) {
817
+ agent.ensureProcessing();
818
+ }
819
+ return agent;
820
+ }
821
+ static async resumeFromStore(agentId, deps, opts) {
822
+ const store = Agent.requireStore(deps);
823
+ const info = await store.loadInfo(agentId);
824
+ if (!info || !info.metadata) {
825
+ throw new errors_1.ResumeError('AGENT_NOT_FOUND', `Agent metadata not found: ${agentId}`);
826
+ }
827
+ const metadata = info.metadata;
828
+ const baseConfig = {
829
+ agentId,
830
+ templateId: metadata.templateId,
831
+ templateVersion: metadata.templateVersion,
832
+ modelConfig: metadata.modelConfig,
833
+ sandbox: metadata.sandboxConfig,
834
+ exposeThinking: metadata.exposeThinking,
835
+ context: metadata.context,
836
+ metadata: metadata.metadata,
837
+ overrides: {
838
+ permission: metadata.permission,
839
+ todo: metadata.todo,
840
+ subagents: metadata.subagents,
841
+ },
842
+ tools: metadata.tools.map((descriptor) => descriptor.registryId || descriptor.name),
843
+ };
844
+ const overrides = opts?.overrides ?? {};
845
+ return Agent.resume(agentId, { ...baseConfig, ...overrides }, deps, opts);
846
+ }
847
+ ensureProcessing() {
848
+ // 检查是否超时
849
+ if (this.processingPromise) {
850
+ const now = Date.now();
851
+ const elapsed = now - this.lastProcessingStart;
852
+ const bp = this.breakpoints.getCurrent();
853
+ // Waiting for approval is a valid "paused" state: do not treat it as a hung processing loop.
854
+ // Otherwise long approval waits can trigger a false timeout and cause the server to mark the run as error.
855
+ if (this.state === 'PAUSED' && bp === 'AWAITING_APPROVAL') {
856
+ this.processingQueued = true;
857
+ return;
858
+ }
859
+ // Long-running tools (e.g. sub-agent tasks / installs / builds) may legitimately exceed this timeout.
860
+ // Treat TOOL_EXECUTING as active work; rely on per-tool abort/timeout for truly stuck tools.
861
+ if (this.state === 'WORKING' && bp === 'TOOL_EXECUTING') {
862
+ this.processingQueued = true;
863
+ return;
864
+ }
865
+ if (elapsed > this.PROCESSING_TIMEOUT) {
866
+ this.events.emitMonitor({
867
+ channel: 'monitor',
868
+ type: 'error',
869
+ severity: 'error',
870
+ phase: 'lifecycle',
871
+ message: 'Processing timeout detected, forcing restart',
872
+ detail: {
873
+ lastStart: this.lastProcessingStart,
874
+ elapsed,
875
+ state: this.state,
876
+ breakpoint: bp,
877
+ },
878
+ });
879
+ this.processingPromise = null; // 强制重启
880
+ }
881
+ else {
882
+ this.processingQueued = true;
883
+ return; // 正常执行中(结束后会再跑一轮)
884
+ }
885
+ }
886
+ this.lastProcessingStart = Date.now();
887
+ this.processingPromise = this.runStep()
888
+ .finally(() => {
889
+ this.processingPromise = null;
890
+ if (this.processingQueued) {
891
+ this.processingQueued = false;
892
+ this.ensureProcessing();
893
+ }
894
+ })
895
+ .catch((err) => {
896
+ // 确保异常不会导致状态卡住
897
+ this.events.emitMonitor({
898
+ channel: 'monitor',
899
+ type: 'error',
900
+ severity: 'error',
901
+ phase: 'lifecycle',
902
+ message: 'Processing failed',
903
+ detail: { error: err.message, stack: err.stack }
904
+ });
905
+ this.setState('READY');
906
+ this.setBreakpoint('READY');
907
+ });
908
+ }
909
+ async runStep() {
910
+ if (this.state !== 'READY')
911
+ return;
912
+ if (this.interrupted) {
913
+ this.interrupted = false;
914
+ return;
915
+ }
916
+ // Defensive: Some crashes can persist an assistant tool_use without its required tool_result.
917
+ // Anthropic validates tool_use/tool_result ordering strictly; seal any dangling tool_use before calling the model.
918
+ try {
919
+ await this.autoSealDanglingToolUses();
920
+ }
921
+ catch (err) {
922
+ this.events.emitMonitor({
923
+ channel: 'monitor',
924
+ type: 'error',
925
+ severity: 'warn',
926
+ phase: 'lifecycle',
927
+ message: 'Failed to auto-seal dangling tool calls (continuing anyway)',
928
+ detail: { error: err?.message, stack: err?.stack },
929
+ });
930
+ }
931
+ this.setState('WORKING');
932
+ this.setBreakpoint('PRE_MODEL');
933
+ this.suppressAutoContinue = false;
934
+ this.suppressAutoContinueReason = undefined;
935
+ // Heartbeat: (re)starting a step counts as activity.
936
+ this.lastProcessingStart = Date.now();
937
+ try {
938
+ await this.messageQueue.flush();
939
+ const usage = this.contextManager.analyze(this.messages);
940
+ if (usage.shouldCompress) {
941
+ this.events.emitMonitor({
942
+ channel: 'monitor',
943
+ type: 'context_compression',
944
+ phase: 'start',
945
+ });
946
+ const compression = await this.contextManager.compress(this.messages, this.events.getTimeline(), this.filePool, this.sandbox);
947
+ if (compression) {
948
+ this.messages = [...compression.retainedMessages];
949
+ this.messages.unshift(compression.summary);
950
+ this.lastSfpIndex = this.messages.length - 1;
951
+ await this.persistMessages();
952
+ this.events.emitMonitor({
953
+ channel: 'monitor',
954
+ type: 'context_compression',
955
+ phase: 'end',
956
+ summary: compression.summary.content.map((block) => (block.type === 'text' ? block.text : JSON.stringify(block))).join('\n'),
957
+ ratio: compression.ratio,
958
+ });
959
+ }
960
+ }
961
+ // After compression (or any previous corruption), make sure we don't keep "orphaned" tool_result blocks
962
+ // that reference a missing tool_use id. Anthropic rejects such histories with invalid_request_error (2013).
963
+ try {
964
+ await this.sanitizeOrphanToolResults('Sanitized orphan tool_result before model call.');
965
+ }
966
+ catch (err) {
967
+ this.events.emitMonitor({
968
+ channel: 'monitor',
969
+ type: 'error',
970
+ severity: 'warn',
971
+ phase: 'lifecycle',
972
+ message: 'Failed to sanitize orphan tool_result blocks (continuing anyway)',
973
+ detail: { error: err?.message, stack: err?.stack },
974
+ });
975
+ }
976
+ // Compression can also create new dangling tool_use blocks (missing tool_result). Seal again defensively.
977
+ try {
978
+ await this.autoSealDanglingToolUses('Sealed missing tool_result after context compression; verify potential side effects.');
979
+ }
980
+ catch (err) {
981
+ this.events.emitMonitor({
982
+ channel: 'monitor',
983
+ type: 'error',
984
+ severity: 'warn',
985
+ phase: 'lifecycle',
986
+ message: 'Failed to auto-seal dangling tool calls after compression (continuing anyway)',
987
+ detail: { error: err?.message, stack: err?.stack },
988
+ });
989
+ }
990
+ await this.hooks.runPreModel(this.messages);
991
+ this.setBreakpoint('STREAMING_MODEL');
992
+ const toolsOverride = this.nextModelToolsOverride;
993
+ if (toolsOverride)
994
+ this.nextModelToolsOverride = null;
995
+ const tools = toolsOverride?.mode === 'none'
996
+ ? []
997
+ : toolsOverride?.mode === 'allowlist'
998
+ ? this.getToolSchemas().filter((t) => toolsOverride.allow.includes(String(t?.name || '')))
999
+ : this.getToolSchemas();
1000
+ if (toolsOverride) {
1001
+ this.events.emitMonitor({
1002
+ channel: 'monitor',
1003
+ type: 'tool_custom_event',
1004
+ toolName: 'agent',
1005
+ eventType: 'model_tools_override',
1006
+ data: toolsOverride,
1007
+ timestamp: Date.now(),
1008
+ });
1009
+ }
1010
+ const stream = this.model.stream(this.messages, {
1011
+ tools,
1012
+ maxTokens: this.config.metadata?.maxTokens,
1013
+ temperature: this.config.metadata?.temperature,
1014
+ system: this.template.systemPrompt,
1015
+ });
1016
+ const assistantBlocks = [];
1017
+ let currentBlockIndex = -1;
1018
+ const textBuffers = new Map();
1019
+ const toolBuffers = new Map();
1020
+ if (this.exposeThinking) {
1021
+ this.events.emitProgress({ channel: 'progress', type: 'think_chunk_start', step: this.stepCount });
1022
+ }
1023
+ for await (const chunk of stream) {
1024
+ // Heartbeat: receiving any stream chunk indicates forward progress.
1025
+ this.lastProcessingStart = Date.now();
1026
+ if (chunk.type === 'content_block_start') {
1027
+ if (chunk.content_block?.type === 'text') {
1028
+ currentBlockIndex = chunk.index ?? 0;
1029
+ assistantBlocks[currentBlockIndex] = { type: 'text', text: '' };
1030
+ this.events.emitProgress({ channel: 'progress', type: 'text_chunk_start', step: this.stepCount });
1031
+ }
1032
+ else if (chunk.content_block?.type === 'tool_use') {
1033
+ currentBlockIndex = chunk.index ?? 0;
1034
+ toolBuffers.set(currentBlockIndex, '');
1035
+ const inputFromStart = chunk.content_block?.input;
1036
+ assistantBlocks[currentBlockIndex] = {
1037
+ type: 'tool_use',
1038
+ id: chunk.content_block.id,
1039
+ name: chunk.content_block.name,
1040
+ // Some providers may include tool input in content_block_start (not only input_json_delta).
1041
+ input: inputFromStart && typeof inputFromStart === 'object' ? inputFromStart : {},
1042
+ };
1043
+ }
1044
+ }
1045
+ else if (chunk.type === 'content_block_delta') {
1046
+ const idx = typeof chunk.index === 'number' ? Number(chunk.index) : currentBlockIndex;
1047
+ if (chunk.delta?.type === 'text_delta') {
1048
+ const text = chunk.delta.text ?? '';
1049
+ const existing = textBuffers.get(idx) ?? '';
1050
+ textBuffers.set(idx, existing + text);
1051
+ if (assistantBlocks[idx]?.type === 'text') {
1052
+ assistantBlocks[idx].text = existing + text;
1053
+ }
1054
+ this.events.emitProgress({ channel: 'progress', type: 'text_chunk', step: this.stepCount, delta: text });
1055
+ }
1056
+ else if (chunk.delta?.type === 'input_json_delta') {
1057
+ const prev = toolBuffers.get(idx) ?? '';
1058
+ const next = prev + (chunk.delta.partial_json ?? '');
1059
+ toolBuffers.set(idx, next);
1060
+ try {
1061
+ const parsed = JSON.parse(next);
1062
+ if (assistantBlocks[idx]?.type === 'tool_use') {
1063
+ assistantBlocks[idx].input = parsed;
1064
+ }
1065
+ }
1066
+ catch {
1067
+ // continue buffering
1068
+ }
1069
+ }
1070
+ }
1071
+ else if (chunk.type === 'message_delta') {
1072
+ const inputTokens = chunk.usage?.input_tokens ?? 0;
1073
+ const outputTokens = chunk.usage?.output_tokens ?? 0;
1074
+ if (inputTokens || outputTokens) {
1075
+ this.events.emitMonitor({
1076
+ channel: 'monitor',
1077
+ type: 'token_usage',
1078
+ inputTokens,
1079
+ outputTokens,
1080
+ totalTokens: inputTokens + outputTokens,
1081
+ });
1082
+ }
1083
+ }
1084
+ else if (chunk.type === 'content_block_stop') {
1085
+ const stopIndex = typeof chunk.index === 'number' ? Number(chunk.index) : currentBlockIndex;
1086
+ if (assistantBlocks[stopIndex]?.type === 'text') {
1087
+ const fullText = textBuffers.get(stopIndex) ?? '';
1088
+ this.events.emitProgress({ channel: 'progress', type: 'text_chunk_end', step: this.stepCount, text: fullText });
1089
+ }
1090
+ else if (assistantBlocks[stopIndex]?.type === 'tool_use') {
1091
+ // Some providers may not emit input_json_delta; attempt a final parse if we have buffered json.
1092
+ const buf = toolBuffers.get(stopIndex) ?? '';
1093
+ if (buf) {
1094
+ try {
1095
+ const parsed = JSON.parse(buf);
1096
+ assistantBlocks[stopIndex].input = parsed;
1097
+ }
1098
+ catch {
1099
+ // ignore
1100
+ }
1101
+ }
1102
+ }
1103
+ currentBlockIndex = -1;
1104
+ toolBuffers.delete(stopIndex);
1105
+ }
1106
+ }
1107
+ if (this.exposeThinking) {
1108
+ this.events.emitProgress({ channel: 'progress', type: 'think_chunk_end', step: this.stepCount });
1109
+ }
1110
+ // Important: assistantBlocks can be sparse (holes) if the model emits non-zero `index`.
1111
+ // JSON serialization turns sparse array holes into `null`, which breaks resume (`block.type` access).
1112
+ let compactAssistantBlocks = assistantBlocks.filter((block) => Boolean(block && typeof block === 'object' && typeof block.type === 'string'));
1113
+ // Guard: interrupted streams / provider quirks can yield no content blocks at all.
1114
+ // Persisting an empty assistant message leads to a "空回复/像断线" experience.
1115
+ if (compactAssistantBlocks.length === 0) {
1116
+ this.events.emitMonitor({
1117
+ channel: 'monitor',
1118
+ type: 'error',
1119
+ severity: 'warn',
1120
+ phase: 'model',
1121
+ message: 'Model returned no content blocks (empty assistant message suppressed)',
1122
+ detail: { step: this.stepCount },
1123
+ });
1124
+ compactAssistantBlocks = [
1125
+ {
1126
+ type: 'text',
1127
+ text: '本轮回复生成过程中未收到有效内容(可能是网络中断或输出被截断)。你可以直接重试上一条消息;如果包含大段代码/长文本,建议拆分或先写入一个小骨架文件再逐步补全。',
1128
+ },
1129
+ ];
1130
+ }
1131
+ await this.hooks.runPostModel({ role: 'assistant', content: compactAssistantBlocks });
1132
+ this.messages.push({ role: 'assistant', content: compactAssistantBlocks });
1133
+ await this.persistMessages();
1134
+ const toolBlocks = compactAssistantBlocks.filter((block) => block.type === 'tool_use');
1135
+ if (toolBlocks.length > 0) {
1136
+ this.setBreakpoint('TOOL_PENDING');
1137
+ const outcomes = await this.executeTools(toolBlocks);
1138
+ if (outcomes.length > 0) {
1139
+ const nudge = this.nextModelNudgeText;
1140
+ if (nudge) {
1141
+ this.nextModelNudgeText = undefined;
1142
+ outcomes.unshift({ type: 'text', text: nudge });
1143
+ }
1144
+ this.messages.push({ role: 'user', content: outcomes });
1145
+ this.lastSfpIndex = this.messages.length - 1;
1146
+ this.stepCount++;
1147
+ await this.persistMessages();
1148
+ this.todoManager.onStep();
1149
+ if (this.suppressAutoContinue) {
1150
+ this.events.emitMonitor({
1151
+ channel: 'monitor',
1152
+ type: 'error',
1153
+ severity: 'warn',
1154
+ phase: 'tool',
1155
+ message: this.suppressAutoContinueReason || 'Auto-continue suppressed due to repeated invalid tool calls',
1156
+ detail: {
1157
+ invalidToolArgsStreak: this.invalidToolArgsStreak,
1158
+ lastTool: this.invalidToolArgsLastTool,
1159
+ },
1160
+ });
1161
+ // Recovery: don't leave the run stuck active without a progress.done event.
1162
+ // Force a text-only model turn to surface a user-facing next step.
1163
+ const reason = this.suppressAutoContinueReason || 'Repeated invalid tool inputs';
1164
+ this.suppressAutoContinue = false;
1165
+ this.suppressAutoContinueReason = undefined;
1166
+ this.nextModelToolsOverride = { mode: 'none', reason: 'invalid_tool_args_suppressed_auto_continue' };
1167
+ this.nextModelNudgeText =
1168
+ `Tool calls are failing repeatedly (${reason}). In your next response, DO NOT call any tools. ` +
1169
+ `Explain the issue to the user and propose a concrete next step (Retry, reduce output size, or split file writes).`;
1170
+ this.ensureProcessing();
1171
+ return;
1172
+ }
1173
+ this.ensureProcessing();
1174
+ return;
1175
+ }
1176
+ }
1177
+ else {
1178
+ this.lastSfpIndex = this.messages.length - 1;
1179
+ }
1180
+ const envelope = this.events.emitProgress({
1181
+ channel: 'progress',
1182
+ type: 'done',
1183
+ step: this.stepCount,
1184
+ reason: this.pendingPermissions.size > 0 ? 'interrupted' : 'completed',
1185
+ });
1186
+ this.lastBookmark = envelope.bookmark;
1187
+ this.stepCount++;
1188
+ this.scheduler.notifyStep(this.stepCount);
1189
+ this.todoManager.onStep();
1190
+ this.events.emitMonitor({ channel: 'monitor', type: 'step_complete', step: this.stepCount, bookmark: envelope.bookmark });
1191
+ }
1192
+ catch (error) {
1193
+ this.events.emitMonitor({
1194
+ channel: 'monitor',
1195
+ type: 'error',
1196
+ severity: 'error',
1197
+ phase: 'model',
1198
+ message: error?.message || 'Model execution failed',
1199
+ detail: { stack: error?.stack },
1200
+ });
1201
+ }
1202
+ finally {
1203
+ this.setState('READY');
1204
+ this.setBreakpoint('READY');
1205
+ }
1206
+ }
1207
+ async executeTools(toolUses) {
1208
+ const uses = toolUses.filter((block) => block.type === 'tool_use');
1209
+ if (uses.length === 0) {
1210
+ return [];
1211
+ }
1212
+ const results = new Map();
1213
+ await Promise.all(uses.map((use) => this.toolRunner.run(async () => {
1214
+ const result = await this.processToolCall(use);
1215
+ if (result) {
1216
+ results.set(use.id, result);
1217
+ }
1218
+ })));
1219
+ await this.persistToolRecords();
1220
+ const ordered = [];
1221
+ for (const use of uses) {
1222
+ const block = results.get(use.id);
1223
+ if (block) {
1224
+ ordered.push(block);
1225
+ }
1226
+ }
1227
+ return ordered;
1228
+ }
1229
+ async processToolCall(toolUse) {
1230
+ const tool = this.tools.get(toolUse.name);
1231
+ const record = this.registerToolRecord(toolUse);
1232
+ // Heartbeat: tool execution may be long; mark activity at tool start.
1233
+ this.lastProcessingStart = Date.now();
1234
+ this.events.emitProgress({ channel: 'progress', type: 'tool:start', call: this.snapshotToolRecord(record.id) });
1235
+ if (!tool) {
1236
+ const message = `Tool not found: ${toolUse.name}`;
1237
+ this.updateToolRecord(record.id, { state: 'FAILED', error: message, isError: true }, 'tool missing');
1238
+ this.events.emitMonitor({ channel: 'monitor', type: 'error', severity: 'warn', phase: 'tool', message });
1239
+ return this.makeToolResult(toolUse.id, {
1240
+ ok: false,
1241
+ error: message,
1242
+ recommendations: ['确认工具是否已注册', '检查模板或配置中的工具列表'],
1243
+ });
1244
+ }
1245
+ const validation = this.validateToolArgs(tool, toolUse.input);
1246
+ if (!validation.ok) {
1247
+ const required = Array.isArray(tool.input_schema?.required) ? tool.input_schema.required : [];
1248
+ const requiredKeys = required.map((v) => String(v)).filter(Boolean);
1249
+ const example = {};
1250
+ for (const k of requiredKeys) {
1251
+ if (k === 'path')
1252
+ example[k] = 'README.md';
1253
+ else if (k === 'content')
1254
+ example[k] = '(file contents...)';
1255
+ else if (k === 'id')
1256
+ example[k] = 'todo_1';
1257
+ else if (k.toLowerCase().includes('name'))
1258
+ example[k] = 'example';
1259
+ else
1260
+ example[k] = '(required)';
1261
+ }
1262
+ if (this.invalidToolArgsLastTool === toolUse.name)
1263
+ this.invalidToolArgsStreak += 1;
1264
+ else {
1265
+ this.invalidToolArgsLastTool = toolUse.name;
1266
+ this.invalidToolArgsStreak = 1;
1267
+ }
1268
+ const base = validation.error || 'Tool input validation failed';
1269
+ const message = `Tool input validation failed for ${toolUse.name}: ${base}`;
1270
+ this.updateToolRecord(record.id, { state: 'FAILED', error: message, isError: true }, 'input schema invalid');
1271
+ const input = toolUse.input;
1272
+ const isPlainObject = Boolean(input && typeof input === 'object' && !Array.isArray(input));
1273
+ const inputKeys = isPlainObject ? Object.keys(input) : [];
1274
+ const likelyTruncated = isPlainObject && inputKeys.length === 0 && requiredKeys.length > 0;
1275
+ // Help the model recover immediately:
1276
+ // - Narrow the tool list to the failing tool (avoid it "wandering")
1277
+ // - Nudge it to emit a minimal corrected tool call
1278
+ //
1279
+ // Rationale: many real-world failures are single-shot truncations that drop required keys (e.g. `path`).
1280
+ // Waiting for a multi-hit streak often feels like "the chat broke".
1281
+ this.nextModelToolsOverride = { mode: 'allowlist', allow: [toolUse.name], reason: 'recover_invalid_tool_args' };
1282
+ if (!this.nextModelNudgeText) {
1283
+ this.nextModelNudgeText =
1284
+ `Your last tool call to \`${toolUse.name}\` failed schema validation (missing required keys: ${requiredKeys.join(', ')}). ` +
1285
+ (likelyTruncated ? `The tool input looked empty, which often means your output was truncated at max tokens. ` : ``) +
1286
+ `Retry by emitting ONLY one \`tool_use\` block for \`${toolUse.name}\` with a complete JSON object containing all required keys. ` +
1287
+ `Keep the tool input small: if writing a large file, write a short skeleton placeholder first (headers only), then expand via multiple \`fs_edit\` patches.`;
1288
+ }
1289
+ if (this.invalidToolArgsStreak >= 6) {
1290
+ this.suppressAutoContinue = true;
1291
+ this.suppressAutoContinueReason = `Too many invalid tool calls for ${toolUse.name} (streak=${this.invalidToolArgsStreak}).`;
1292
+ }
1293
+ // Emit a custom monitor event so product UIs can surface a helpful prompt,
1294
+ // and servers can decide to end the run early (avoid infinite loops).
1295
+ this.events.emitMonitor({
1296
+ channel: 'monitor',
1297
+ type: 'tool_custom_event',
1298
+ toolName: toolUse.name,
1299
+ eventType: 'tool_input_invalid',
1300
+ data: {
1301
+ callId: toolUse.id,
1302
+ tool: toolUse.name,
1303
+ error: base,
1304
+ requiredKeys,
1305
+ example,
1306
+ streak: this.invalidToolArgsStreak,
1307
+ inputKeys,
1308
+ likelyTruncated,
1309
+ },
1310
+ timestamp: Date.now(),
1311
+ });
1312
+ return this.makeToolResult(toolUse.id, {
1313
+ ok: false,
1314
+ error: message,
1315
+ errorType: likelyTruncated ? 'truncated_tool_input' : 'invalid_tool_input',
1316
+ retryable: true,
1317
+ data: {
1318
+ tool: toolUse.name,
1319
+ requiredKeys: requiredKeys.length > 0 ? requiredKeys : undefined,
1320
+ },
1321
+ recommendations: [
1322
+ requiredKeys.length > 0 ? `Provide required keys: ${requiredKeys.join(', ')}` : 'Provide input matching the tool schema',
1323
+ requiredKeys.length > 0 ? `Example input: ${JSON.stringify(example)}` : 'Retry with corrected input',
1324
+ 'If writing large file content, write a small skeleton first and expand via multiple fs_edit patches.',
1325
+ 'If this keeps happening, it may be max-tokens truncation: reduce output size or switch model, then retry.',
1326
+ 'If the model keeps emitting invalid tool inputs, ask the user for missing details and then retry.',
1327
+ ],
1328
+ });
1329
+ }
1330
+ // Any successful validation resets the streak.
1331
+ this.invalidToolArgsStreak = 0;
1332
+ this.invalidToolArgsLastTool = '';
1333
+ const context = {
1334
+ agentId: this.agentId,
1335
+ callId: toolUse.id,
1336
+ sandbox: this.sandbox,
1337
+ agent: this,
1338
+ services: {
1339
+ todo: this.todoService,
1340
+ filePool: this.filePool,
1341
+ },
1342
+ };
1343
+ let approvalMeta;
1344
+ let requireApproval = false;
1345
+ const policyDecision = this.permissions.evaluate(toolUse.name);
1346
+ if (policyDecision === 'deny') {
1347
+ const message = 'Tool denied by policy';
1348
+ this.updateToolRecord(record.id, {
1349
+ state: 'DENIED',
1350
+ approval: buildApproval('deny', 'policy', message),
1351
+ error: message,
1352
+ isError: true,
1353
+ }, 'policy deny');
1354
+ this.setBreakpoint('POST_TOOL');
1355
+ this.events.emitProgress({ channel: 'progress', type: 'tool:end', call: this.snapshotToolRecord(record.id) });
1356
+ return this.makeToolResult(toolUse.id, {
1357
+ ok: false,
1358
+ error: message,
1359
+ recommendations: ['检查模板或权限配置的 allow/deny 列表', '如需执行该工具,请调整权限模式或审批策略'],
1360
+ });
1361
+ }
1362
+ if (policyDecision === 'ask') {
1363
+ requireApproval = true;
1364
+ approvalMeta = { reason: 'Policy requires approval', tool: toolUse.name };
1365
+ }
1366
+ const decision = await this.hooks.runPreToolUse({ id: toolUse.id, name: toolUse.name, args: toolUse.input, agentId: this.agentId }, context);
1367
+ if (decision) {
1368
+ if ('decision' in decision) {
1369
+ if (decision.decision === 'ask') {
1370
+ requireApproval = true;
1371
+ approvalMeta = { ...(approvalMeta || {}), ...(decision.meta || {}) };
1372
+ }
1373
+ else if (decision.decision === 'deny') {
1374
+ const message = decision.reason || 'Denied by hook';
1375
+ this.updateToolRecord(record.id, {
1376
+ state: 'DENIED',
1377
+ approval: buildApproval('deny', 'hook', message),
1378
+ error: message,
1379
+ isError: true,
1380
+ }, 'hook deny');
1381
+ this.setBreakpoint('POST_TOOL');
1382
+ this.events.emitProgress({ channel: 'progress', type: 'tool:end', call: this.snapshotToolRecord(record.id) });
1383
+ return this.makeToolResult(toolUse.id, {
1384
+ ok: false,
1385
+ error: decision.toolResult || message,
1386
+ recommendations: ['根据 Hook 给出的原因调整输入或策略'],
1387
+ });
1388
+ }
1389
+ }
1390
+ else if ('result' in decision) {
1391
+ this.updateToolRecord(record.id, {
1392
+ state: 'COMPLETED',
1393
+ result: decision.result,
1394
+ completedAt: Date.now(),
1395
+ }, 'hook provided result');
1396
+ this.events.emitMonitor({ channel: 'monitor', type: 'tool_executed', call: this.snapshotToolRecord(record.id) });
1397
+ this.setBreakpoint('POST_TOOL');
1398
+ this.events.emitProgress({ channel: 'progress', type: 'tool:end', call: this.snapshotToolRecord(record.id) });
1399
+ return this.makeToolResult(toolUse.id, { ok: true, data: decision.result });
1400
+ }
1401
+ }
1402
+ if (requireApproval) {
1403
+ this.setBreakpoint('AWAITING_APPROVAL');
1404
+ const decisionResult = await this.requestPermission(record.id, toolUse.name, toolUse.input, approvalMeta);
1405
+ if (decisionResult === 'deny') {
1406
+ const message = approvalMeta?.reason || 'Denied by approval';
1407
+ this.updateToolRecord(record.id, { state: 'DENIED', error: message, isError: true }, 'approval denied');
1408
+ this.setBreakpoint('POST_TOOL');
1409
+ this.events.emitProgress({ channel: 'progress', type: 'tool:end', call: this.snapshotToolRecord(record.id) });
1410
+ return this.makeToolResult(toolUse.id, { ok: false, error: message });
1411
+ }
1412
+ this.setBreakpoint('PRE_TOOL');
1413
+ }
1414
+ this.setBreakpoint('PRE_TOOL');
1415
+ this.updateToolRecord(record.id, { state: 'EXECUTING', startedAt: Date.now() }, 'execution start');
1416
+ this.setBreakpoint('TOOL_EXECUTING');
1417
+ this.lastProcessingStart = Date.now();
1418
+ const controller = new AbortController();
1419
+ this.toolControllers.set(toolUse.id, controller);
1420
+ context.signal = controller.signal;
1421
+ const timeoutId = setTimeout(() => controller.abort(), this.toolTimeoutMs);
1422
+ try {
1423
+ const output = await tool.exec(toolUse.input, context);
1424
+ this.lastProcessingStart = Date.now();
1425
+ // 检查 output 是否包含 ok 字段来判断工具是否成功
1426
+ const outputOk = output && typeof output === 'object' && 'ok' in output ? output.ok : true;
1427
+ let outcome = {
1428
+ id: toolUse.id,
1429
+ name: toolUse.name,
1430
+ ok: outputOk !== false,
1431
+ content: output
1432
+ };
1433
+ outcome = await this.hooks.runPostToolUse(outcome, context);
1434
+ if (toolUse.name === 'fs_read' && toolUse.input?.path) {
1435
+ await this.filePool.recordRead(toolUse.input.path);
1436
+ }
1437
+ if ((toolUse.name === 'fs_write' || toolUse.name === 'fs_edit' || toolUse.name === 'fs_multi_edit') && toolUse.input?.path) {
1438
+ await this.filePool.recordEdit(toolUse.input.path);
1439
+ }
1440
+ if (toolUse.name === 'fs_rm' && toolUse.input?.path) {
1441
+ await this.filePool.recordDelete(toolUse.input.path);
1442
+ }
1443
+ const success = outcome.ok !== false;
1444
+ const duration = Date.now() - (this.toolRecords.get(record.id)?.startedAt ?? Date.now());
1445
+ if (success) {
1446
+ this.updateToolRecord(record.id, {
1447
+ state: 'COMPLETED',
1448
+ result: outcome.content,
1449
+ durationMs: duration,
1450
+ completedAt: Date.now(),
1451
+ }, 'execution complete');
1452
+ this.events.emitMonitor({ channel: 'monitor', type: 'tool_executed', call: this.snapshotToolRecord(record.id) });
1453
+ this.lastProcessingStart = Date.now();
1454
+ return this.makeToolResult(toolUse.id, { ok: true, data: outcome.content });
1455
+ }
1456
+ else {
1457
+ const errorContent = outcome.content;
1458
+ const errorMessage = errorContent?.error || 'Tool returned failure';
1459
+ const errorType = errorContent?._validationError ? 'validation' :
1460
+ errorContent?._thrownError ? 'runtime' : 'logical';
1461
+ const isRetryable = errorType !== 'validation';
1462
+ this.updateToolRecord(record.id, {
1463
+ state: 'FAILED',
1464
+ result: outcome.content,
1465
+ error: errorMessage,
1466
+ isError: true,
1467
+ durationMs: duration,
1468
+ completedAt: Date.now(),
1469
+ }, 'tool reported failure');
1470
+ this.events.emitProgress({
1471
+ channel: 'progress',
1472
+ type: 'tool:error',
1473
+ call: this.snapshotToolRecord(record.id),
1474
+ error: errorMessage,
1475
+ });
1476
+ this.events.emitMonitor({
1477
+ channel: 'monitor',
1478
+ type: 'error',
1479
+ severity: 'warn',
1480
+ phase: 'tool',
1481
+ message: errorMessage,
1482
+ detail: { ...outcome.content, errorType, retryable: isRetryable },
1483
+ });
1484
+ const recommendations = errorContent?.recommendations || this.getErrorRecommendations(errorType, toolUse.name);
1485
+ this.lastProcessingStart = Date.now();
1486
+ return this.makeToolResult(toolUse.id, {
1487
+ ok: false,
1488
+ error: errorMessage,
1489
+ errorType,
1490
+ retryable: isRetryable,
1491
+ data: outcome.content,
1492
+ recommendations,
1493
+ });
1494
+ }
1495
+ }
1496
+ catch (error) {
1497
+ const isAbort = error?.name === 'AbortError';
1498
+ const message = isAbort ? (this.toolAbortNotes.get(toolUse.id) || 'Tool execution aborted') : error?.message || String(error);
1499
+ const errorType = isAbort ? 'aborted' : 'exception';
1500
+ this.lastProcessingStart = Date.now();
1501
+ this.updateToolRecord(record.id, { state: 'FAILED', error: message, isError: true }, isAbort ? 'tool aborted' : 'execution failed');
1502
+ this.events.emitProgress({
1503
+ channel: 'progress',
1504
+ type: 'tool:error',
1505
+ call: this.snapshotToolRecord(record.id),
1506
+ error: message,
1507
+ });
1508
+ this.events.emitMonitor({
1509
+ channel: 'monitor',
1510
+ type: 'error',
1511
+ severity: isAbort ? 'warn' : 'error',
1512
+ phase: 'tool',
1513
+ message,
1514
+ detail: { errorType, stack: error?.stack },
1515
+ });
1516
+ const recommendations = isAbort
1517
+ ? ['检查是否手动中断', '根据需要重新触发工具', '考虑调整超时时间']
1518
+ : this.getErrorRecommendations('runtime', toolUse.name);
1519
+ return this.makeToolResult(toolUse.id, {
1520
+ ok: false,
1521
+ error: message,
1522
+ errorType,
1523
+ retryable: !isAbort,
1524
+ recommendations,
1525
+ });
1526
+ }
1527
+ finally {
1528
+ clearTimeout(timeoutId);
1529
+ this.toolControllers.delete(toolUse.id);
1530
+ this.toolAbortNotes.delete(toolUse.id);
1531
+ this.setBreakpoint('POST_TOOL');
1532
+ this.events.emitProgress({ channel: 'progress', type: 'tool:end', call: this.snapshotToolRecord(record.id) });
1533
+ this.lastProcessingStart = Date.now();
1534
+ }
1535
+ }
1536
+ registerToolRecord(toolUse) {
1537
+ const now = Date.now();
1538
+ const record = {
1539
+ id: toolUse.id,
1540
+ name: toolUse.name,
1541
+ input: toolUse.input,
1542
+ state: 'PENDING',
1543
+ approval: { required: false },
1544
+ createdAt: now,
1545
+ updatedAt: now,
1546
+ auditTrail: [{ state: 'PENDING', timestamp: now }],
1547
+ };
1548
+ this.toolRecords.set(record.id, record);
1549
+ return record;
1550
+ }
1551
+ updateToolRecord(id, update, auditNote) {
1552
+ const record = this.toolRecords.get(id);
1553
+ if (!record)
1554
+ return;
1555
+ const now = Date.now();
1556
+ if (update.state && update.state !== record.state) {
1557
+ record.auditTrail.push({ state: update.state, timestamp: now, note: auditNote });
1558
+ }
1559
+ else if (auditNote) {
1560
+ record.auditTrail.push({ state: record.state, timestamp: now, note: auditNote });
1561
+ }
1562
+ Object.assign(record, update, { updatedAt: now });
1563
+ }
1564
+ snapshotToolRecord(id) {
1565
+ const record = this.toolRecords.get(id);
1566
+ if (!record)
1567
+ throw new Error(`Tool record not found: ${id}`);
1568
+ return {
1569
+ id: record.id,
1570
+ name: record.name,
1571
+ state: record.state,
1572
+ approval: record.approval,
1573
+ result: record.result,
1574
+ error: record.error,
1575
+ isError: record.isError,
1576
+ durationMs: record.durationMs,
1577
+ startedAt: record.startedAt,
1578
+ completedAt: record.completedAt,
1579
+ inputPreview: this.preview(record.input),
1580
+ auditTrail: [...record.auditTrail],
1581
+ };
1582
+ }
1583
+ normalizeToolRecord(record) {
1584
+ const timestamp = record.updatedAt ?? record.createdAt ?? Date.now();
1585
+ const auditTrail = record.auditTrail && record.auditTrail.length > 0
1586
+ ? record.auditTrail.map((entry) => ({ ...entry }))
1587
+ : [{ state: record.state, timestamp }];
1588
+ return { ...record, auditTrail };
1589
+ }
1590
+ preview(value, limit = 200) {
1591
+ const text = typeof value === 'string' ? value : JSON.stringify(value);
1592
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
1593
+ }
1594
+ async sanitizeOrphanToolResults(note = 'Sanitized orphan tool_result blocks (missing tool_use).') {
1595
+ const toolUseIds = new Set();
1596
+ for (const msg of this.messages || []) {
1597
+ for (const block of msg?.content || []) {
1598
+ if (block && block.type === 'tool_use') {
1599
+ const id = String(block.id ?? '').trim();
1600
+ if (id)
1601
+ toolUseIds.add(id);
1602
+ }
1603
+ }
1604
+ }
1605
+ let converted = 0;
1606
+ let changedAny = false;
1607
+ const nextMessages = (this.messages || []).map((msg) => {
1608
+ if (!msg || !Array.isArray(msg.content))
1609
+ return msg;
1610
+ let changed = false;
1611
+ const nextBlocks = [];
1612
+ for (const block of msg.content) {
1613
+ if (block && block.type === 'tool_result') {
1614
+ const toolUseId = String(block.tool_use_id ?? '').trim();
1615
+ if (!toolUseId || !toolUseIds.has(toolUseId)) {
1616
+ changed = true;
1617
+ changedAny = true;
1618
+ converted += 1;
1619
+ const isErr = Boolean(block.is_error);
1620
+ const payload = this.preview(block.content, 1400);
1621
+ nextBlocks.push({
1622
+ type: 'text',
1623
+ text: `[orphaned tool_result] tool_use_id=${toolUseId || 'unknown'}${isErr ? ' (error)' : ''}\n${payload}`,
1624
+ });
1625
+ continue;
1626
+ }
1627
+ }
1628
+ nextBlocks.push(block);
1629
+ }
1630
+ return changed ? { ...msg, content: nextBlocks } : msg;
1631
+ });
1632
+ if (changedAny) {
1633
+ this.messages = nextMessages;
1634
+ await this.persistMessages();
1635
+ this.events.emitMonitor({
1636
+ channel: 'monitor',
1637
+ type: 'context_repair',
1638
+ reason: 'orphan_tool_result',
1639
+ converted,
1640
+ note,
1641
+ });
1642
+ }
1643
+ return { converted };
1644
+ }
1645
+ async requestPermission(id, _toolName, _args, meta) {
1646
+ const approval = {
1647
+ required: true,
1648
+ decision: undefined,
1649
+ decidedAt: undefined,
1650
+ decidedBy: undefined,
1651
+ note: undefined,
1652
+ meta,
1653
+ };
1654
+ this.updateToolRecord(id, { state: 'APPROVAL_REQUIRED', approval }, 'awaiting approval');
1655
+ return new Promise((resolve) => {
1656
+ this.pendingPermissions.set(id, {
1657
+ resolve: (decision, note) => {
1658
+ this.updateToolRecord(id, {
1659
+ approval: buildApproval(decision, 'api', note),
1660
+ state: decision === 'allow' ? 'APPROVED' : 'DENIED',
1661
+ error: decision === 'deny' ? note : undefined,
1662
+ isError: decision === 'deny',
1663
+ }, decision === 'allow' ? 'approval granted' : 'approval denied');
1664
+ if (decision === 'allow') {
1665
+ this.setBreakpoint('PRE_TOOL');
1666
+ }
1667
+ else {
1668
+ this.setBreakpoint('POST_TOOL');
1669
+ }
1670
+ resolve(decision);
1671
+ },
1672
+ });
1673
+ this.events.emitControl({
1674
+ channel: 'control',
1675
+ type: 'permission_required',
1676
+ call: this.snapshotToolRecord(id),
1677
+ respond: async (decision, opts) => {
1678
+ await this.decide(id, decision, opts?.note);
1679
+ },
1680
+ });
1681
+ this.setState('PAUSED');
1682
+ this.setBreakpoint('AWAITING_APPROVAL');
1683
+ });
1684
+ }
1685
+ findLastSfp() {
1686
+ for (let i = this.messages.length - 1; i >= 0; i--) {
1687
+ const msg = this.messages[i];
1688
+ if (msg.role === 'user')
1689
+ return i;
1690
+ if (msg.role === 'assistant' && !msg.content.some((block) => block.type === 'tool_use'))
1691
+ return i;
1692
+ }
1693
+ return -1;
1694
+ }
1695
+ async appendSyntheticToolResults(note) {
1696
+ const last = this.messages[this.messages.length - 1];
1697
+ if (!last || last.role !== 'assistant')
1698
+ return;
1699
+ const toolUses = last.content.filter((block) => block.type === 'tool_use');
1700
+ if (!toolUses.length)
1701
+ return;
1702
+ const resultIds = new Set();
1703
+ for (const message of this.messages) {
1704
+ for (const block of message.content) {
1705
+ if (block.type === 'tool_result')
1706
+ resultIds.add(block.tool_use_id);
1707
+ }
1708
+ }
1709
+ const synthetic = [];
1710
+ for (const tu of toolUses) {
1711
+ if (!resultIds.has(tu.id)) {
1712
+ const sealedResult = this.buildSealPayload('TOOL_RESULT_MISSING', tu.id, note);
1713
+ this.updateToolRecord(tu.id, { state: 'SEALED', error: sealedResult.message, isError: true }, 'sealed due to interrupt');
1714
+ synthetic.push(this.makeToolResult(tu.id, sealedResult.payload));
1715
+ }
1716
+ }
1717
+ if (synthetic.length) {
1718
+ this.messages.push({ role: 'user', content: synthetic });
1719
+ await this.persistMessages();
1720
+ await this.persistToolRecords();
1721
+ }
1722
+ }
1723
+ async autoSealIncompleteCalls(note = 'Sealed due to crash while executing; verify potential side effects.') {
1724
+ const sealedSnapshots = [];
1725
+ const resultIds = new Set();
1726
+ for (const message of this.messages) {
1727
+ for (const block of message.content) {
1728
+ if (block.type === 'tool_result') {
1729
+ resultIds.add(block.tool_use_id);
1730
+ }
1731
+ }
1732
+ }
1733
+ const synthetic = [];
1734
+ for (const [id, record] of this.toolRecords) {
1735
+ if (['COMPLETED', 'FAILED', 'DENIED', 'SEALED'].includes(record.state))
1736
+ continue;
1737
+ const sealedResult = this.buildSealPayload(record.state, id, note, record);
1738
+ this.updateToolRecord(id, { state: 'SEALED', error: sealedResult.message, isError: true, completedAt: Date.now() }, 'auto seal');
1739
+ const snapshot = this.snapshotToolRecord(id);
1740
+ sealedSnapshots.push(snapshot);
1741
+ if (!resultIds.has(id)) {
1742
+ synthetic.push(this.makeToolResult(id, sealedResult.payload));
1743
+ }
1744
+ }
1745
+ if (synthetic.length > 0) {
1746
+ this.messages.push({ role: 'user', content: synthetic });
1747
+ await this.persistMessages();
1748
+ }
1749
+ await this.persistToolRecords();
1750
+ return sealedSnapshots;
1751
+ }
1752
+ findNextAssistantIndex(fromIndex) {
1753
+ for (let i = fromIndex; i < this.messages.length; i++) {
1754
+ if (this.messages[i]?.role === 'assistant')
1755
+ return i;
1756
+ }
1757
+ return -1;
1758
+ }
1759
+ ensureSealedToolRecordForDanglingUse(toolUse, note) {
1760
+ const existing = this.toolRecords.get(toolUse.id);
1761
+ const now = Date.now();
1762
+ // If we have a completed/failed record, reconstruct a reasonable tool_result from it.
1763
+ if (existing) {
1764
+ if (existing.state === 'COMPLETED') {
1765
+ return {
1766
+ snapshot: this.snapshotToolRecord(existing.id),
1767
+ toolResult: this.makeToolResult(existing.id, {
1768
+ ok: true,
1769
+ data: existing.result,
1770
+ note: note || 'Recovered tool result after crash/resume.',
1771
+ }),
1772
+ };
1773
+ }
1774
+ if (existing.state === 'FAILED' || existing.state === 'DENIED') {
1775
+ return {
1776
+ snapshot: this.snapshotToolRecord(existing.id),
1777
+ toolResult: this.makeToolResult(existing.id, {
1778
+ ok: false,
1779
+ data: existing.result,
1780
+ error: existing.error || 'Tool failed (recovered after crash/resume).',
1781
+ errorType: existing.state === 'DENIED' ? 'denied' : 'runtime',
1782
+ retryable: existing.state !== 'DENIED',
1783
+ note: note || 'Recovered tool error after crash/resume.',
1784
+ recommendations: existing.state === 'DENIED' ? ['检查权限/审批策略', '如需继续,请重新触发工具并完成审批'] : ['检查工具副作用后重试'],
1785
+ }),
1786
+ };
1787
+ }
1788
+ // Otherwise seal it.
1789
+ const sealedResult = this.buildSealPayload(existing.state, existing.id, note, existing);
1790
+ this.updateToolRecord(existing.id, { state: 'SEALED', error: sealedResult.message, isError: true, completedAt: now }, 'auto seal dangling tool_use');
1791
+ return { snapshot: this.snapshotToolRecord(existing.id), toolResult: this.makeToolResult(existing.id, sealedResult.payload) };
1792
+ }
1793
+ // Missing tool record entirely (can happen if the process crashes between persisting assistant tool_use and registering tool record).
1794
+ const sealedResult = this.buildSealPayload('TOOL_RESULT_MISSING', toolUse.id, note);
1795
+ const record = {
1796
+ id: toolUse.id,
1797
+ name: toolUse.name,
1798
+ input: toolUse.input,
1799
+ state: 'SEALED',
1800
+ approval: { required: false },
1801
+ result: sealedResult.payload,
1802
+ error: sealedResult.message,
1803
+ isError: true,
1804
+ startedAt: undefined,
1805
+ completedAt: now,
1806
+ durationMs: undefined,
1807
+ createdAt: now,
1808
+ updatedAt: now,
1809
+ auditTrail: [{ state: 'SEALED', timestamp: now, note: 'auto seal dangling tool_use (missing tool record)' }],
1810
+ };
1811
+ this.toolRecords.set(record.id, record);
1812
+ return { snapshot: this.snapshotToolRecord(record.id), toolResult: this.makeToolResult(record.id, sealedResult.payload) };
1813
+ }
1814
+ async autoSealDanglingToolUses(note = 'Sealed missing tool_result to satisfy tool_use/tool_result ordering.') {
1815
+ const sealedSnapshots = [];
1816
+ // Track tool_result ids that already exist anywhere (best-effort).
1817
+ const globalResultIds = new Set();
1818
+ for (const message of this.messages) {
1819
+ for (const block of message.content || []) {
1820
+ if (block && block.type === 'tool_result')
1821
+ globalResultIds.add(String(block.tool_use_id));
1822
+ }
1823
+ }
1824
+ // Insert synthetic tool_result blocks *immediately after* the assistant message that emitted tool_use,
1825
+ // and *before* the next assistant message, to satisfy strict provider validation.
1826
+ const insertions = [];
1827
+ const alreadyInserted = new Set();
1828
+ for (let i = 0; i < this.messages.length; i++) {
1829
+ const msg = this.messages[i];
1830
+ if (!msg || msg.role !== 'assistant')
1831
+ continue;
1832
+ const toolUses = (msg.content || []).filter((b) => b && b.type === 'tool_use');
1833
+ if (!toolUses.length)
1834
+ continue;
1835
+ const nextAssistantIndex = this.findNextAssistantIndex(i + 1);
1836
+ const end = nextAssistantIndex >= 0 ? nextAssistantIndex : this.messages.length;
1837
+ const localResultIds = new Set();
1838
+ for (let j = i + 1; j < end; j++) {
1839
+ const m2 = this.messages[j];
1840
+ for (const block of m2?.content || []) {
1841
+ if (block && block.type === 'tool_result')
1842
+ localResultIds.add(String(block.tool_use_id));
1843
+ }
1844
+ }
1845
+ const blocks = [];
1846
+ for (const tu of toolUses) {
1847
+ const id = typeof tu?.id === 'string' ? tu.id : '';
1848
+ const name = typeof tu?.name === 'string' ? tu.name : 'tool';
1849
+ const input = tu?.input;
1850
+ if (!id)
1851
+ continue;
1852
+ if (localResultIds.has(id))
1853
+ continue;
1854
+ if (globalResultIds.has(id))
1855
+ continue; // avoid duplicate tool_result in weird histories
1856
+ if (alreadyInserted.has(id))
1857
+ continue;
1858
+ const { snapshot, toolResult } = this.ensureSealedToolRecordForDanglingUse({ id, name, input }, note);
1859
+ if (snapshot)
1860
+ sealedSnapshots.push(snapshot);
1861
+ blocks.push(toolResult);
1862
+ alreadyInserted.add(id);
1863
+ globalResultIds.add(id);
1864
+ }
1865
+ if (blocks.length) {
1866
+ insertions.push({ index: i + 1, blocks });
1867
+ }
1868
+ }
1869
+ if (!insertions.length)
1870
+ return [];
1871
+ // Apply from back to front so indices remain stable.
1872
+ for (let k = insertions.length - 1; k >= 0; k--) {
1873
+ const ins = insertions[k];
1874
+ this.messages.splice(ins.index, 0, { role: 'user', content: ins.blocks });
1875
+ }
1876
+ await this.persistMessages();
1877
+ await this.persistToolRecords();
1878
+ return sealedSnapshots;
1879
+ }
1880
+ recoverFromStaleAwaitingApprovalAfterCrash() {
1881
+ // Crash-resume seals incomplete calls, but the persisted breakpoint/state might still say we're waiting for approval.
1882
+ // If there is no in-memory pending permission AND no tool record still waiting approval, unpause so processing can continue.
1883
+ const bp = this.breakpoints.getCurrent();
1884
+ if (bp !== 'AWAITING_APPROVAL')
1885
+ return false;
1886
+ if (this.pendingPermissions.size > 0)
1887
+ return false;
1888
+ const hasApprovalRequiredRecord = Array.from(this.toolRecords.values()).some((r) => {
1889
+ if (!r)
1890
+ return false;
1891
+ if (r.state !== 'APPROVAL_REQUIRED')
1892
+ return false;
1893
+ const approval = r.approval;
1894
+ return Boolean(approval?.required) && !approval?.decision;
1895
+ });
1896
+ if (hasApprovalRequiredRecord)
1897
+ return false;
1898
+ this.setState('READY');
1899
+ this.setBreakpoint('READY', 'recovered stale AWAITING_APPROVAL after crash-resume');
1900
+ this.events.emitMonitor({
1901
+ channel: 'monitor',
1902
+ type: 'agent_recovered',
1903
+ reason: 'stale_awaiting_approval',
1904
+ detail: { breakpoint: bp },
1905
+ });
1906
+ return true;
1907
+ }
1908
+ validateToolArgs(tool, args) {
1909
+ if (!tool.input_schema) {
1910
+ return { ok: true };
1911
+ }
1912
+ const key = JSON.stringify(tool.input_schema);
1913
+ let validator = this.validatorCache.get(key);
1914
+ if (!validator) {
1915
+ validator = this.ajv.compile(tool.input_schema);
1916
+ this.validatorCache.set(key, validator);
1917
+ }
1918
+ const valid = validator(args);
1919
+ if (!valid) {
1920
+ return {
1921
+ ok: false,
1922
+ error: this.ajv.errorsText(validator.errors, { separator: '\n' }),
1923
+ };
1924
+ }
1925
+ return { ok: true };
1926
+ }
1927
+ makeToolResult(toolUseId, payload) {
1928
+ return {
1929
+ type: 'tool_result',
1930
+ tool_use_id: toolUseId,
1931
+ content: {
1932
+ ok: payload.ok,
1933
+ data: payload.data,
1934
+ error: payload.error,
1935
+ errorType: payload.errorType,
1936
+ retryable: payload.retryable,
1937
+ note: payload.note,
1938
+ recommendations: payload.recommendations,
1939
+ },
1940
+ is_error: payload.ok ? false : true,
1941
+ };
1942
+ }
1943
+ buildSealPayload(state, toolUseId, fallbackNote, record) {
1944
+ const baseMessage = (() => {
1945
+ switch (state) {
1946
+ case 'APPROVAL_REQUIRED':
1947
+ return '工具在等待审批时会话中断,系统已自动封口。';
1948
+ case 'APPROVED':
1949
+ return '工具已通过审批但尚未执行,系统已自动封口。';
1950
+ case 'EXECUTING':
1951
+ return '工具执行过程中会话中断,系统已自动封口。';
1952
+ case 'PENDING':
1953
+ return '工具刚准备执行时会话中断,系统已自动封口。';
1954
+ default:
1955
+ return fallbackNote;
1956
+ }
1957
+ })();
1958
+ const recommendations = (() => {
1959
+ switch (state) {
1960
+ case 'APPROVAL_REQUIRED':
1961
+ return ['确认审批是否仍然需要', '如需继续,请重新触发工具并完成审批'];
1962
+ case 'APPROVED':
1963
+ return ['确认工具输入是否仍然有效', '如需执行,请重新触发工具'];
1964
+ case 'EXECUTING':
1965
+ return ['检查工具可能产生的副作用', '确认外部系统状态后再重试'];
1966
+ case 'PENDING':
1967
+ return ['确认工具参数是否正确', '再次触发工具以继续流程'];
1968
+ default:
1969
+ return ['检查封口说明并决定是否重试工具'];
1970
+ }
1971
+ })();
1972
+ const detail = {
1973
+ status: state,
1974
+ startedAt: record?.startedAt,
1975
+ approval: record?.approval,
1976
+ toolId: toolUseId,
1977
+ note: baseMessage,
1978
+ };
1979
+ return {
1980
+ payload: {
1981
+ ok: false,
1982
+ error: baseMessage,
1983
+ data: detail,
1984
+ recommendations,
1985
+ },
1986
+ message: baseMessage,
1987
+ };
1988
+ }
1989
+ wrapReminder(content, options) {
1990
+ if (options?.skipStandardEnding)
1991
+ return content;
1992
+ return [
1993
+ '<system-reminder>',
1994
+ content,
1995
+ '',
1996
+ 'This is a system reminder. DO NOT respond to this message directly.',
1997
+ 'DO NOT mention this reminder to the user.',
1998
+ 'Continue with your current task.',
1999
+ '</system-reminder>',
2000
+ ].join('\n');
2001
+ }
2002
+ getToolSchemas() {
2003
+ return Array.from(this.tools.values()).map((tool) => ({
2004
+ name: tool.name,
2005
+ description: tool.description,
2006
+ input_schema: tool.input_schema,
2007
+ }));
2008
+ }
2009
+ setState(state) {
2010
+ if (this.state === state)
2011
+ return;
2012
+ this.state = state;
2013
+ this.events.emitMonitor({ channel: 'monitor', type: 'state_changed', state });
2014
+ }
2015
+ async persistMessages() {
2016
+ try {
2017
+ await this.persistentStore.saveMessages(this.agentId, this.messages);
2018
+ await this.persistInfo();
2019
+ const snapshot = {
2020
+ agentId: this.agentId,
2021
+ messages: this.messages.map((message) => ({
2022
+ role: message.role,
2023
+ content: message.content.map((block) => ({ ...block })),
2024
+ })),
2025
+ lastBookmark: this.lastBookmark,
2026
+ };
2027
+ await this.hooks.runMessagesChanged(snapshot);
2028
+ }
2029
+ catch (error) {
2030
+ const message = error?.message || String(error);
2031
+ this.events.emitMonitor({
2032
+ channel: 'monitor',
2033
+ type: 'tool_custom_event',
2034
+ toolName: 'store',
2035
+ eventType: 'store_write_error',
2036
+ data: {
2037
+ agentId: this.agentId,
2038
+ resource: 'messages',
2039
+ error: message,
2040
+ },
2041
+ timestamp: Date.now(),
2042
+ });
2043
+ this.events.emitMonitor({
2044
+ channel: 'monitor',
2045
+ type: 'error',
2046
+ severity: 'error',
2047
+ phase: 'lifecycle',
2048
+ message: 'Failed to persist messages',
2049
+ detail: { error: message, stack: error?.stack },
2050
+ });
2051
+ throw error;
2052
+ }
2053
+ }
2054
+ async persistToolRecords() {
2055
+ try {
2056
+ await this.persistentStore.saveToolCallRecords(this.agentId, Array.from(this.toolRecords.values()));
2057
+ }
2058
+ catch (error) {
2059
+ const message = error?.message || String(error);
2060
+ this.events.emitMonitor({
2061
+ channel: 'monitor',
2062
+ type: 'tool_custom_event',
2063
+ toolName: 'store',
2064
+ eventType: 'store_write_error',
2065
+ data: {
2066
+ agentId: this.agentId,
2067
+ resource: 'tool-calls',
2068
+ error: message,
2069
+ },
2070
+ timestamp: Date.now(),
2071
+ });
2072
+ this.events.emitMonitor({
2073
+ channel: 'monitor',
2074
+ type: 'error',
2075
+ severity: 'error',
2076
+ phase: 'lifecycle',
2077
+ message: 'Failed to persist tool call records',
2078
+ detail: { error: message, stack: error?.stack },
2079
+ });
2080
+ throw error;
2081
+ }
2082
+ }
2083
+ async persistInfo() {
2084
+ const metadata = {
2085
+ agentId: this.agentId,
2086
+ templateId: this.template.id,
2087
+ templateVersion: this.config.templateVersion || this.template.version,
2088
+ sandboxConfig: this.sandboxConfig,
2089
+ modelConfig: this.model.toConfig(),
2090
+ tools: this.toolDescriptors,
2091
+ exposeThinking: this.exposeThinking,
2092
+ permission: this.permission,
2093
+ todo: this.todoConfig,
2094
+ subagents: this.subagents,
2095
+ context: this.config.context,
2096
+ skills: this.skillsConfig,
2097
+ createdAt: this.createdAt,
2098
+ updatedAt: new Date().toISOString(),
2099
+ configVersion: CONFIG_VERSION,
2100
+ metadata: this.config.metadata,
2101
+ lineage: this.lineage,
2102
+ breakpoint: this.breakpoints.getCurrent(),
2103
+ };
2104
+ const info = {
2105
+ agentId: this.agentId,
2106
+ templateId: this.template.id,
2107
+ createdAt: this.createdAt,
2108
+ lineage: metadata.lineage || [],
2109
+ configVersion: CONFIG_VERSION,
2110
+ messageCount: this.messages.length,
2111
+ lastSfpIndex: this.lastSfpIndex,
2112
+ lastBookmark: this.lastBookmark,
2113
+ };
2114
+ info.metadata = metadata;
2115
+ await this.persistentStore.saveInfo(this.agentId, info);
2116
+ }
2117
+ registerTodoTools() {
2118
+ const read = todo_read_1.TodoRead;
2119
+ const write = todo_write_1.TodoWrite;
2120
+ this.tools.set(read.name, read);
2121
+ this.tools.set(write.name, write);
2122
+ const descriptorNames = new Set(this.toolDescriptors.map((d) => d.name));
2123
+ if (!descriptorNames.has(read.name)) {
2124
+ const descriptor = read.toDescriptor();
2125
+ this.toolDescriptors.push(descriptor);
2126
+ this.toolDescriptorIndex.set(descriptor.name, descriptor);
2127
+ }
2128
+ if (!descriptorNames.has(write.name)) {
2129
+ const descriptor = write.toDescriptor();
2130
+ this.toolDescriptors.push(descriptor);
2131
+ this.toolDescriptorIndex.set(descriptor.name, descriptor);
2132
+ }
2133
+ }
2134
+ // ========== 工具说明书自动注入 ==========
2135
+ /**
2136
+ * 收集所有工具的使用说明
2137
+ */
2138
+ collectToolPrompts() {
2139
+ const prompts = [];
2140
+ for (const tool of this.tools.values()) {
2141
+ if (tool.prompt) {
2142
+ const promptText = typeof tool.prompt === 'string' ? tool.prompt : undefined;
2143
+ if (promptText) {
2144
+ prompts.push({
2145
+ name: tool.name,
2146
+ prompt: promptText,
2147
+ });
2148
+ }
2149
+ }
2150
+ }
2151
+ return prompts;
2152
+ }
2153
+ /**
2154
+ * 渲染工具手册
2155
+ */
2156
+ renderManual(prompts) {
2157
+ if (prompts.length === 0)
2158
+ return '';
2159
+ const sections = prompts.map(({ name, prompt }) => {
2160
+ return `**${name}**\n${prompt}`;
2161
+ });
2162
+ return `\n\n### Tools Manual\n\nThe following tools are available for your use. Please read their usage guidance carefully:\n\n${sections.join('\n\n')}`;
2163
+ }
2164
+ /**
2165
+ * 刷新工具手册(运行时工具变更时调用)
2166
+ */
2167
+ refreshToolManual() {
2168
+ // 移除旧的 Tools Manual 部分
2169
+ const manualPattern = /\n\n### Tools Manual\n\n[\s\S]*$/;
2170
+ if (this.template.systemPrompt) {
2171
+ this.template.systemPrompt = this.template.systemPrompt.replace(manualPattern, '');
2172
+ }
2173
+ // 重新注入
2174
+ this.injectManualIntoSystemPrompt();
2175
+ }
2176
+ /**
2177
+ * 根据错误类型生成建议
2178
+ */
2179
+ getErrorRecommendations(errorType, toolName) {
2180
+ switch (errorType) {
2181
+ case 'validation':
2182
+ return [
2183
+ '检查工具参数是否符合schema要求',
2184
+ '确认所有必填参数已提供',
2185
+ '检查参数类型是否正确',
2186
+ '参考工具手册中的参数说明'
2187
+ ];
2188
+ case 'runtime':
2189
+ return [
2190
+ '检查系统资源是否可用',
2191
+ '确认文件/路径是否存在且有权限',
2192
+ '考虑添加错误处理逻辑',
2193
+ '可以重试该操作'
2194
+ ];
2195
+ case 'logical':
2196
+ if (toolName.startsWith('fs_')) {
2197
+ return [
2198
+ '确认文件内容是否符合预期',
2199
+ '检查文件是否被外部修改',
2200
+ '验证路径和模式是否正确',
2201
+ '可以先用 fs_read 确认文件状态'
2202
+ ];
2203
+ }
2204
+ else if (toolName.startsWith('bash_')) {
2205
+ return [
2206
+ '检查命令语法是否正确',
2207
+ '确认命令在沙箱环境中可执行',
2208
+ '查看stderr输出了解详细错误',
2209
+ '考虑调整超时时间或拆分命令'
2210
+ ];
2211
+ }
2212
+ else {
2213
+ return [
2214
+ '检查工具逻辑是否符合预期',
2215
+ '验证输入数据的完整性',
2216
+ '考虑重试或使用替代方案',
2217
+ '查看错误详情调整策略'
2218
+ ];
2219
+ }
2220
+ default:
2221
+ return [
2222
+ '查看错误信息调整输入',
2223
+ '考虑使用替代工具',
2224
+ '必要时寻求人工协助'
2225
+ ];
2226
+ }
2227
+ }
2228
+ /**
2229
+ * 将工具手册注入到系统提示中
2230
+ */
2231
+ injectManualIntoSystemPrompt() {
2232
+ const prompts = this.collectToolPrompts();
2233
+ if (prompts.length === 0)
2234
+ return;
2235
+ const manual = this.renderManual(prompts);
2236
+ // 追加到模板的 systemPrompt
2237
+ if (this.template.systemPrompt) {
2238
+ this.template.systemPrompt += manual;
2239
+ }
2240
+ else {
2241
+ this.template.systemPrompt = manual;
2242
+ }
2243
+ // 发出 Monitor 事件
2244
+ this.events.emitMonitor({
2245
+ channel: 'monitor',
2246
+ type: 'tool_manual_updated',
2247
+ tools: prompts.map((p) => p.name),
2248
+ timestamp: Date.now(),
2249
+ });
2250
+ }
2251
+ enqueueMessage(message, kind) {
2252
+ this.messages.push(message);
2253
+ if (kind === 'user') {
2254
+ this.lastSfpIndex = this.messages.length - 1;
2255
+ this.stepCount++;
2256
+ // When the user provides new guidance, give the model a fresh chance.
2257
+ // Otherwise a previous invalid tool-call streak can keep suppressing auto-continue.
2258
+ this.invalidToolArgsStreak = 0;
2259
+ this.invalidToolArgsLastTool = '';
2260
+ this.suppressAutoContinue = false;
2261
+ this.suppressAutoContinueReason = undefined;
2262
+ }
2263
+ }
2264
+ handleExternalFileChange(path, mtime) {
2265
+ const relPath = this.relativePath(path);
2266
+ this.events.emitMonitor({ channel: 'monitor', type: 'file_changed', path: relPath, mtime });
2267
+ const reminder = `检测到外部修改:${relPath}。请重新使用 fs_read 确认文件内容,并在必要时向用户同步。`;
2268
+ this.remind(reminder, { category: 'file', priority: 'medium' });
2269
+ }
2270
+ relativePath(absPath) {
2271
+ const path = require('path');
2272
+ return path.relative(this.sandbox.workDir || process.cwd(), this.sandbox.fs.resolve(absPath));
2273
+ }
2274
+ static generateAgentId() {
2275
+ const chars = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
2276
+ const now = Date.now();
2277
+ const timePart = encodeUlid(now, 10, chars);
2278
+ const random = Array.from({ length: 16 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
2279
+ return `agt:${timePart}${random}`;
2280
+ }
2281
+ }
2282
+ exports.Agent = Agent;
2283
+ function ensureModelFactory(factory) {
2284
+ if (factory)
2285
+ return factory;
2286
+ return (config) => {
2287
+ if (config.provider === 'anthropic') {
2288
+ if (!config.apiKey) {
2289
+ throw new Error('Anthropic provider requires apiKey');
2290
+ }
2291
+ return new provider_1.AnthropicProvider(config.apiKey, config.model, config.baseUrl, { retry: config.retry });
2292
+ }
2293
+ throw new Error(`Model factory not provided for provider: ${config.provider}`);
2294
+ };
2295
+ }
2296
+ function resolveTools(config, template, registry, templateRegistry) {
2297
+ const requested = config.tools ?? (template.tools === '*' ? registry.list() : template.tools || []);
2298
+ const instances = [];
2299
+ const descriptors = [];
2300
+ for (const id of requested) {
2301
+ const creationConfig = buildToolConfig(id, template, templateRegistry);
2302
+ const tool = registry.create(id, creationConfig);
2303
+ instances.push(tool);
2304
+ descriptors.push(tool.toDescriptor());
2305
+ }
2306
+ return { instances, descriptors };
2307
+ }
2308
+ function buildToolConfig(id, template, templateRegistry) {
2309
+ if (id === 'task_run') {
2310
+ const allowed = template.runtime?.subagents?.templates;
2311
+ const templates = allowed && allowed.length > 0 ? allowed.map((tplId) => templateRegistry.get(tplId)) : templateRegistry.list();
2312
+ return { templates };
2313
+ }
2314
+ return undefined;
2315
+ }
2316
+ function encodeUlid(time, length, chars) {
2317
+ let remaining = time;
2318
+ const encoded = Array(length);
2319
+ for (let i = length - 1; i >= 0; i--) {
2320
+ const mod = remaining % 32;
2321
+ encoded[i] = chars.charAt(mod);
2322
+ remaining = Math.floor(remaining / 32);
2323
+ }
2324
+ return encoded.join('');
2325
+ }
2326
+ function buildApproval(decision, by, note) {
2327
+ return {
2328
+ required: true,
2329
+ decision,
2330
+ decidedBy: by,
2331
+ decidedAt: Date.now(),
2332
+ note,
2333
+ };
2334
+ }