@kkelly-offical/kkcode 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/session/loop.mjs
CHANGED
|
@@ -1,905 +1,1005 @@
|
|
|
1
|
-
import { newId } from "../core/types.mjs"
|
|
2
|
-
import { EventBus } from "../core/events.mjs"
|
|
3
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
|
-
import { requestProviderStream, countTokensProvider } from "../provider/router.mjs"
|
|
5
|
-
import { ToolRegistry } from "../tool/registry.mjs"
|
|
6
|
-
import { executeTool } from "../tool/executor.mjs"
|
|
7
|
-
import { PermissionEngine } from "../permission/engine.mjs"
|
|
8
|
-
import { createTaskDelegate } from "../orchestration/task-scheduler.mjs"
|
|
9
|
-
import { loadInstructions } from "./instruction-loader.mjs"
|
|
10
|
-
import { buildSystemPromptBlocks } from "./system-prompt.mjs"
|
|
11
|
-
import { detectProjectContext } from "./project-context.mjs"
|
|
12
|
-
import { renderRulesPrompt } from "../rules/load-rules.mjs"
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
let
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
configState,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
for (const call of response.toolCalls) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1
|
+
import { newId } from "../core/types.mjs"
|
|
2
|
+
import { EventBus } from "../core/events.mjs"
|
|
3
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
4
|
+
import { requestProviderStream, countTokensProvider } from "../provider/router.mjs"
|
|
5
|
+
import { ToolRegistry } from "../tool/registry.mjs"
|
|
6
|
+
import { executeTool } from "../tool/executor.mjs"
|
|
7
|
+
import { PermissionEngine } from "../permission/engine.mjs"
|
|
8
|
+
import { createTaskDelegate } from "../orchestration/task-scheduler.mjs"
|
|
9
|
+
import { loadInstructions } from "./instruction-loader.mjs"
|
|
10
|
+
import { buildSystemPromptBlocks } from "./system-prompt.mjs"
|
|
11
|
+
import { detectProjectContext } from "./project-context.mjs"
|
|
12
|
+
import { renderRulesPrompt } from "../rules/load-rules.mjs"
|
|
13
|
+
import { loadProfile } from "../onboarding.mjs"
|
|
14
|
+
import { SkillRegistry } from "../skill/registry.mjs"
|
|
15
|
+
import {
|
|
16
|
+
touchSession,
|
|
17
|
+
appendMessage,
|
|
18
|
+
appendPart,
|
|
19
|
+
getConversationHistory,
|
|
20
|
+
markSessionStatus,
|
|
21
|
+
updateSession
|
|
22
|
+
} from "./store.mjs"
|
|
23
|
+
import { pendingRejections, markRejectionsConsumed } from "../review/rejection-queue.mjs"
|
|
24
|
+
import { isRecoveryEnabled, markTurnFinished, markTurnInProgress } from "./recovery.mjs"
|
|
25
|
+
import { HookBus, initHookBus } from "../plugin/hook-bus.mjs"
|
|
26
|
+
import { shouldCompact, compactSession, estimateTokenCount, modelContextLimit, contextUtilization, supportsNativeCompaction } from "./compaction.mjs"
|
|
27
|
+
import { createStreamRenderer } from "../theme/markdown.mjs"
|
|
28
|
+
import { paint } from "../theme/color.mjs"
|
|
29
|
+
import { saveCheckpoint } from "./checkpoint.mjs"
|
|
30
|
+
import { askPlanApproval } from "../tool/question-prompt.mjs"
|
|
31
|
+
import { createValidator } from "./task-validator.mjs"
|
|
32
|
+
|
|
33
|
+
// Max chars kept in active context per tool_result — process output beyond this is truncated
|
|
34
|
+
const TOOL_RESULT_ACTIVE_LIMIT = 3000
|
|
35
|
+
|
|
36
|
+
const READ_ONLY_TOOLS = new Set([
|
|
37
|
+
"read", "glob", "grep", "list", "webfetch", "websearch", "codesearch", "background_output", "todowrite", "enter_plan"
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
function addUsage(target, delta) {
|
|
41
|
+
target.input += delta.input || 0
|
|
42
|
+
target.output += delta.output || 0
|
|
43
|
+
target.cacheRead += delta.cacheRead || 0
|
|
44
|
+
target.cacheWrite += delta.cacheWrite || 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async function buildSystemPrompt({ mode, model, cwd, agent = null, tools = [], skills = [], language = "en" }) {
|
|
49
|
+
// Assemble user instructions + rules (Layer 6)
|
|
50
|
+
const instructions = await loadInstructions(cwd)
|
|
51
|
+
const rules = await renderRulesPrompt(cwd)
|
|
52
|
+
|
|
53
|
+
// Inject user profile as a context block
|
|
54
|
+
const profile = await loadProfile()
|
|
55
|
+
let profileBlock = ""
|
|
56
|
+
if (profile && !profile.beginner) {
|
|
57
|
+
const lines = ["# User Profile", "", "Apply these preferences consistently in all code you write and suggestions you make:"]
|
|
58
|
+
if (profile.languages?.length) {
|
|
59
|
+
lines.push(`- Languages: ${profile.languages.join(", ")} — prefer these when suggesting solutions or writing code`)
|
|
60
|
+
}
|
|
61
|
+
if (profile.tech_stack?.length) {
|
|
62
|
+
lines.push(`- Tech stack: ${profile.tech_stack.join(", ")} — use these frameworks/tools when relevant`)
|
|
63
|
+
}
|
|
64
|
+
if (profile.design_style) {
|
|
65
|
+
lines.push(`- Code style: ${profile.design_style}`)
|
|
66
|
+
const s = profile.design_style.toLowerCase()
|
|
67
|
+
if (s.includes("minimal") || s.startsWith("clean")) {
|
|
68
|
+
lines.push(" → Write minimal code. Avoid over-engineering, unnecessary abstractions, and verbose implementations. Prefer simple, direct solutions.")
|
|
69
|
+
} else if (s.startsWith("functional") || s.includes("pure function")) {
|
|
70
|
+
lines.push(" → Prefer pure functions and immutability. Use map/filter/reduce over loops. Avoid side effects and mutable state where possible.")
|
|
71
|
+
} else if (s.startsWith("object-oriented") || s.includes("class")) {
|
|
72
|
+
lines.push(" → Use OOP patterns — encapsulation, design patterns, well-defined classes. Organize code around objects and their behaviors.")
|
|
73
|
+
} else if (s.startsWith("performance") || s.includes("optimize")) {
|
|
74
|
+
lines.push(" → Optimize for performance. Consider time/space complexity. Avoid unnecessary allocations and redundant operations.")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (profile.extra_notes) {
|
|
78
|
+
lines.push(`- User requirements: ${profile.extra_notes} — treat these as hard requirements`)
|
|
79
|
+
}
|
|
80
|
+
profileBlock = lines.join("\n")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userInstructions = [...instructions, rules, profileBlock].filter(Boolean).join("\n\n")
|
|
84
|
+
|
|
85
|
+
// Detect project context (framework, language, build tool, etc.)
|
|
86
|
+
const projectContext = await detectProjectContext(cwd)
|
|
87
|
+
|
|
88
|
+
// Build structured blocks for provider-level cache optimization
|
|
89
|
+
const result = await buildSystemPromptBlocks({ mode, model, cwd, agent, tools, skills, userInstructions, projectContext, language })
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toolPatternFromArgs(args) {
|
|
94
|
+
if (!args || typeof args !== "object") return "*"
|
|
95
|
+
if (Array.isArray(args.changes) && args.changes.length > 0) {
|
|
96
|
+
return args.changes
|
|
97
|
+
.map((change) => change?.path)
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(",")
|
|
100
|
+
}
|
|
101
|
+
return String(args.path || args.command || args.pattern || args.task_id || "*")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeMessageForCache(msg) {
|
|
105
|
+
const content = msg?.content
|
|
106
|
+
// For array content (image blocks, tool_use, tool_result), serialize to a stable string
|
|
107
|
+
if (Array.isArray(content)) {
|
|
108
|
+
const textParts = content
|
|
109
|
+
.filter((b) => b.type === "text")
|
|
110
|
+
.map((b) => b.text || "")
|
|
111
|
+
.join("\n")
|
|
112
|
+
const imageParts = content
|
|
113
|
+
.filter((b) => b.type === "image")
|
|
114
|
+
.map((b) => `[image:${b.path || "inline"}]`)
|
|
115
|
+
.join(" ")
|
|
116
|
+
const toolUseParts = content
|
|
117
|
+
.filter((b) => b.type === "tool_use")
|
|
118
|
+
.map((b) => `[tool_use:${b.name}:${b.id}]`)
|
|
119
|
+
.join(" ")
|
|
120
|
+
const toolResultParts = content
|
|
121
|
+
.filter((b) => b.type === "tool_result")
|
|
122
|
+
.map((b) => `[tool_result:${b.tool_use_id}:${String(b.content || "").slice(0, 100)}]`)
|
|
123
|
+
.join(" ")
|
|
124
|
+
const extras = [imageParts, toolUseParts, toolResultParts].filter(Boolean).join("\n")
|
|
125
|
+
return {
|
|
126
|
+
role: String(msg?.role || ""),
|
|
127
|
+
content: `${textParts}${extras ? "\n" + extras : ""}`
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
role: String(msg?.role || ""),
|
|
132
|
+
content: String(content || "")
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isPrefixMessages(prefix, full) {
|
|
137
|
+
if (!Array.isArray(prefix) || !Array.isArray(full)) return false
|
|
138
|
+
if (prefix.length > full.length) return false
|
|
139
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
140
|
+
if (prefix[i].role !== full[i].role || prefix[i].content !== full[i].content) return false
|
|
141
|
+
}
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function processTurnLoop({
|
|
146
|
+
prompt,
|
|
147
|
+
contentBlocks = null,
|
|
148
|
+
mode,
|
|
149
|
+
model,
|
|
150
|
+
providerType,
|
|
151
|
+
sessionId,
|
|
152
|
+
configState,
|
|
153
|
+
baseUrl = null,
|
|
154
|
+
apiKeyEnv = null,
|
|
155
|
+
depth = 0,
|
|
156
|
+
signal = null,
|
|
157
|
+
output = null,
|
|
158
|
+
subagent = null,
|
|
159
|
+
agent = null,
|
|
160
|
+
allowQuestion = true,
|
|
161
|
+
toolContext = {}
|
|
162
|
+
}) {
|
|
163
|
+
const cwd = process.cwd()
|
|
164
|
+
await initHookBus(cwd)
|
|
165
|
+
|
|
166
|
+
if (depth > 8) {
|
|
167
|
+
return {
|
|
168
|
+
sessionId,
|
|
169
|
+
turnId: newId("turn"),
|
|
170
|
+
reply: "task delegation depth exceeded",
|
|
171
|
+
emittedText: false,
|
|
172
|
+
context: null,
|
|
173
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
174
|
+
toolEvents: []
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const turnId = newId("turn")
|
|
179
|
+
const configMaxSteps = Math.max(1, Number(configState.config.agent.max_steps || 128))
|
|
180
|
+
const maxSteps = (subagent?.maxTurns > 0) ? Math.min(configMaxSteps, subagent.maxTurns) : configMaxSteps
|
|
181
|
+
const verifyCompletion = configState.config.agent?.verify_completion !== false
|
|
182
|
+
const recoveryEnabled = isRecoveryEnabled(configState.config)
|
|
183
|
+
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
184
|
+
const toolEvents = []
|
|
185
|
+
const doomTracker = [] // recent tool call signatures for doom loop detection
|
|
186
|
+
let emittedAnyText = false
|
|
187
|
+
let lastContextMeter = null
|
|
188
|
+
let contextCachePoint = null
|
|
189
|
+
const thresholdRatio = Number(configState.config.session?.compaction_threshold_ratio ?? 0.7)
|
|
190
|
+
const thresholdMessages = Number(configState.config.session?.compaction_threshold_messages ?? 50)
|
|
191
|
+
const cachePointsEnabled = configState.config.session?.context_cache_points !== false
|
|
192
|
+
const useNativeCompaction = supportsNativeCompaction(providerType, model)
|
|
193
|
+
const nativeCompactionTrigger = useNativeCompaction ? Math.floor(modelContextLimit(model, configState) * thresholdRatio) : 0
|
|
194
|
+
|
|
195
|
+
await touchSession({
|
|
196
|
+
sessionId,
|
|
197
|
+
mode,
|
|
198
|
+
model,
|
|
199
|
+
providerType,
|
|
200
|
+
cwd,
|
|
201
|
+
status: "active",
|
|
202
|
+
title: subagent ? `${subagent.name}: ${prompt.slice(0, 60)}` : null
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await EventBus.emit({
|
|
206
|
+
type: EVENT_TYPES.TURN_START,
|
|
207
|
+
sessionId,
|
|
208
|
+
turnId,
|
|
209
|
+
payload: { mode, model, providerType, prompt }
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const queue = await pendingRejections(cwd)
|
|
213
|
+
const rejectionText = queue.length
|
|
214
|
+
? [
|
|
215
|
+
"<review-rejections>",
|
|
216
|
+
...queue.map((entry, index) => `${index + 1}. file=${entry.file} reason=${entry.reason} risk=${entry.riskScore ?? "unknown"}`),
|
|
217
|
+
"</review-rejections>",
|
|
218
|
+
"Address these rejected changes before introducing new risky edits."
|
|
219
|
+
].join("\n")
|
|
220
|
+
: ""
|
|
221
|
+
const effectivePrompt = rejectionText ? `${prompt}\n\n${rejectionText}` : prompt
|
|
222
|
+
|
|
223
|
+
// If contentBlocks provided (e.g. images), build array content for the message.
|
|
224
|
+
// Prepend rejection text as a text block if needed.
|
|
225
|
+
let messageContent
|
|
226
|
+
if (contentBlocks && Array.isArray(contentBlocks)) {
|
|
227
|
+
const blocks = [...contentBlocks]
|
|
228
|
+
if (rejectionText) {
|
|
229
|
+
// Find the first text block and prepend rejection text
|
|
230
|
+
const textIdx = blocks.findIndex((b) => b.type === "text")
|
|
231
|
+
if (textIdx >= 0) {
|
|
232
|
+
blocks[textIdx] = { type: "text", text: `${blocks[textIdx].text}\n\n${rejectionText}` }
|
|
233
|
+
} else {
|
|
234
|
+
blocks.unshift({ type: "text", text: rejectionText })
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
messageContent = blocks
|
|
238
|
+
} else {
|
|
239
|
+
messageContent = effectivePrompt
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const userMessage = await appendMessage(sessionId, "user", messageContent, {
|
|
243
|
+
mode,
|
|
244
|
+
model,
|
|
245
|
+
providerType,
|
|
246
|
+
turnId
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
await appendPart(sessionId, {
|
|
250
|
+
type: "turn-start",
|
|
251
|
+
messageId: userMessage.id,
|
|
252
|
+
turnId,
|
|
253
|
+
mode,
|
|
254
|
+
model,
|
|
255
|
+
providerType
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
let systemTools = await ToolRegistry.list({ mode, config: configState.config, cwd })
|
|
259
|
+
if (agent?.tools) {
|
|
260
|
+
systemTools = systemTools.filter((t) => agent.tools.includes(t.name))
|
|
261
|
+
}
|
|
262
|
+
const skills = SkillRegistry.isReady() ? SkillRegistry.listForSystemPrompt() : []
|
|
263
|
+
const language = configState.config.language || "en"
|
|
264
|
+
const systemPrompt = await buildSystemPrompt({ mode, model, cwd, agent, tools: systemTools, skills, language })
|
|
265
|
+
// systemPrompt = { text, blocks } — providers use blocks for cache optimization
|
|
266
|
+
const delegateTask = createTaskDelegate({
|
|
267
|
+
config: configState.config,
|
|
268
|
+
parentSessionId: sessionId,
|
|
269
|
+
model,
|
|
270
|
+
providerType,
|
|
271
|
+
runSubtask: async ({
|
|
272
|
+
prompt: subPrompt,
|
|
273
|
+
sessionId: subSessionId,
|
|
274
|
+
model: subModel,
|
|
275
|
+
providerType: subProvider,
|
|
276
|
+
subagent: resolvedSubagent,
|
|
277
|
+
allowQuestion: subAllowQuestion = false
|
|
278
|
+
}) => {
|
|
279
|
+
return processTurnLoop({
|
|
280
|
+
prompt: subPrompt,
|
|
281
|
+
mode: "agent",
|
|
282
|
+
model: subModel,
|
|
283
|
+
providerType: subProvider,
|
|
284
|
+
sessionId: subSessionId,
|
|
285
|
+
configState,
|
|
286
|
+
baseUrl,
|
|
287
|
+
apiKeyEnv,
|
|
288
|
+
depth: depth + 1,
|
|
289
|
+
signal,
|
|
290
|
+
subagent: resolvedSubagent,
|
|
291
|
+
allowQuestion: subAllowQuestion,
|
|
292
|
+
toolContext
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const MAX_CONTINUES = 8
|
|
298
|
+
const MAX_TOTAL_CONTINUES = 24 // hard cap on total auto-continues per turn
|
|
299
|
+
let continueCount = 0
|
|
300
|
+
let totalContinueCount = 0
|
|
301
|
+
let nudgeCount = 0
|
|
302
|
+
let finalReply = ""
|
|
303
|
+
const sinkWrite = typeof output?.write === "function"
|
|
304
|
+
? output.write
|
|
305
|
+
: () => {}
|
|
306
|
+
try {
|
|
307
|
+
for (let step = 1; step <= maxSteps; step++) {
|
|
308
|
+
await markTurnInProgress(sessionId, turnId, step, recoveryEnabled)
|
|
309
|
+
await EventBus.emit({
|
|
310
|
+
type: EVENT_TYPES.TURN_STEP_START,
|
|
311
|
+
sessionId,
|
|
312
|
+
turnId,
|
|
313
|
+
payload: { step }
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
let tools = await ToolRegistry.list({ mode, config: configState.config, cwd })
|
|
317
|
+
if (agent?.tools) {
|
|
318
|
+
tools = tools.filter((t) => agent.tools.includes(t.name))
|
|
319
|
+
}
|
|
320
|
+
let history = await getConversationHistory(sessionId, Number(configState.config.session.max_history || 30))
|
|
321
|
+
|
|
322
|
+
const normalizedHistory = history.map(normalizeMessageForCache)
|
|
323
|
+
let contextTokens = estimateTokenCount(normalizedHistory)
|
|
324
|
+
let contextFromCache = false
|
|
325
|
+
|
|
326
|
+
// Use real token counting API when available (includes system + tools + messages)
|
|
327
|
+
const realCount = await countTokensProvider({
|
|
328
|
+
configState, providerType, model,
|
|
329
|
+
system: systemPrompt, messages: history, tools,
|
|
330
|
+
baseUrl, apiKeyEnv
|
|
331
|
+
})
|
|
332
|
+
if (realCount != null) {
|
|
333
|
+
contextTokens = realCount
|
|
334
|
+
} else if (contextCachePoint && isPrefixMessages(contextCachePoint.messages, normalizedHistory)) {
|
|
335
|
+
const delta = normalizedHistory.slice(contextCachePoint.messages.length)
|
|
336
|
+
contextTokens = contextCachePoint.tokens + estimateTokenCount(delta)
|
|
337
|
+
contextFromCache = true
|
|
338
|
+
} else if (contextCachePoint) {
|
|
339
|
+
contextCachePoint = null
|
|
340
|
+
}
|
|
341
|
+
const contextLimit = modelContextLimit(model, configState)
|
|
342
|
+
const contextRatio = contextLimit > 0 ? Math.min(1, contextTokens / contextLimit) : 0
|
|
343
|
+
lastContextMeter = {
|
|
344
|
+
tokens: contextTokens,
|
|
345
|
+
limit: contextLimit,
|
|
346
|
+
ratio: contextRatio,
|
|
347
|
+
percent: Math.round(contextRatio * 100),
|
|
348
|
+
fromCache: contextFromCache
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (cachePointsEnabled && (step === 1 || contextRatio >= thresholdRatio)) {
|
|
352
|
+
contextCachePoint = {
|
|
353
|
+
messages: normalizedHistory,
|
|
354
|
+
tokens: contextTokens
|
|
355
|
+
}
|
|
356
|
+
await appendPart(sessionId, {
|
|
357
|
+
type: "context-cache-point",
|
|
358
|
+
turnId,
|
|
359
|
+
step,
|
|
360
|
+
tokenEstimate: contextTokens,
|
|
361
|
+
contextLimit,
|
|
362
|
+
contextRatio
|
|
363
|
+
})
|
|
364
|
+
await saveCheckpoint(sessionId, {
|
|
365
|
+
kind: "context-cache-point",
|
|
366
|
+
iteration: step,
|
|
367
|
+
turnId,
|
|
368
|
+
step,
|
|
369
|
+
tokenEstimate: contextTokens,
|
|
370
|
+
contextLimit,
|
|
371
|
+
contextRatio,
|
|
372
|
+
messageCount: normalizedHistory.length,
|
|
373
|
+
fromCache: contextFromCache
|
|
374
|
+
})
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!useNativeCompaction && shouldCompact({
|
|
378
|
+
messages: normalizedHistory,
|
|
379
|
+
model,
|
|
380
|
+
thresholdMessages,
|
|
381
|
+
thresholdRatio,
|
|
382
|
+
configState,
|
|
383
|
+
realTokenCount: realCount != null ? contextTokens : null
|
|
384
|
+
})) {
|
|
385
|
+
const compactResult = await compactSession({
|
|
386
|
+
sessionId, model, providerType, configState, baseUrl, apiKeyEnv
|
|
387
|
+
})
|
|
388
|
+
if (compactResult.compacted) {
|
|
389
|
+
await EventBus.emit({ type: EVENT_TYPES.SESSION_COMPACTED, sessionId, turnId, payload: compactResult })
|
|
390
|
+
history = await getConversationHistory(sessionId, Number(configState.config.session.max_history || 30))
|
|
391
|
+
const compactedMeter = contextUtilization(history.map(normalizeMessageForCache), model, configState)
|
|
392
|
+
lastContextMeter = { ...compactedMeter, fromCache: false }
|
|
393
|
+
contextCachePoint = {
|
|
394
|
+
messages: history.map(normalizeMessageForCache),
|
|
395
|
+
tokens: compactedMeter.tokens
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const messages = await HookBus.messagesTransform([...history])
|
|
401
|
+
|
|
402
|
+
let response
|
|
403
|
+
try {
|
|
404
|
+
const chunks = requestProviderStream({
|
|
405
|
+
configState,
|
|
406
|
+
providerType,
|
|
407
|
+
model,
|
|
408
|
+
system: systemPrompt,
|
|
409
|
+
messages,
|
|
410
|
+
tools,
|
|
411
|
+
baseUrl,
|
|
412
|
+
apiKeyEnv,
|
|
413
|
+
signal,
|
|
414
|
+
compaction: useNativeCompaction ? { trigger: nativeCompactionTrigger } : null
|
|
415
|
+
})
|
|
416
|
+
const textParts = []
|
|
417
|
+
const streamToolCalls = []
|
|
418
|
+
let streamUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
419
|
+
let streamStopReason = "end_turn"
|
|
420
|
+
const mdEnabled = configState.config.ui?.markdown_render !== false
|
|
421
|
+
const streamRenderer = mdEnabled ? createStreamRenderer() : null
|
|
422
|
+
let inThinking = false
|
|
423
|
+
let thinkingLineStart = true
|
|
424
|
+
|
|
425
|
+
for await (const chunk of chunks) {
|
|
426
|
+
if (chunk.type === "thinking") {
|
|
427
|
+
const text = chunk.content || ""
|
|
428
|
+
if (!inThinking) {
|
|
429
|
+
inThinking = true
|
|
430
|
+
thinkingLineStart = true
|
|
431
|
+
await EventBus.emit({ type: EVENT_TYPES.STREAM_THINKING_START, sessionId, turnId, payload: { step } })
|
|
432
|
+
sinkWrite(paint("●", "#666666") + " " + paint("Thinking", null, { dim: true }) + " " + paint("∨", null, { dim: true }) + "\n")
|
|
433
|
+
}
|
|
434
|
+
// 只在行首加缩进,避免 chunk 中间出现多余空格
|
|
435
|
+
const indented = text.replace(/^|\n/g, (m) => {
|
|
436
|
+
if (m === "\n") { thinkingLineStart = true; return "\n" }
|
|
437
|
+
if (thinkingLineStart) { thinkingLineStart = false; return " " }
|
|
438
|
+
return ""
|
|
439
|
+
})
|
|
440
|
+
// 如果 chunk 末尾是换行,标记下一个 chunk 需要缩进
|
|
441
|
+
if (text.endsWith("\n")) thinkingLineStart = true
|
|
442
|
+
sinkWrite(paint(indented, null, { dim: true }))
|
|
443
|
+
} else if (chunk.type === "text") {
|
|
444
|
+
if (inThinking) {
|
|
445
|
+
sinkWrite("\n")
|
|
446
|
+
inThinking = false
|
|
447
|
+
}
|
|
448
|
+
if (textParts.length === 0) {
|
|
449
|
+
await EventBus.emit({ type: EVENT_TYPES.STREAM_TEXT_START, sessionId, turnId, payload: { step } })
|
|
450
|
+
}
|
|
451
|
+
if (streamRenderer) {
|
|
452
|
+
const rendered = streamRenderer.push(chunk.content)
|
|
453
|
+
if (rendered) sinkWrite(rendered)
|
|
454
|
+
} else {
|
|
455
|
+
sinkWrite(chunk.content)
|
|
456
|
+
}
|
|
457
|
+
textParts.push(chunk.content)
|
|
458
|
+
} else if (chunk.type === "tool_call") {
|
|
459
|
+
if (inThinking) {
|
|
460
|
+
sinkWrite("\n")
|
|
461
|
+
inThinking = false
|
|
462
|
+
}
|
|
463
|
+
streamToolCalls.push(chunk.call)
|
|
464
|
+
} else if (chunk.type === "usage") {
|
|
465
|
+
streamUsage = chunk.usage
|
|
466
|
+
} else if (chunk.type === "compaction") {
|
|
467
|
+
sinkWrite(paint("\n ↻ context compacted by provider\n", "cyan", { dim: true }))
|
|
468
|
+
} else if (chunk.type === "stop") {
|
|
469
|
+
streamStopReason = chunk.reason || "end_turn"
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (inThinking) {
|
|
473
|
+
sinkWrite("\n")
|
|
474
|
+
}
|
|
475
|
+
if (streamRenderer) {
|
|
476
|
+
const tail = streamRenderer.flush()
|
|
477
|
+
if (tail) sinkWrite(tail)
|
|
478
|
+
}
|
|
479
|
+
if (textParts.length) {
|
|
480
|
+
sinkWrite("\n")
|
|
481
|
+
emittedAnyText = true
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
response = {
|
|
485
|
+
text: textParts.join(""),
|
|
486
|
+
toolCalls: streamToolCalls,
|
|
487
|
+
usage: streamUsage,
|
|
488
|
+
stopReason: streamStopReason
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
if (error.needsCompaction) {
|
|
492
|
+
const compactResult = await compactSession({
|
|
493
|
+
sessionId, model, providerType, configState, baseUrl, apiKeyEnv
|
|
494
|
+
})
|
|
495
|
+
if (compactResult.compacted) {
|
|
496
|
+
await EventBus.emit({ type: EVENT_TYPES.SESSION_COMPACTED, sessionId, turnId, payload: compactResult })
|
|
497
|
+
continue
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
await appendPart(sessionId, {
|
|
501
|
+
type: "provider-error",
|
|
502
|
+
messageId: userMessage.id,
|
|
503
|
+
step,
|
|
504
|
+
turnId,
|
|
505
|
+
error: error.message,
|
|
506
|
+
errorClass: error.errorClass || "unknown",
|
|
507
|
+
needsCompaction: Boolean(error.needsCompaction)
|
|
508
|
+
})
|
|
509
|
+
throw error
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
addUsage(usage, response.usage || {})
|
|
513
|
+
|
|
514
|
+
// Update context meter with real API total input tokens
|
|
515
|
+
// Anthropic: input_tokens is only non-cached portion; total = input + cacheRead + cacheWrite
|
|
516
|
+
// OpenAI: prompt_tokens is already the total
|
|
517
|
+
const u = response.usage || {}
|
|
518
|
+
const totalInput = (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0)
|
|
519
|
+
if (totalInput > 0) {
|
|
520
|
+
const contextLimit = modelContextLimit(model, configState)
|
|
521
|
+
const contextRatio = contextLimit > 0 ? Math.min(1, totalInput / contextLimit) : 0
|
|
522
|
+
lastContextMeter = {
|
|
523
|
+
tokens: totalInput,
|
|
524
|
+
limit: contextLimit,
|
|
525
|
+
ratio: contextRatio,
|
|
526
|
+
percent: Math.round(contextRatio * 100),
|
|
527
|
+
fromCache: false,
|
|
528
|
+
cacheRead: u.cacheRead || 0,
|
|
529
|
+
cacheWrite: u.cacheWrite || 0,
|
|
530
|
+
inputUncached: u.input || 0
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Emit cumulative usage so status bar can update in real-time
|
|
535
|
+
await EventBus.emit({
|
|
536
|
+
type: EVENT_TYPES.TURN_USAGE_UPDATE,
|
|
537
|
+
sessionId,
|
|
538
|
+
turnId,
|
|
539
|
+
payload: { usage: { ...usage }, step, model, context: lastContextMeter }
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
// --- Auto-continue on output truncation (max_tokens) ---
|
|
543
|
+
if (response.stopReason === "max_tokens" && continueCount < MAX_CONTINUES && totalContinueCount < MAX_TOTAL_CONTINUES) {
|
|
544
|
+
continueCount++
|
|
545
|
+
totalContinueCount++
|
|
546
|
+
sinkWrite(paint(`\n ↳ output truncated, auto-continuing (${continueCount}/${MAX_CONTINUES})...\n`, "yellow", { dim: true }))
|
|
547
|
+
|
|
548
|
+
// Drop any tool calls with parse errors (truncated JSON from cutoff)
|
|
549
|
+
const validToolCalls = (response.toolCalls || []).filter(tc => !tc.args?.__parse_error)
|
|
550
|
+
|
|
551
|
+
// Save partial output as assistant message
|
|
552
|
+
const partialContent = []
|
|
553
|
+
if (response.text) {
|
|
554
|
+
partialContent.push({ type: "text", text: response.text })
|
|
555
|
+
}
|
|
556
|
+
for (const call of validToolCalls) {
|
|
557
|
+
partialContent.push({ type: "tool_use", id: call.id, name: call.name, input: call.args || {} })
|
|
558
|
+
}
|
|
559
|
+
if (partialContent.length) {
|
|
560
|
+
await appendMessage(sessionId, "assistant", partialContent.length === 1 && partialContent[0].type === "text"
|
|
561
|
+
? partialContent[0].text
|
|
562
|
+
: partialContent, {
|
|
563
|
+
mode, model, providerType, step, turnId, truncated: true
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// If there were valid tool calls, execute them and add results before continuing
|
|
568
|
+
if (validToolCalls.length) {
|
|
569
|
+
const resultContent = []
|
|
570
|
+
for (const call of validToolCalls) {
|
|
571
|
+
resultContent.push({
|
|
572
|
+
type: "tool_result",
|
|
573
|
+
tool_use_id: call.id,
|
|
574
|
+
content: "[truncated response — tool call acknowledged but output was cut off]",
|
|
575
|
+
is_error: true
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
await appendMessage(sessionId, "user", resultContent, {
|
|
579
|
+
mode, model, providerType, step, turnId, synthetic: true
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Inject continue prompt (localized) — include info about what was truncated
|
|
584
|
+
const hadTruncatedToolCalls = (response.toolCalls || []).some(tc => tc.args?.__parse_error)
|
|
585
|
+
const truncatedToolNames = (response.toolCalls || []).filter(tc => tc.args?.__parse_error).map(tc => tc.name).join(", ")
|
|
586
|
+
const toolHint = hadTruncatedToolCalls
|
|
587
|
+
? (language === "zh"
|
|
588
|
+
? `\n被截断的工具调用: ${truncatedToolNames}。请完整重新发起这些工具调用。如果是创建大文件,使用 write(mode="append") 分段追加;如果是修改已有文件的局部内容,使用 patch 按行号范围替换。`
|
|
589
|
+
: `\nTruncated tool calls: ${truncatedToolNames}. Re-issue these tool calls completely. For large file creation, use write(mode="append") to append in chunks. For modifying sections of existing files, use patch to replace by line range.`)
|
|
590
|
+
: ""
|
|
591
|
+
// Anchor: last 200 chars of truncated text so model knows exactly where to resume
|
|
592
|
+
const textTail = response.text ? response.text.slice(-200) : ""
|
|
593
|
+
const anchorHint = textTail
|
|
594
|
+
? (language === "zh"
|
|
595
|
+
? `\n[锚点] 上次输出末尾:...${textTail}`
|
|
596
|
+
: `\n[Anchor] Last output ended with: ...${textTail}`)
|
|
597
|
+
: ""
|
|
598
|
+
const continuePrompt = language === "zh"
|
|
599
|
+
? `[输出被截断 ${continueCount}/${MAX_CONTINUES}] 你的上一条回复在输出 token 上限处被截断。请从你停止的地方精确继续,不要重复已经写过的内容。如果你正在执行工具调用,请完整重新发起。${toolHint}${anchorHint}`
|
|
600
|
+
: `[OUTPUT TRUNCATED ${continueCount}/${MAX_CONTINUES}] Your previous response was cut off at the output token limit. Continue EXACTLY from where you stopped. Do not repeat any content you already wrote. If you were in the middle of a tool call, re-issue it completely.${toolHint}${anchorHint}`
|
|
601
|
+
await appendMessage(sessionId, "user", continuePrompt,
|
|
602
|
+
{ mode, model, providerType, step, turnId, synthetic: true }
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
// Don't consume a step for auto-continue
|
|
606
|
+
step--
|
|
607
|
+
continue
|
|
608
|
+
}
|
|
609
|
+
// Reset continue count on successful non-truncated response
|
|
610
|
+
continueCount = 0
|
|
611
|
+
|
|
612
|
+
if (!response.toolCalls?.length) {
|
|
613
|
+
// Enhanced task completion verification
|
|
614
|
+
if (verifyCompletion && nudgeCount < 2) {
|
|
615
|
+
try {
|
|
616
|
+
const validator = await createValidator({ cwd, configState })
|
|
617
|
+
const validationResult = await validator.validate({
|
|
618
|
+
todoState: toolContext._todoState
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
if (!validationResult.passed) {
|
|
622
|
+
nudgeCount++
|
|
623
|
+
const validationPrompt = language === "zh"
|
|
624
|
+
? `[任务验证失败] 您报告任务已完成,但以下验证失败:\n\n${validationResult.message}\n\n请修复问题后再报告完成。`
|
|
625
|
+
: `[TASK VERIFICATION FAILED] You indicated completion, but verification failed:\n\n${validationResult.message}\n\nPlease fix the issues before declaring completion.`
|
|
626
|
+
|
|
627
|
+
await appendMessage(sessionId, "user", validationPrompt,
|
|
628
|
+
{ mode, model, providerType, step, turnId, synthetic: true }
|
|
629
|
+
)
|
|
630
|
+
continue
|
|
631
|
+
}
|
|
632
|
+
} catch (validationError) {
|
|
633
|
+
sinkWrite(paint(`\n ⚠ Task validation skipped: ${validationError.message}\n`, "yellow", { dim: true }))
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
finalReply = (response.text || "").trim() || "No content returned from provider."
|
|
638
|
+
const assistant = await appendMessage(sessionId, "assistant", finalReply, {
|
|
639
|
+
mode,
|
|
640
|
+
model,
|
|
641
|
+
providerType,
|
|
642
|
+
step,
|
|
643
|
+
turnId
|
|
644
|
+
})
|
|
645
|
+
await appendPart(sessionId, {
|
|
646
|
+
type: "assistant-response",
|
|
647
|
+
messageId: assistant.id,
|
|
648
|
+
step,
|
|
649
|
+
turnId,
|
|
650
|
+
hasText: Boolean(finalReply)
|
|
651
|
+
})
|
|
652
|
+
await markSessionStatus(sessionId, "active")
|
|
653
|
+
if (queue.length) {
|
|
654
|
+
await markRejectionsConsumed(
|
|
655
|
+
queue.map((entry) => entry.id),
|
|
656
|
+
sessionId,
|
|
657
|
+
cwd
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
await markTurnFinished(sessionId, recoveryEnabled)
|
|
661
|
+
await EventBus.emit({
|
|
662
|
+
type: EVENT_TYPES.TURN_FINISH,
|
|
663
|
+
sessionId,
|
|
664
|
+
turnId,
|
|
665
|
+
payload: { step, reply: finalReply }
|
|
666
|
+
})
|
|
667
|
+
return {
|
|
668
|
+
sessionId,
|
|
669
|
+
turnId,
|
|
670
|
+
reply: finalReply,
|
|
671
|
+
emittedText: emittedAnyText,
|
|
672
|
+
context: lastContextMeter,
|
|
673
|
+
usage,
|
|
674
|
+
toolEvents
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// --- Execute tool calls (read-only in parallel, write tools serially) ---
|
|
679
|
+
async function executeOneCall(call) {
|
|
680
|
+
const runningPart = await appendPart(sessionId, {
|
|
681
|
+
type: "tool-call",
|
|
682
|
+
messageId: userMessage.id,
|
|
683
|
+
step,
|
|
684
|
+
turnId,
|
|
685
|
+
tool: call.name,
|
|
686
|
+
args: call.args,
|
|
687
|
+
status: "running",
|
|
688
|
+
output: ""
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
const pattern = toolPatternFromArgs(call.args)
|
|
692
|
+
const command = call.name === "bash" ? String(call.args?.command || "") : ""
|
|
693
|
+
const risk = ["bash", "write", "edit", "task"].includes(call.name) ? 9 : 1
|
|
694
|
+
let result
|
|
695
|
+
try {
|
|
696
|
+
const hookTransformed = await HookBus.toolBefore({
|
|
697
|
+
tool: call.name,
|
|
698
|
+
toolName: call.name,
|
|
699
|
+
args: call.args,
|
|
700
|
+
sessionId,
|
|
701
|
+
step,
|
|
702
|
+
cwd,
|
|
703
|
+
mode
|
|
704
|
+
})
|
|
705
|
+
if (hookTransformed?.args) call.args = hookTransformed.args
|
|
706
|
+
|
|
707
|
+
if (call.name === "question" && !allowQuestion) {
|
|
708
|
+
call.args = {
|
|
709
|
+
...(call.args || {}),
|
|
710
|
+
_allowQuestion: false
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Plan mode enforcement: block write tools when _planMode is active
|
|
715
|
+
if (toolContext._planMode && !READ_ONLY_TOOLS.has(call.name) && call.name !== "exit_plan") {
|
|
716
|
+
result = {
|
|
717
|
+
name: call.name,
|
|
718
|
+
status: "error",
|
|
719
|
+
output: `[PLAN MODE] Cannot execute '${call.name}' in plan mode. Finish your plan outline and call exit_plan to present it for user approval.`
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
await PermissionEngine.check({
|
|
723
|
+
config: configState.config,
|
|
724
|
+
sessionId,
|
|
725
|
+
tool: call.name,
|
|
726
|
+
mode,
|
|
727
|
+
pattern,
|
|
728
|
+
command,
|
|
729
|
+
risk,
|
|
730
|
+
reason: `tool call from model at step ${step}`
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
const tool = await ToolRegistry.get(call.name)
|
|
734
|
+
result = !tool
|
|
735
|
+
? {
|
|
736
|
+
name: call.name,
|
|
737
|
+
status: "error",
|
|
738
|
+
output: `unknown tool: ${call.name}`,
|
|
739
|
+
error: `unknown tool: ${call.name}`
|
|
740
|
+
}
|
|
741
|
+
: await executeTool({
|
|
742
|
+
tool,
|
|
743
|
+
args: call.args,
|
|
744
|
+
sessionId,
|
|
745
|
+
turnId,
|
|
746
|
+
context: {
|
|
747
|
+
cwd,
|
|
748
|
+
mode,
|
|
749
|
+
delegateTask,
|
|
750
|
+
signal,
|
|
751
|
+
sessionId,
|
|
752
|
+
turnId,
|
|
753
|
+
config: configState.config,
|
|
754
|
+
...toolContext
|
|
755
|
+
},
|
|
756
|
+
signal
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
result = {
|
|
761
|
+
name: call.name,
|
|
762
|
+
status: "error",
|
|
763
|
+
output: error.message,
|
|
764
|
+
error: error.message
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Sync _planMode back to toolContext after enter_plan / exit_plan
|
|
769
|
+
if (call.name === "enter_plan" && result.status !== "error") {
|
|
770
|
+
toolContext._planMode = true
|
|
771
|
+
} else if (call.name === "exit_plan" && result.status !== "error") {
|
|
772
|
+
toolContext._planMode = false
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const hookAfterResult = await HookBus.toolAfter({
|
|
776
|
+
tool: call.name,
|
|
777
|
+
toolName: call.name,
|
|
778
|
+
args: call.args,
|
|
779
|
+
result,
|
|
780
|
+
sessionId,
|
|
781
|
+
step,
|
|
782
|
+
cwd,
|
|
783
|
+
mode
|
|
784
|
+
})
|
|
785
|
+
if (hookAfterResult?.result) result = hookAfterResult.result
|
|
786
|
+
|
|
787
|
+
// Plan approval interception: if the tool returned planApproval metadata,
|
|
788
|
+
// pause and ask the user to approve/reject the plan
|
|
789
|
+
if (result.metadata?.planApproval) {
|
|
790
|
+
const approval = await askPlanApproval({
|
|
791
|
+
plan: result.metadata.plan || "",
|
|
792
|
+
files: result.metadata.files || []
|
|
793
|
+
})
|
|
794
|
+
result = {
|
|
795
|
+
...result,
|
|
796
|
+
output: approval.approved
|
|
797
|
+
? "User APPROVED the plan. Proceed with implementation immediately."
|
|
798
|
+
: approval.requestChanges
|
|
799
|
+
? `User requested changes to the plan. Feedback: ${approval.feedback || "no specific feedback"}. Revise your plan and call exit_plan again with the updated plan.`
|
|
800
|
+
: `User REJECTED the plan. Feedback: ${approval.feedback || "no feedback provided"}. Do not proceed — the plan has been cancelled.`,
|
|
801
|
+
metadata: { ...result.metadata, planApprovalResult: approval }
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
await appendPart(sessionId, {
|
|
806
|
+
type: "tool-call",
|
|
807
|
+
messageId: userMessage.id,
|
|
808
|
+
step,
|
|
809
|
+
turnId,
|
|
810
|
+
runPartId: runningPart.id,
|
|
811
|
+
tool: call.name,
|
|
812
|
+
args: call.args,
|
|
813
|
+
status: result.status,
|
|
814
|
+
output: result.output
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
return { call, result }
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Split into read-only (parallelizable) and write (serial) groups
|
|
821
|
+
const readOnlyCalls = []
|
|
822
|
+
const writeCalls = []
|
|
823
|
+
for (const call of response.toolCalls) {
|
|
824
|
+
if (READ_ONLY_TOOLS.has(call.name)) {
|
|
825
|
+
readOnlyCalls.push(call)
|
|
826
|
+
} else {
|
|
827
|
+
writeCalls.push(call)
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Execute read-only tools in parallel
|
|
832
|
+
const callResults = new Map() // call.id → { call, result }
|
|
833
|
+
if (readOnlyCalls.length > 0) {
|
|
834
|
+
const settled = await Promise.allSettled(readOnlyCalls.map(executeOneCall))
|
|
835
|
+
for (let si = 0; si < settled.length; si++) {
|
|
836
|
+
const outcome = settled[si]
|
|
837
|
+
if (outcome.status === "fulfilled") {
|
|
838
|
+
callResults.set(outcome.value.call.id, outcome.value)
|
|
839
|
+
} else {
|
|
840
|
+
const failedCall = readOnlyCalls[si]
|
|
841
|
+
callResults.set(failedCall.id, {
|
|
842
|
+
call: failedCall,
|
|
843
|
+
result: {
|
|
844
|
+
name: failedCall.name,
|
|
845
|
+
status: "error",
|
|
846
|
+
output: `Tool execution failed: ${outcome.reason?.message || "unknown error"}`,
|
|
847
|
+
error: outcome.reason?.message || "unknown error"
|
|
848
|
+
}
|
|
849
|
+
})
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Execute write tools serially
|
|
855
|
+
for (const call of writeCalls) {
|
|
856
|
+
const outcome = await executeOneCall(call)
|
|
857
|
+
callResults.set(outcome.call.id, outcome)
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Collect results in original order
|
|
861
|
+
for (const call of response.toolCalls) {
|
|
862
|
+
const entry = callResults.get(call.id)
|
|
863
|
+
if (entry) {
|
|
864
|
+
toolEvents.push({
|
|
865
|
+
step,
|
|
866
|
+
name: entry.call.name,
|
|
867
|
+
args: entry.call.args,
|
|
868
|
+
...entry.result
|
|
869
|
+
})
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// --- Build native tool_use / tool_result messages ---
|
|
874
|
+
// Assistant message: text + tool_use blocks
|
|
875
|
+
const assistantContent = []
|
|
876
|
+
if (response.text) {
|
|
877
|
+
assistantContent.push({ type: "text", text: response.text })
|
|
878
|
+
}
|
|
879
|
+
for (const call of response.toolCalls) {
|
|
880
|
+
assistantContent.push({
|
|
881
|
+
type: "tool_use",
|
|
882
|
+
id: call.id,
|
|
883
|
+
name: call.name,
|
|
884
|
+
input: call.args || {}
|
|
885
|
+
})
|
|
886
|
+
}
|
|
887
|
+
await appendMessage(sessionId, "assistant", assistantContent, {
|
|
888
|
+
mode,
|
|
889
|
+
model,
|
|
890
|
+
providerType,
|
|
891
|
+
step,
|
|
892
|
+
turnId,
|
|
893
|
+
toolCallPhase: true
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
// User message: tool_result blocks (one per tool call, in order)
|
|
897
|
+
// Process output beyond TOOL_RESULT_ACTIVE_LIMIT is truncated to keep context lean
|
|
898
|
+
const resultContent = []
|
|
899
|
+
for (const call of response.toolCalls) {
|
|
900
|
+
const entry = callResults.get(call.id)
|
|
901
|
+
const rawOutput = entry?.result?.output || ""
|
|
902
|
+
const isError = entry?.result?.status === "error"
|
|
903
|
+
const content = rawOutput.length > TOOL_RESULT_ACTIVE_LIMIT
|
|
904
|
+
? `${rawOutput.slice(0, TOOL_RESULT_ACTIVE_LIMIT)}\n[...过程输出已截断,共 ${rawOutput.length} 字符,仅保留前 ${TOOL_RESULT_ACTIVE_LIMIT} 字符]`
|
|
905
|
+
: rawOutput
|
|
906
|
+
resultContent.push({
|
|
907
|
+
type: "tool_result",
|
|
908
|
+
tool_use_id: call.id,
|
|
909
|
+
content,
|
|
910
|
+
is_error: isError
|
|
911
|
+
})
|
|
912
|
+
}
|
|
913
|
+
await appendMessage(sessionId, "user", resultContent, {
|
|
914
|
+
mode,
|
|
915
|
+
model,
|
|
916
|
+
providerType,
|
|
917
|
+
step,
|
|
918
|
+
turnId,
|
|
919
|
+
synthetic: true
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
// --- Doom loop detection: 3x identical tool call → inject warning ---
|
|
923
|
+
for (const call of response.toolCalls) {
|
|
924
|
+
doomTracker.push(`${call.name}::${JSON.stringify(call.args || {})}`)
|
|
925
|
+
}
|
|
926
|
+
if (doomTracker.length > 6) doomTracker.splice(0, doomTracker.length - 6)
|
|
927
|
+
if (doomTracker.length >= 3) {
|
|
928
|
+
const last3 = doomTracker.slice(-3)
|
|
929
|
+
if (last3[0] === last3[1] && last3[1] === last3[2]) {
|
|
930
|
+
await appendMessage(sessionId, "user", "[DOOM LOOP DETECTED] You called the same tool with identical arguments 3 times consecutively. STOP repeating this approach — it will not work. Try a completely different strategy, re-read the relevant files, or ask the user for guidance.", {
|
|
931
|
+
mode, model, providerType, step, turnId, synthetic: true
|
|
932
|
+
})
|
|
933
|
+
doomTracker.length = 0
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// --- Soft step warning: alert model when nearing the limit ---
|
|
938
|
+
if (step === maxSteps - 2) {
|
|
939
|
+
await appendMessage(sessionId, "user", `[STEP LIMIT WARNING] You have used ${step} of ${maxSteps} steps. You are running low — wrap up your current work, summarize progress, and list any remaining tasks.`, {
|
|
940
|
+
mode, model, providerType, step, turnId, synthetic: true
|
|
941
|
+
})
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
await EventBus.emit({
|
|
945
|
+
type: EVENT_TYPES.TURN_STEP_FINISH,
|
|
946
|
+
sessionId,
|
|
947
|
+
turnId,
|
|
948
|
+
payload: { step, toolCalls: response.toolCalls.length }
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
finalReply = "Reached max steps. Review tool outputs and continue in a new turn."
|
|
953
|
+
await appendMessage(sessionId, "assistant", finalReply, {
|
|
954
|
+
mode,
|
|
955
|
+
model,
|
|
956
|
+
providerType,
|
|
957
|
+
turnId,
|
|
958
|
+
maxSteps: true
|
|
959
|
+
})
|
|
960
|
+
await markTurnFinished(sessionId, recoveryEnabled)
|
|
961
|
+
await EventBus.emit({
|
|
962
|
+
type: EVENT_TYPES.TURN_FINISH,
|
|
963
|
+
sessionId,
|
|
964
|
+
turnId,
|
|
965
|
+
payload: { maxSteps: true, reply: finalReply }
|
|
966
|
+
})
|
|
967
|
+
return {
|
|
968
|
+
sessionId,
|
|
969
|
+
turnId,
|
|
970
|
+
reply: finalReply,
|
|
971
|
+
emittedText: emittedAnyText,
|
|
972
|
+
context: lastContextMeter,
|
|
973
|
+
usage,
|
|
974
|
+
toolEvents
|
|
975
|
+
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
await markSessionStatus(sessionId, "error")
|
|
978
|
+
await markTurnFinished(sessionId, recoveryEnabled)
|
|
979
|
+
if (recoveryEnabled) {
|
|
980
|
+
await updateSession(sessionId, {
|
|
981
|
+
retryMeta: {
|
|
982
|
+
inProgress: false,
|
|
983
|
+
turnId,
|
|
984
|
+
failedAt: Date.now(),
|
|
985
|
+
error: error.message
|
|
986
|
+
}
|
|
987
|
+
})
|
|
988
|
+
}
|
|
989
|
+
await EventBus.emit({
|
|
990
|
+
type: EVENT_TYPES.TURN_ERROR,
|
|
991
|
+
sessionId,
|
|
992
|
+
turnId,
|
|
993
|
+
payload: { error: error.message }
|
|
994
|
+
})
|
|
995
|
+
return {
|
|
996
|
+
sessionId,
|
|
997
|
+
turnId,
|
|
998
|
+
reply: `provider error: ${error.message}`,
|
|
999
|
+
emittedText: emittedAnyText,
|
|
1000
|
+
context: lastContextMeter,
|
|
1001
|
+
usage,
|
|
1002
|
+
toolEvents
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|