@tomsun28/pizza 0.1.0 → 0.1.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/README.md +20 -143
- package/README.zh-CN.md +36 -0
- package/dist/bun/cli.d.ts +2 -0
- package/dist/bun/cli.js +6 -0
- package/dist/bun/register-bedrock.d.ts +1 -0
- package/dist/bun/register-bedrock.js +3 -0
- package/dist/cli/args.d.ts +48 -0
- package/dist/cli/args.js +330 -0
- package/dist/cli/config-selector.d.ts +13 -0
- package/dist/cli/config-selector.js +30 -0
- package/dist/cli/file-processor.d.ts +14 -0
- package/dist/cli/file-processor.js +82 -0
- package/dist/cli/initial-message.d.ts +17 -0
- package/dist/cli/initial-message.js +21 -0
- package/dist/cli/list-models.d.ts +8 -0
- package/dist/cli/list-models.js +96 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +68 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +223 -0
- package/dist/core/agent/index.d.ts +6 -0
- package/dist/core/agent/index.js +6 -0
- package/dist/core/agent/types.d.ts +202 -0
- package/dist/core/agent/types.js +51 -0
- package/dist/core/auth-storage.d.ts +135 -0
- package/dist/core/auth-storage.js +417 -0
- package/dist/core/bash-executor.d.ts +31 -0
- package/dist/core/bash-executor.js +107 -0
- package/dist/core/compaction/branch-summarization.d.ts +87 -0
- package/dist/core/compaction/branch-summarization.js +242 -0
- package/dist/core/compaction/compaction-engine.d.ts +40 -0
- package/dist/core/compaction/compaction-engine.js +292 -0
- package/dist/core/compaction/compaction.d.ts +106 -0
- package/dist/core/compaction/compaction.js +295 -0
- package/dist/core/compaction/index.d.ts +7 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/utils.d.ts +37 -0
- package/dist/core/compaction/utils.js +152 -0
- package/dist/core/defaults.d.ts +2 -0
- package/dist/core/defaults.js +1 -0
- package/dist/core/diagnostics.d.ts +14 -0
- package/dist/core/diagnostics.js +1 -0
- package/dist/core/event-bus.d.ts +8 -0
- package/dist/core/event-bus.js +24 -0
- package/dist/core/event-store/events.d.ts +465 -0
- package/dist/core/event-store/events.js +6 -0
- package/dist/core/event-store/index.d.ts +10 -0
- package/dist/core/event-store/index.js +10 -0
- package/dist/core/event-store/sqlite-store.d.ts +27 -0
- package/dist/core/event-store/sqlite-store.js +275 -0
- package/dist/core/event-store/store.d.ts +91 -0
- package/dist/core/event-store/store.js +7 -0
- package/dist/core/event-store/thread-scoped-store.d.ts +29 -0
- package/dist/core/event-store/thread-scoped-store.js +50 -0
- package/dist/core/event-store/types.d.ts +70 -0
- package/dist/core/event-store/types.js +7 -0
- package/dist/core/event-store/workspace.d.ts +37 -0
- package/dist/core/event-store/workspace.js +71 -0
- package/dist/core/exec.d.ts +28 -0
- package/dist/core/exec.js +74 -0
- package/dist/core/export-html/ansi-to-html.d.ts +21 -0
- package/dist/core/export-html/ansi-to-html.js +248 -0
- package/dist/core/export-html/index.d.ts +29 -0
- package/dist/core/export-html/index.js +186 -0
- package/dist/core/export-html/template.css +1017 -0
- package/dist/core/export-html/template.html +55 -0
- package/dist/core/export-html/template.js +1742 -0
- package/dist/core/export-html/tool-renderer.d.ts +39 -0
- package/dist/core/export-html/tool-renderer.js +94 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/extensions/index.d.ts +12 -0
- package/dist/core/extensions/index.js +9 -0
- package/dist/core/extensions/loader.d.ts +24 -0
- package/dist/core/extensions/loader.js +492 -0
- package/dist/core/extensions/runner.d.ts +167 -0
- package/dist/core/extensions/runner.js +898 -0
- package/dist/core/extensions/session-context.d.ts +67 -0
- package/dist/core/extensions/session-context.js +272 -0
- package/dist/core/extensions/types.d.ts +1147 -0
- package/dist/core/extensions/types.js +44 -0
- package/dist/core/extensions/wrapper.d.ts +19 -0
- package/dist/core/extensions/wrapper.js +21 -0
- package/dist/core/footer-data-provider.d.ts +47 -0
- package/dist/core/footer-data-provider.js +321 -0
- package/dist/core/goal/index.d.ts +3 -0
- package/dist/core/goal/index.js +1 -0
- package/dist/core/goal/scheduler.d.ts +83 -0
- package/dist/core/goal/scheduler.js +154 -0
- package/dist/core/goal/types.d.ts +182 -0
- package/dist/core/goal/types.js +11 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/intent/classifier.d.ts +53 -0
- package/dist/core/intent/classifier.js +353 -0
- package/dist/core/intent/index.d.ts +8 -0
- package/dist/core/intent/index.js +8 -0
- package/dist/core/intent/tool-adapter.d.ts +31 -0
- package/dist/core/intent/tool-adapter.js +168 -0
- package/dist/core/intent/types.d.ts +71 -0
- package/dist/core/intent/types.js +6 -0
- package/dist/core/keybindings.d.ts +297 -0
- package/dist/core/keybindings.js +250 -0
- package/dist/core/messages.d.ts +30 -0
- package/dist/core/messages.js +134 -0
- package/dist/core/model-registry.d.ts +131 -0
- package/dist/core/model-registry.js +634 -0
- package/dist/core/model-resolver.d.ts +109 -0
- package/dist/core/model-resolver.js +486 -0
- package/dist/core/output-guard.d.ts +5 -0
- package/dist/core/output-guard.js +58 -0
- package/dist/core/package-manager.d.ts +196 -0
- package/dist/core/package-manager.js +1873 -0
- package/dist/core/projection/boundary-inferrer.d.ts +58 -0
- package/dist/core/projection/boundary-inferrer.js +122 -0
- package/dist/core/projection/event-to-message.d.ts +35 -0
- package/dist/core/projection/event-to-message.js +253 -0
- package/dist/core/projection/goal-projection.d.ts +67 -0
- package/dist/core/projection/goal-projection.js +339 -0
- package/dist/core/projection/index.d.ts +13 -0
- package/dist/core/projection/index.js +13 -0
- package/dist/core/projection/session-manager.d.ts +100 -0
- package/dist/core/projection/session-manager.js +331 -0
- package/dist/core/projection/session-projection.d.ts +58 -0
- package/dist/core/projection/session-projection.js +257 -0
- package/dist/core/projection/task-history.d.ts +13 -0
- package/dist/core/projection/task-history.js +61 -0
- package/dist/core/projection/timeline-projection.d.ts +82 -0
- package/dist/core/projection/timeline-projection.js +276 -0
- package/dist/core/projection/types.d.ts +69 -0
- package/dist/core/projection/types.js +6 -0
- package/dist/core/prompt-templates.d.ts +51 -0
- package/dist/core/prompt-templates.js +249 -0
- package/dist/core/resolve-config-value.d.ts +22 -0
- package/dist/core/resolve-config-value.js +125 -0
- package/dist/core/resource-loader.d.ts +193 -0
- package/dist/core/resource-loader.js +685 -0
- package/dist/core/runtime/ai-client.d.ts +12 -0
- package/dist/core/runtime/ai-client.js +153 -0
- package/dist/core/runtime/checkpoint.d.ts +12 -0
- package/dist/core/runtime/checkpoint.js +78 -0
- package/dist/core/runtime/index.d.ts +12 -0
- package/dist/core/runtime/index.js +12 -0
- package/dist/core/runtime/llm-types.d.ts +99 -0
- package/dist/core/runtime/llm-types.js +6 -0
- package/dist/core/runtime/local-runtime.d.ts +23 -0
- package/dist/core/runtime/local-runtime.js +59 -0
- package/dist/core/runtime/policies.d.ts +72 -0
- package/dist/core/runtime/policies.js +37 -0
- package/dist/core/runtime/reactor.d.ts +213 -0
- package/dist/core/runtime/reactor.js +992 -0
- package/dist/core/runtime/runtime.d.ts +191 -0
- package/dist/core/runtime/runtime.js +449 -0
- package/dist/core/runtime/types.d.ts +42 -0
- package/dist/core/runtime/types.js +1 -0
- package/dist/core/sdk.d.ts +12 -0
- package/dist/core/sdk.js +10 -0
- package/dist/core/session-context-builder.d.ts +9 -0
- package/dist/core/session-context-builder.js +87 -0
- package/dist/core/session-cwd.d.ts +18 -0
- package/dist/core/session-cwd.js +36 -0
- package/dist/core/session-facade-factory.d.ts +79 -0
- package/dist/core/session-facade-factory.js +460 -0
- package/dist/core/session-facade.d.ts +57 -0
- package/dist/core/session-facade.js +91 -0
- package/dist/core/session-listing.d.ts +30 -0
- package/dist/core/session-listing.js +168 -0
- package/dist/core/session-reader.d.ts +19 -0
- package/dist/core/session-reader.js +101 -0
- package/dist/core/session-ref.d.ts +7 -0
- package/dist/core/session-ref.js +25 -0
- package/dist/core/session-services.d.ts +51 -0
- package/dist/core/session-services.js +90 -0
- package/dist/core/session-stats.d.ts +43 -0
- package/dist/core/session-stats.js +41 -0
- package/dist/core/settings-manager.d.ts +236 -0
- package/dist/core/settings-manager.js +706 -0
- package/dist/core/skill-block-parser.d.ts +12 -0
- package/dist/core/skill-block-parser.js +15 -0
- package/dist/core/skills.d.ts +59 -0
- package/dist/core/skills.js +408 -0
- package/dist/core/slash-commands.d.ts +13 -0
- package/dist/core/slash-commands.js +17 -0
- package/dist/core/source-info.d.ts +17 -0
- package/dist/core/source-info.js +18 -0
- package/dist/core/system-prompt.d.ts +27 -0
- package/dist/core/system-prompt.js +179 -0
- package/dist/core/timings.d.ts +7 -0
- package/dist/core/timings.js +30 -0
- package/dist/core/tools/bash.d.ts +95 -0
- package/dist/core/tools/bash.js +513 -0
- package/dist/core/tools/builtin-commands.d.ts +75 -0
- package/dist/core/tools/builtin-commands.js +503 -0
- package/dist/core/tools/compat-commands.d.ts +12 -0
- package/dist/core/tools/compat-commands.js +1280 -0
- package/dist/core/tools/edit-diff.d.ts +59 -0
- package/dist/core/tools/edit-diff.js +263 -0
- package/dist/core/tools/edit.d.ts +49 -0
- package/dist/core/tools/edit.js +340 -0
- package/dist/core/tools/file-mutation-queue.d.ts +5 -0
- package/dist/core/tools/file-mutation-queue.js +36 -0
- package/dist/core/tools/find.d.ts +34 -0
- package/dist/core/tools/find.js +459 -0
- package/dist/core/tools/grep.d.ts +36 -0
- package/dist/core/tools/grep.js +422 -0
- package/dist/core/tools/index.d.ts +70 -0
- package/dist/core/tools/index.js +129 -0
- package/dist/core/tools/line-anchors.d.ts +33 -0
- package/dist/core/tools/line-anchors.js +151 -0
- package/dist/core/tools/ls.d.ts +36 -0
- package/dist/core/tools/ls.js +268 -0
- package/dist/core/tools/path-utils.d.ts +7 -0
- package/dist/core/tools/path-utils.js +80 -0
- package/dist/core/tools/read.d.ts +35 -0
- package/dist/core/tools/read.js +244 -0
- package/dist/core/tools/render-utils.d.ts +20 -0
- package/dist/core/tools/render-utils.js +48 -0
- package/dist/core/tools/session-split.d.ts +9 -0
- package/dist/core/tools/session-split.js +77 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +13 -0
- package/dist/core/tools/tool-definition-wrapper.js +32 -0
- package/dist/core/tools/truncate.d.ts +69 -0
- package/dist/core/tools/truncate.js +204 -0
- package/dist/core/tools/write.d.ts +25 -0
- package/dist/core/tools/write.js +211 -0
- package/dist/core/types/session-types.d.ts +123 -0
- package/dist/core/types/session-types.js +8 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +38 -0
- package/dist/main.d.ts +11 -2
- package/dist/main.js +577 -149
- package/dist/modes/event-mapper.d.ts +115 -0
- package/dist/modes/event-mapper.js +200 -0
- package/dist/modes/gui/server.d.ts +14 -0
- package/dist/modes/gui/server.js +1032 -0
- package/dist/modes/index.d.ts +10 -0
- package/dist/modes/index.js +9 -0
- package/dist/modes/interactive/assets/clankolas.png +0 -0
- package/dist/modes/interactive/components/armin.d.ts +33 -0
- package/dist/modes/interactive/components/armin.js +328 -0
- package/dist/modes/interactive/components/assistant-message.d.ts +19 -0
- package/dist/modes/interactive/components/assistant-message.js +115 -0
- package/dist/modes/interactive/components/bash-execution.d.ts +33 -0
- package/dist/modes/interactive/components/bash-execution.js +169 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts +15 -0
- package/dist/modes/interactive/components/bordered-loader.js +50 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts +15 -0
- package/dist/modes/interactive/components/branch-summary-message.js +41 -0
- package/dist/modes/interactive/components/brand-header.d.ts +33 -0
- package/dist/modes/interactive/components/brand-header.js +235 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +15 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +42 -0
- package/dist/modes/interactive/components/config-selector.d.ts +70 -0
- package/dist/modes/interactive/components/config-selector.js +469 -0
- package/dist/modes/interactive/components/countdown-timer.d.ts +13 -0
- package/dist/modes/interactive/components/countdown-timer.js +27 -0
- package/dist/modes/interactive/components/custom-editor.d.ts +27 -0
- package/dist/modes/interactive/components/custom-editor.js +94 -0
- package/dist/modes/interactive/components/custom-message.d.ts +19 -0
- package/dist/modes/interactive/components/custom-message.js +73 -0
- package/dist/modes/interactive/components/daxnuts.d.ts +22 -0
- package/dist/modes/interactive/components/daxnuts.js +137 -0
- package/dist/modes/interactive/components/diff.d.ts +11 -0
- package/dist/modes/interactive/components/diff.js +132 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts +14 -0
- package/dist/modes/interactive/components/dynamic-border.js +19 -0
- package/dist/modes/interactive/components/earendil-announcement.d.ts +4 -0
- package/dist/modes/interactive/components/earendil-announcement.js +39 -0
- package/dist/modes/interactive/components/extension-editor.d.ts +19 -0
- package/dist/modes/interactive/components/extension-editor.js +105 -0
- package/dist/modes/interactive/components/extension-input.d.ts +22 -0
- package/dist/modes/interactive/components/extension-input.js +54 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +23 -0
- package/dist/modes/interactive/components/extension-selector.js +70 -0
- package/dist/modes/interactive/components/footer.d.ts +55 -0
- package/dist/modes/interactive/components/footer.js +207 -0
- package/dist/modes/interactive/components/index.d.ts +31 -0
- package/dist/modes/interactive/components/index.js +32 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts +7 -0
- package/dist/modes/interactive/components/keybinding-hints.js +21 -0
- package/dist/modes/interactive/components/login-dialog.d.ts +41 -0
- package/dist/modes/interactive/components/login-dialog.js +138 -0
- package/dist/modes/interactive/components/model-selector.d.ts +32 -0
- package/dist/modes/interactive/components/model-selector.js +209 -0
- package/dist/modes/interactive/components/oauth-selector.d.ts +18 -0
- package/dist/modes/interactive/components/oauth-selector.js +91 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +57 -0
- package/dist/modes/interactive/components/settings-selector.js +299 -0
- package/dist/modes/interactive/components/show-images-selector.d.ts +9 -0
- package/dist/modes/interactive/components/show-images-selector.js +37 -0
- package/dist/modes/interactive/components/skill-invocation-message.d.ts +16 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +44 -0
- package/dist/modes/interactive/components/theme-selector.d.ts +10 -0
- package/dist/modes/interactive/components/theme-selector.js +47 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +10 -0
- package/dist/modes/interactive/components/thinking-selector.js +50 -0
- package/dist/modes/interactive/components/timeline-view.d.ts +31 -0
- package/dist/modes/interactive/components/timeline-view.js +81 -0
- package/dist/modes/interactive/components/tool-execution.d.ts +64 -0
- package/dist/modes/interactive/components/tool-execution.js +305 -0
- package/dist/modes/interactive/components/tree-selector.d.ts +88 -0
- package/dist/modes/interactive/components/tree-selector.js +1072 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts +29 -0
- package/dist/modes/interactive/components/user-message-selector.js +110 -0
- package/dist/modes/interactive/components/user-message.d.ts +9 -0
- package/dist/modes/interactive/components/user-message.js +27 -0
- package/dist/modes/interactive/components/visual-truncate.d.ts +23 -0
- package/dist/modes/interactive/components/visual-truncate.js +32 -0
- package/dist/modes/interactive/interactive-mode.d.ts +394 -0
- package/dist/modes/interactive/interactive-mode.js +4116 -0
- package/dist/modes/interactive/theme/dark.json +85 -0
- package/dist/modes/interactive/theme/light.json +84 -0
- package/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/dist/modes/interactive/theme/theme.d.ts +80 -0
- package/dist/modes/interactive/theme/theme.js +973 -0
- package/dist/modes/print-mode.d.ts +27 -0
- package/dist/modes/print-mode.js +102 -0
- package/dist/modes/rpc/jsonl.d.ts +16 -0
- package/dist/modes/rpc/jsonl.js +48 -0
- package/dist/modes/rpc/rpc-client.d.ts +219 -0
- package/dist/modes/rpc/rpc-client.js +405 -0
- package/dist/modes/rpc/rpc-mode.d.ts +19 -0
- package/dist/modes/rpc/rpc-mode.js +381 -0
- package/dist/modes/rpc/rpc-types.d.ts +406 -0
- package/dist/modes/rpc/rpc-types.js +7 -0
- package/dist/package-manager-cli.d.ts +3 -0
- package/dist/package-manager-cli.js +233 -0
- package/dist/utils/changelog.d.ts +20 -0
- package/dist/utils/changelog.js +86 -0
- package/dist/utils/child-process.d.ts +10 -0
- package/dist/utils/child-process.js +77 -0
- package/dist/utils/clipboard-image.d.ts +10 -0
- package/dist/utils/clipboard-image.js +244 -0
- package/dist/utils/clipboard-native.d.ts +7 -0
- package/dist/utils/clipboard-native.js +13 -0
- package/dist/utils/clipboard.d.ts +1 -0
- package/dist/utils/clipboard.js +77 -0
- package/dist/utils/exif-orientation.d.ts +4 -0
- package/dist/utils/exif-orientation.js +157 -0
- package/dist/utils/frontmatter.d.ts +7 -0
- package/dist/utils/frontmatter.js +25 -0
- package/dist/utils/git.d.ts +25 -0
- package/dist/utils/git.js +162 -0
- package/dist/utils/image-convert.d.ts +8 -0
- package/dist/utils/image-convert.js +38 -0
- package/dist/utils/image-resize.d.ts +35 -0
- package/dist/utils/image-resize.js +136 -0
- package/dist/utils/mime.d.ts +1 -0
- package/dist/utils/mime.js +25 -0
- package/dist/utils/path-shims.d.ts +1 -0
- package/dist/utils/path-shims.js +90 -0
- package/dist/utils/paths.d.ts +6 -0
- package/dist/utils/paths.js +18 -0
- package/dist/utils/photon.d.ts +20 -0
- package/dist/utils/photon.js +120 -0
- package/dist/utils/shell.d.ts +29 -0
- package/dist/utils/shell.js +189 -0
- package/dist/utils/sleep.d.ts +4 -0
- package/dist/utils/sleep.js +16 -0
- package/dist/utils/tools-manager.d.ts +3 -0
- package/dist/utils/tools-manager.js +101 -0
- package/dist/vendor/bin/darwin-arm64/fd +0 -0
- package/dist/vendor/bin/darwin-arm64/rg +0 -0
- package/dist/vendor/bin/darwin-x64/fd +0 -0
- package/dist/vendor/bin/darwin-x64/rg +0 -0
- package/dist/vendor/bin/linux-arm64/fd +0 -0
- package/dist/vendor/bin/linux-arm64/rg +0 -0
- package/dist/vendor/bin/linux-x64/fd +0 -0
- package/dist/vendor/bin/linux-x64/rg +0 -0
- package/dist/vendor/bin/win32-arm64/fd.exe +0 -0
- package/dist/vendor/bin/win32-arm64/rg.exe +0 -0
- package/dist/vendor/bin/win32-x64/fd.exe +0 -0
- package/dist/vendor/bin/win32-x64/rg.exe +0 -0
- package/package.json +90 -26
- package/dist/extension/core.d.ts +0 -69
- package/dist/extension/core.js +0 -270
- package/dist/extension/core.js.map +0 -1
- package/dist/extension/index.d.ts +0 -14
- package/dist/extension/index.js +0 -374
- package/dist/extension/index.js.map +0 -1
- package/dist/main.js.map +0 -1
- package/themes/pizza.json +0 -87
|
@@ -0,0 +1,992 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactor — Event-Driven Agent Core
|
|
3
|
+
*
|
|
4
|
+
* Replaces the AgentLoop while-loop with a set of typed handlers that react
|
|
5
|
+
* to events appended to the EventStore. Each handler is a pure async function
|
|
6
|
+
* that may emit new events. The reactor itself is stateless — all mutable
|
|
7
|
+
* state lives in the EventStore.
|
|
8
|
+
*
|
|
9
|
+
* Event flow:
|
|
10
|
+
*
|
|
11
|
+
* USER_MESSAGE / USER_INTERRUPT / TOOL_RESULTS_AGGREGATED / RETRY_SCHEDULED
|
|
12
|
+
* ↓
|
|
13
|
+
* [onUserMessage] ──→ emit AGENT_TURN_REQUESTED
|
|
14
|
+
* ↓
|
|
15
|
+
* [onAgentTurnRequested] ──→ emit LLM_CALL_REQUESTED
|
|
16
|
+
* ↓
|
|
17
|
+
* [onLlmCallRequested] ──→ call LLM → emit AGENT_MESSAGE_START/CHUNK/END
|
|
18
|
+
* ↓
|
|
19
|
+
* [onAgentMessageEnd]
|
|
20
|
+
* ├── if no tool_calls → emit AGENT_TURN_COMPLETED
|
|
21
|
+
* └── if tool_calls → emit INTENT_TOOL_CALL (one per call)
|
|
22
|
+
* ↓
|
|
23
|
+
* [onIntentToolCall] ──→ (approval gate) → emit TOOL_EXECUTION_START/END
|
|
24
|
+
* ↓
|
|
25
|
+
* [onToolExecutionEnd] ──→ track count → if all done → emit TOOL_RESULTS_AGGREGATED
|
|
26
|
+
* ↓
|
|
27
|
+
* [onToolResultsAggregated] ──→ emit AGENT_TURN_REQUESTED ← loop back
|
|
28
|
+
*
|
|
29
|
+
* Other handlers (run in parallel, do not gate the main flow):
|
|
30
|
+
* onAgentTurnCompleted — UI notification
|
|
31
|
+
* onLlmCallFailed — retry scheduling
|
|
32
|
+
* onUserInterrupt — abort signal + turn completion
|
|
33
|
+
* onCompactionRequested — compaction orchestration
|
|
34
|
+
*/
|
|
35
|
+
import { extractToolCalls } from "../projection/event-to-message.js";
|
|
36
|
+
import { DefaultRetryPolicy, NoopCompactionPolicy } from "./policies.js";
|
|
37
|
+
/**
|
|
38
|
+
* Safety limit: if the model calls the exact same set of tools (by name) in this
|
|
39
|
+
* many consecutive turns, the reactor assumes it is stuck in an unproductive loop
|
|
40
|
+
* and completes the turn instead of requesting another. Prevents runaway loops
|
|
41
|
+
* (e.g. a model repeatedly calling a control tool like session_split).
|
|
42
|
+
*/
|
|
43
|
+
const MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS = 6;
|
|
44
|
+
export class Reactor {
|
|
45
|
+
constructor(config) {
|
|
46
|
+
this.turnTrackers = new Map();
|
|
47
|
+
this.handlers = {};
|
|
48
|
+
this.unsubscribers = [];
|
|
49
|
+
this._isRunning = false;
|
|
50
|
+
/** Pending follow-up messages (delivered after current turn completes without interrupt). */
|
|
51
|
+
this.followUpQueue = [];
|
|
52
|
+
/** Retry timers waiting to re-enter the turn loop. */
|
|
53
|
+
this.retryTimers = new Map();
|
|
54
|
+
/** Set when abort() is called with no new content — next turn completion should discard queued follow-ups. */
|
|
55
|
+
this._abortedByUser = false;
|
|
56
|
+
/** Tool names called in each consecutive tool-use round within the current prompt cycle. */
|
|
57
|
+
this._toolRoundSignatures = [];
|
|
58
|
+
// ─── INTENT_TOOL_CALL ──────────────────────────────────────────────────
|
|
59
|
+
this._pendingApprovals = new Map();
|
|
60
|
+
this.config = config;
|
|
61
|
+
this.retryPolicy = config.retryPolicy ?? new DefaultRetryPolicy();
|
|
62
|
+
this.compactionPolicy = config.compactionPolicy ?? new NoopCompactionPolicy();
|
|
63
|
+
this._registerAllHandlers();
|
|
64
|
+
}
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// Lifecycle
|
|
67
|
+
// =========================================================================
|
|
68
|
+
/** Subscribe to all events and start the reactor. */
|
|
69
|
+
async start() {
|
|
70
|
+
if (this._isRunning)
|
|
71
|
+
return;
|
|
72
|
+
this._isRunning = true;
|
|
73
|
+
this.abortController = new AbortController();
|
|
74
|
+
// Subscribe to the event store — all events flow through the store
|
|
75
|
+
const unsub = this.config.store.subscribe((event) => this._dispatch(event));
|
|
76
|
+
this.unsubscribers.push(unsub);
|
|
77
|
+
// Replay any pre-existing follow-up events that were queued while the reactor was idle
|
|
78
|
+
this._replayPendingFollowUps();
|
|
79
|
+
// Emit RUNTIME_STARTED if the store is fresh
|
|
80
|
+
if (this.config.store.size === 0) {
|
|
81
|
+
this._emit({
|
|
82
|
+
actor_id: "runtime",
|
|
83
|
+
type: "RUNTIME_STARTED",
|
|
84
|
+
payload: {},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Replay USER_FOLLOWUP_QUEUED events that were appended while the reactor was idle.
|
|
90
|
+
* Skips events that have already been consumed (i.e. an AGENT_TURN_COMPLETED with a
|
|
91
|
+
* caused_by chain that includes them).
|
|
92
|
+
*
|
|
93
|
+
* For now we use a simple heuristic: count followups vs followup-driven user messages.
|
|
94
|
+
* If followups outnumber the consumed ones, replay the difference into the in-memory queue.
|
|
95
|
+
*/
|
|
96
|
+
_replayPendingFollowUps() {
|
|
97
|
+
const followups = this.config.store.query({ types: ["USER_FOLLOWUP_QUEUED"] });
|
|
98
|
+
if (followups.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
// A USER_FOLLOWUP_QUEUED is "consumed" when a USER_MESSAGE event has it as caused_by.
|
|
101
|
+
const userMessages = this.config.store.query({ types: ["USER_MESSAGE"] });
|
|
102
|
+
const consumedCausedBy = new Set(userMessages.map((m) => m.caused_by).filter((id) => !!id));
|
|
103
|
+
for (const f of followups) {
|
|
104
|
+
if (consumedCausedBy.has(f.event_id))
|
|
105
|
+
continue;
|
|
106
|
+
const p = f.payload;
|
|
107
|
+
this.followUpQueue.push({ content: p.content, images: p.images });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Stop the reactor. Clears all subscriptions. */
|
|
111
|
+
stop() {
|
|
112
|
+
this._isRunning = false;
|
|
113
|
+
this.abortController?.abort();
|
|
114
|
+
for (const { timeout } of this.retryTimers.values()) {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
}
|
|
117
|
+
this.retryTimers.clear();
|
|
118
|
+
for (const unsub of this.unsubscribers)
|
|
119
|
+
unsub();
|
|
120
|
+
this.unsubscribers = [];
|
|
121
|
+
}
|
|
122
|
+
/** Check if the reactor is running. */
|
|
123
|
+
get isRunning() {
|
|
124
|
+
return this._isRunning;
|
|
125
|
+
}
|
|
126
|
+
/** Abort signal for the currently active reactor run. */
|
|
127
|
+
get signal() {
|
|
128
|
+
return this.abortController?.signal;
|
|
129
|
+
}
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// Event dispatch
|
|
132
|
+
// =========================================================================
|
|
133
|
+
async _dispatch(event) {
|
|
134
|
+
const handler = this.handlers[event.type];
|
|
135
|
+
if (!handler)
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
await handler(event);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// Emit as RUNTIME_ERROR instead of throwing — the reactor must keep running
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
this._emit({
|
|
144
|
+
actor_id: "runtime",
|
|
145
|
+
type: "RUNTIME_ERROR",
|
|
146
|
+
payload: { error: msg, stack: err instanceof Error ? err.stack : undefined, causing_event_id: event.event_id },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
_emit(input) {
|
|
151
|
+
return this.config.store.append(input);
|
|
152
|
+
}
|
|
153
|
+
/** Check if the reactor has been asked to stop. */
|
|
154
|
+
_shouldInterrupt() {
|
|
155
|
+
return this.abortController?.signal.aborted ?? false;
|
|
156
|
+
}
|
|
157
|
+
/** Interrupt the reactor. */
|
|
158
|
+
interrupt() {
|
|
159
|
+
this.abortController?.abort();
|
|
160
|
+
}
|
|
161
|
+
// =========================================================================
|
|
162
|
+
// Handler registration
|
|
163
|
+
// =========================================================================
|
|
164
|
+
_registerAllHandlers() {
|
|
165
|
+
this.handlers = {
|
|
166
|
+
USER_MESSAGE: this._onUserMessage.bind(this),
|
|
167
|
+
USER_INTERRUPT: this._onUserInterrupt.bind(this),
|
|
168
|
+
USER_FOLLOWUP_QUEUED: this._onUserFollowupQueued.bind(this),
|
|
169
|
+
AGENT_TURN_REQUESTED: this._onAgentTurnRequested.bind(this),
|
|
170
|
+
LLM_CALL_REQUESTED: this._onLlmCallRequested.bind(this),
|
|
171
|
+
AGENT_MESSAGE_END: this._onAgentMessageEnd.bind(this),
|
|
172
|
+
INTENT_TOOL_CALL: this._onIntentToolCall.bind(this),
|
|
173
|
+
TOOL_EXECUTION_END: this._onToolExecutionEnd.bind(this),
|
|
174
|
+
TOOL_RESULTS_AGGREGATED: this._onToolResultsAggregated.bind(this),
|
|
175
|
+
LLM_CALL_FAILED: this._onLlmCallFailed.bind(this),
|
|
176
|
+
AGENT_TURN_COMPLETED: this._onAgentTurnCompleted.bind(this),
|
|
177
|
+
USER_APPROVAL: this._onUserApproval.bind(this),
|
|
178
|
+
USER_REJECTION: this._onUserRejection.bind(this),
|
|
179
|
+
COMPACTION_REQUESTED: this._onCompactionRequested.bind(this),
|
|
180
|
+
SESSION_BOUNDARY_INFERRED: this._onSessionBoundaryInferred.bind(this),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// =========================================================================
|
|
184
|
+
// User-facing entry points (used by modes)
|
|
185
|
+
// =========================================================================
|
|
186
|
+
/**
|
|
187
|
+
* Send a user message to the agent (idle path).
|
|
188
|
+
* For interrupt-while-streaming use steer(). For after-turn-completion use followUp().
|
|
189
|
+
*/
|
|
190
|
+
prompt(content, images) {
|
|
191
|
+
this._emit({
|
|
192
|
+
actor_id: "user",
|
|
193
|
+
type: "USER_MESSAGE",
|
|
194
|
+
payload: { content, images },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Steer: interrupt current turn with a new user message.
|
|
199
|
+
* The reactor aborts the current LLM call (if any) and treats the steer message
|
|
200
|
+
* as a fresh USER_MESSAGE once the turn settles.
|
|
201
|
+
*/
|
|
202
|
+
steer(content, images) {
|
|
203
|
+
this._emit({
|
|
204
|
+
actor_id: "user",
|
|
205
|
+
type: "USER_INTERRUPT",
|
|
206
|
+
payload: { content, images, reason: "steer" },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Follow-up: queue a message to be delivered after the current turn naturally completes.
|
|
211
|
+
* Does not interrupt. If the reactor is idle, behaves like prompt().
|
|
212
|
+
*/
|
|
213
|
+
followUp(content, images) {
|
|
214
|
+
this._emit({
|
|
215
|
+
actor_id: "user",
|
|
216
|
+
type: "USER_FOLLOWUP_QUEUED",
|
|
217
|
+
payload: { content, images },
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Number of pending follow-up messages. */
|
|
221
|
+
get pendingFollowUpCount() {
|
|
222
|
+
return this.followUpQueue.length;
|
|
223
|
+
}
|
|
224
|
+
/** Number of scheduled retries waiting to re-enter the turn loop. */
|
|
225
|
+
get pendingRetryCount() {
|
|
226
|
+
return this.retryTimers.size;
|
|
227
|
+
}
|
|
228
|
+
/** Clear pending follow-ups (e.g. on user-explicit cancel). */
|
|
229
|
+
clearFollowUpQueue() {
|
|
230
|
+
this.followUpQueue = [];
|
|
231
|
+
}
|
|
232
|
+
// =========================================================================
|
|
233
|
+
// HANDLERS
|
|
234
|
+
// =========================================================================
|
|
235
|
+
// ─── USER_MESSAGE ───────────────────────────────────────────────────────
|
|
236
|
+
async _onUserMessage(event) {
|
|
237
|
+
const payload = event.payload;
|
|
238
|
+
if (this._shouldInterrupt())
|
|
239
|
+
return;
|
|
240
|
+
// Reset loop detection for a new prompt cycle
|
|
241
|
+
this._toolRoundSignatures = [];
|
|
242
|
+
// Emit thinking start
|
|
243
|
+
this._emit({
|
|
244
|
+
actor_id: "coder_agent",
|
|
245
|
+
type: "AGENT_THINKING_START",
|
|
246
|
+
payload: { model: this.config.model.model_id },
|
|
247
|
+
caused_by: event.event_id,
|
|
248
|
+
});
|
|
249
|
+
// Emit turn requested — this kicks off the reactor loop
|
|
250
|
+
this._emit({
|
|
251
|
+
actor_id: "coder_agent",
|
|
252
|
+
type: "AGENT_TURN_REQUESTED",
|
|
253
|
+
payload: { reason: "user_message" },
|
|
254
|
+
caused_by: event.event_id,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// ─── USER_INTERRUPT ─────────────────────────────────────────────────────
|
|
258
|
+
async _onUserInterrupt(event) {
|
|
259
|
+
const payload = event.payload;
|
|
260
|
+
this.interrupt();
|
|
261
|
+
// Track hard abort (no new content) so _onAgentTurnCompleted doesn't drain follow-ups.
|
|
262
|
+
if (payload.content === undefined) {
|
|
263
|
+
this._abortedByUser = true;
|
|
264
|
+
}
|
|
265
|
+
// If steer carried new content, queue it as a follow-up so the turn-completion
|
|
266
|
+
// handler picks it up after the current turn settles (aborted state).
|
|
267
|
+
if (payload.content !== undefined) {
|
|
268
|
+
this.followUpQueue.push({
|
|
269
|
+
content: payload.content,
|
|
270
|
+
images: payload.images,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
// Cancel any pending compaction so we don't block turn completion
|
|
274
|
+
this.compactionAbort?.abort();
|
|
275
|
+
if (this.retryTimers.size > 0) {
|
|
276
|
+
for (const [scheduledEventId, retry] of this.retryTimers) {
|
|
277
|
+
clearTimeout(retry.timeout);
|
|
278
|
+
this._emit({
|
|
279
|
+
actor_id: "runtime",
|
|
280
|
+
type: "RETRY_ABORTED",
|
|
281
|
+
payload: {
|
|
282
|
+
attempt: retry.attempt,
|
|
283
|
+
reason: "user_interrupt",
|
|
284
|
+
error_message: retry.errorMessage,
|
|
285
|
+
scheduled_event_id: scheduledEventId,
|
|
286
|
+
},
|
|
287
|
+
caused_by: event.event_id,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
this.retryTimers.clear();
|
|
291
|
+
this._emit({
|
|
292
|
+
actor_id: "coder_agent",
|
|
293
|
+
type: "AGENT_TURN_END",
|
|
294
|
+
payload: { tool_calls_count: 0 },
|
|
295
|
+
caused_by: event.event_id,
|
|
296
|
+
});
|
|
297
|
+
this._emit({
|
|
298
|
+
actor_id: "coder_agent",
|
|
299
|
+
type: "AGENT_TURN_COMPLETED",
|
|
300
|
+
payload: { reason: "aborted" },
|
|
301
|
+
caused_by: event.event_id,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// ─── USER_FOLLOWUP_QUEUED ───────────────────────────────────────────────
|
|
306
|
+
async _onUserFollowupQueued(event) {
|
|
307
|
+
const payload = event.payload;
|
|
308
|
+
this.followUpQueue.push({ content: payload.content, images: payload.images });
|
|
309
|
+
}
|
|
310
|
+
// ─── AGENT_TURN_REQUESTED ───────────────────────────────────────────────
|
|
311
|
+
async _onAgentTurnRequested(event) {
|
|
312
|
+
if (this._shouldInterrupt())
|
|
313
|
+
return;
|
|
314
|
+
const payload = event.payload;
|
|
315
|
+
// Build context from projection
|
|
316
|
+
const context = this.config.projection.buildContext({
|
|
317
|
+
max_tokens: this.config.contextBudget,
|
|
318
|
+
});
|
|
319
|
+
// Stash context for the LLM_CALL_REQUESTED handler. MUST be set before _emit
|
|
320
|
+
// because store subscribers fire synchronously and _onLlmCallRequested reads
|
|
321
|
+
// _pendingContext during its sync portion before awaiting the LLM call.
|
|
322
|
+
this._pendingContext = context;
|
|
323
|
+
// Emit turn start
|
|
324
|
+
this._emit({
|
|
325
|
+
actor_id: "coder_agent",
|
|
326
|
+
type: "AGENT_TURN_START",
|
|
327
|
+
payload: { message_count: context.messages.length },
|
|
328
|
+
caused_by: event.event_id,
|
|
329
|
+
});
|
|
330
|
+
// Request an LLM call
|
|
331
|
+
this._emit({
|
|
332
|
+
actor_id: "coder_agent",
|
|
333
|
+
type: "LLM_CALL_REQUESTED",
|
|
334
|
+
payload: { message_count: context.messages.length },
|
|
335
|
+
caused_by: event.event_id,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// ─── LLM_CALL_REQUESTED ─────────────────────────────────────────────────
|
|
339
|
+
async _onLlmCallRequested(event) {
|
|
340
|
+
if (this._shouldInterrupt())
|
|
341
|
+
return;
|
|
342
|
+
const context = this._pendingContext ?? this.config.projection.buildContext({ max_tokens: this.config.contextBudget });
|
|
343
|
+
this._pendingContext = undefined;
|
|
344
|
+
// Emit AGENT_MESSAGE_START up front so chunks have a parent in the causal chain.
|
|
345
|
+
const msgStart = this._emit({
|
|
346
|
+
actor_id: "coder_agent",
|
|
347
|
+
type: "AGENT_MESSAGE_START",
|
|
348
|
+
payload: { model: { provider: this.config.model.provider, model_id: this.config.model.model_id } },
|
|
349
|
+
caused_by: event.event_id,
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
const response = await this.config.llmClient.complete({
|
|
353
|
+
messages: context.messages,
|
|
354
|
+
systemPrompt: this.config.systemPrompt,
|
|
355
|
+
model: this.config.model,
|
|
356
|
+
tools: this.config.tools,
|
|
357
|
+
signal: this.abortController?.signal,
|
|
358
|
+
onChunk: (chunk) => {
|
|
359
|
+
// Translate LLMChunk into an AGENT_MESSAGE_CHUNK event so projections / UI
|
|
360
|
+
// can render the streaming response in real time.
|
|
361
|
+
this._emit({
|
|
362
|
+
actor_id: "coder_agent",
|
|
363
|
+
type: "AGENT_MESSAGE_CHUNK",
|
|
364
|
+
payload: { chunk: chunk },
|
|
365
|
+
caused_by: msgStart.event_id,
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
this._handleLlmResponse(response, event.event_id, msgStart.event_id);
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
const isRetryable = this.retryPolicy.isRetryable({ message: msg });
|
|
374
|
+
this._emit({
|
|
375
|
+
actor_id: "runtime",
|
|
376
|
+
type: "LLM_CALL_FAILED",
|
|
377
|
+
payload: { error: msg, retryable: isRetryable },
|
|
378
|
+
caused_by: event.event_id,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
_handleLlmResponse(response, causedBy, msgStartEventId) {
|
|
383
|
+
// AGENT_MESSAGE_START is now emitted by _onLlmCallRequested before the call so chunks
|
|
384
|
+
// can reference it. For backwards compat with callers that don't stream, emit it here
|
|
385
|
+
// if not already emitted.
|
|
386
|
+
if (!msgStartEventId) {
|
|
387
|
+
const started = this._emit({
|
|
388
|
+
actor_id: "coder_agent",
|
|
389
|
+
type: "AGENT_MESSAGE_START",
|
|
390
|
+
payload: { model: { provider: response.provider, model_id: response.model } },
|
|
391
|
+
caused_by: causedBy,
|
|
392
|
+
});
|
|
393
|
+
msgStartEventId = started.event_id;
|
|
394
|
+
}
|
|
395
|
+
// Emit message end (the chunking is optional; for now we skip to end for simplicity)
|
|
396
|
+
const toolCalls = extractToolCalls(response.content);
|
|
397
|
+
// Append the AGENT_MESSAGE_END event.
|
|
398
|
+
const msgEnd = this._emit({
|
|
399
|
+
actor_id: "coder_agent",
|
|
400
|
+
type: "AGENT_MESSAGE_END",
|
|
401
|
+
payload: {
|
|
402
|
+
content: response.content,
|
|
403
|
+
model: { provider: response.provider, model_id: response.model },
|
|
404
|
+
usage: response.usage,
|
|
405
|
+
stop_reason: response.stopReason,
|
|
406
|
+
error_message: response.errorMessage,
|
|
407
|
+
},
|
|
408
|
+
caused_by: causedBy,
|
|
409
|
+
});
|
|
410
|
+
// Emit thinking end (turn_end is now emitted in _onAgentMessageEnd)
|
|
411
|
+
this._emit({
|
|
412
|
+
actor_id: "coder_agent",
|
|
413
|
+
type: "AGENT_THINKING_END",
|
|
414
|
+
payload: {},
|
|
415
|
+
caused_by: msgEnd.event_id,
|
|
416
|
+
});
|
|
417
|
+
// The ON_AGENT_MESSAGE_END handler will dispatch tool calls or complete the turn
|
|
418
|
+
}
|
|
419
|
+
// ─── AGENT_MESSAGE_END ──────────────────────────────────────────────────
|
|
420
|
+
async _onAgentMessageEnd(event) {
|
|
421
|
+
const payload = event.payload;
|
|
422
|
+
const toolCalls = extractToolCalls(payload.content);
|
|
423
|
+
if (this._shouldInterrupt()) {
|
|
424
|
+
// Emit turn end before completion when interrupted
|
|
425
|
+
this._emit({
|
|
426
|
+
actor_id: "coder_agent",
|
|
427
|
+
type: "AGENT_TURN_END",
|
|
428
|
+
payload: { tool_calls_count: toolCalls.length },
|
|
429
|
+
caused_by: event.event_id,
|
|
430
|
+
});
|
|
431
|
+
this._emit({
|
|
432
|
+
actor_id: "coder_agent",
|
|
433
|
+
type: "AGENT_TURN_COMPLETED",
|
|
434
|
+
payload: { reason: "aborted" },
|
|
435
|
+
caused_by: event.event_id,
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (toolCalls.length === 0 || payload.stop_reason !== "tool_use") {
|
|
440
|
+
// No tool calls — emit turn end and complete the turn
|
|
441
|
+
this._emit({
|
|
442
|
+
actor_id: "coder_agent",
|
|
443
|
+
type: "AGENT_TURN_END",
|
|
444
|
+
payload: { tool_calls_count: 0 },
|
|
445
|
+
caused_by: event.event_id,
|
|
446
|
+
});
|
|
447
|
+
this._emit({
|
|
448
|
+
actor_id: "coder_agent",
|
|
449
|
+
type: "AGENT_TURN_COMPLETED",
|
|
450
|
+
payload: { reason: payload.stop_reason, error_message: payload.error_message },
|
|
451
|
+
caused_by: event.event_id,
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Track this turn's tool executions
|
|
456
|
+
const tracker = {
|
|
457
|
+
assistantMessageEventId: event.event_id,
|
|
458
|
+
expectedCount: toolCalls.length,
|
|
459
|
+
expectedToolCallIds: new Set(toolCalls.map((toolCall) => toolCall.id)),
|
|
460
|
+
received: [],
|
|
461
|
+
abortSignal: this.abortController?.signal,
|
|
462
|
+
};
|
|
463
|
+
this.turnTrackers.set(event.event_id, tracker);
|
|
464
|
+
// Record the sorted tool-name signature for loop detection
|
|
465
|
+
const signature = toolCalls.map((tc) => tc.name).sort();
|
|
466
|
+
this._toolRoundSignatures.push(signature);
|
|
467
|
+
// Emit one INTENT_TOOL_CALL per tool call
|
|
468
|
+
for (const toolCall of toolCalls) {
|
|
469
|
+
const classification = this.config.classifier.classify(toolCall.name, toolCall.arguments);
|
|
470
|
+
this._emit({
|
|
471
|
+
actor_id: "coder_agent",
|
|
472
|
+
type: "INTENT_TOOL_CALL",
|
|
473
|
+
payload: {
|
|
474
|
+
tool_call_id: toolCall.id,
|
|
475
|
+
tool_name: toolCall.name,
|
|
476
|
+
arguments: toolCall.arguments,
|
|
477
|
+
requires_approval: classification.requires_approval,
|
|
478
|
+
classification,
|
|
479
|
+
},
|
|
480
|
+
caused_by: event.event_id,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async _onIntentToolCall(event) {
|
|
485
|
+
if (this._shouldInterrupt())
|
|
486
|
+
return;
|
|
487
|
+
const payload = event.payload;
|
|
488
|
+
if (payload.requires_approval) {
|
|
489
|
+
if (!this.config.approvalHandler) {
|
|
490
|
+
this._emitToolExecutionEnd(payload.tool_call_id, payload.tool_name, {
|
|
491
|
+
content: [{ type: "text", text: "Tool execution requires approval, but no approval handler is available." }],
|
|
492
|
+
is_error: true,
|
|
493
|
+
error_message: "Approval required but no approval handler is available",
|
|
494
|
+
}, event.event_id);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// Wait for user approval/rejection
|
|
498
|
+
const approved = await new Promise((resolve) => {
|
|
499
|
+
this._pendingApprovals.set(event.event_id, {
|
|
500
|
+
resolve,
|
|
501
|
+
tool_call_id: payload.tool_call_id,
|
|
502
|
+
tool_name: payload.tool_name,
|
|
503
|
+
arguments: payload.arguments,
|
|
504
|
+
});
|
|
505
|
+
this.config.approvalHandler?.requestApproval(event.event_id, payload.classification);
|
|
506
|
+
});
|
|
507
|
+
if (!approved) {
|
|
508
|
+
this._emitToolExecutionEnd(payload.tool_call_id, payload.tool_name, {
|
|
509
|
+
content: [{ type: "text", text: "Tool execution rejected by user." }],
|
|
510
|
+
is_error: true,
|
|
511
|
+
error_message: "User rejected the tool call",
|
|
512
|
+
}, event.event_id);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Execute the tool
|
|
517
|
+
await this._executeTool(event.event_id, payload.tool_call_id, payload.tool_name, payload.arguments);
|
|
518
|
+
}
|
|
519
|
+
async _executeTool(causedBy, tool_call_id, tool_name, args) {
|
|
520
|
+
const startTime = Date.now();
|
|
521
|
+
this._emit({
|
|
522
|
+
actor_id: "runtime",
|
|
523
|
+
type: "TOOL_EXECUTION_START",
|
|
524
|
+
payload: { tool_call_id, tool_name, arguments: args },
|
|
525
|
+
caused_by: causedBy,
|
|
526
|
+
});
|
|
527
|
+
try {
|
|
528
|
+
const result = await this.config.runtimeAdapter.executeTool({
|
|
529
|
+
tool_call_id,
|
|
530
|
+
tool_name,
|
|
531
|
+
arguments: args,
|
|
532
|
+
caused_by: causedBy,
|
|
533
|
+
signal: this.abortController?.signal,
|
|
534
|
+
onUpdate: (partial) => {
|
|
535
|
+
this._emitToolExecutionUpdate(tool_call_id, partial, causedBy);
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
this._emitToolExecutionEnd(tool_call_id, tool_name, result, causedBy, startTime);
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
542
|
+
this._emitToolExecutionEnd(tool_call_id, tool_name, {
|
|
543
|
+
content: [{ type: "text", text: msg }],
|
|
544
|
+
is_error: true,
|
|
545
|
+
error_message: msg,
|
|
546
|
+
}, causedBy, startTime);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
_emitToolExecutionUpdate(tool_call_id, partial, causedBy) {
|
|
550
|
+
this._emit({
|
|
551
|
+
actor_id: "runtime",
|
|
552
|
+
type: "TOOL_EXECUTION_UPDATE",
|
|
553
|
+
payload: {
|
|
554
|
+
tool_call_id,
|
|
555
|
+
update: stringifyToolUpdate(partial),
|
|
556
|
+
progress: partial.progress,
|
|
557
|
+
},
|
|
558
|
+
caused_by: causedBy,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
_emitToolExecutionEnd(tool_call_id, tool_name, result, causedBy, startTime) {
|
|
562
|
+
this._emit({
|
|
563
|
+
actor_id: "runtime",
|
|
564
|
+
type: "TOOL_EXECUTION_END",
|
|
565
|
+
payload: {
|
|
566
|
+
tool_call_id,
|
|
567
|
+
tool_name,
|
|
568
|
+
result: result.content,
|
|
569
|
+
details: result.details,
|
|
570
|
+
is_error: result.is_error,
|
|
571
|
+
duration_ms: startTime !== undefined ? Date.now() - startTime : 0,
|
|
572
|
+
file_mutations: result.file_mutations,
|
|
573
|
+
},
|
|
574
|
+
caused_by: causedBy,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
// ─── TOOL_EXECUTION_END ─────────────────────────────────────────────────
|
|
578
|
+
async _onToolExecutionEnd(event) {
|
|
579
|
+
const payload = event.payload;
|
|
580
|
+
// Emit FILE_MUTATION_APPLIED for each mutation reported by the tool
|
|
581
|
+
if (payload.file_mutations?.length) {
|
|
582
|
+
for (const mutation of payload.file_mutations) {
|
|
583
|
+
this._emit({
|
|
584
|
+
actor_id: "runtime",
|
|
585
|
+
type: "FILE_MUTATION_APPLIED",
|
|
586
|
+
payload: {
|
|
587
|
+
path: mutation.path,
|
|
588
|
+
operation: mutation.operation,
|
|
589
|
+
diff: mutation.diff,
|
|
590
|
+
tool_name: payload.tool_name,
|
|
591
|
+
tool_call_id: payload.tool_call_id,
|
|
592
|
+
},
|
|
593
|
+
caused_by: event.event_id,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
// Find the tracker for this tool call's assistant message
|
|
598
|
+
// Walk up the causal chain from the TOOL_EXECUTION_END event
|
|
599
|
+
const causedBy = event.caused_by;
|
|
600
|
+
// The caused_by points to the INTENT_TOOL_CALL event.
|
|
601
|
+
// We need to find the INTENT_TOOL_CALL → AGENT_MESSAGE_END chain.
|
|
602
|
+
// Actually, the causal chain in EventStore should give us this.
|
|
603
|
+
// For now, find the tracker by looking up the chain:
|
|
604
|
+
const chain = causedBy ? this.config.store.getCausalChain(event.event_id) : [];
|
|
605
|
+
// Find the closest AGENT_MESSAGE_END in the causal chain. Consecutive
|
|
606
|
+
// tool-use turns include older assistant messages earlier in the chain.
|
|
607
|
+
let assistantMessageEventId;
|
|
608
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
609
|
+
const e = chain[i];
|
|
610
|
+
if (e.type === "AGENT_MESSAGE_END") {
|
|
611
|
+
assistantMessageEventId = e.event_id;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
let tracker = assistantMessageEventId ? this.turnTrackers.get(assistantMessageEventId) : undefined;
|
|
616
|
+
if (!tracker || !tracker.expectedToolCallIds.has(payload.tool_call_id)) {
|
|
617
|
+
assistantMessageEventId = undefined;
|
|
618
|
+
for (const [messageEventId, tracker] of this.turnTrackers) {
|
|
619
|
+
if (tracker.expectedToolCallIds.has(payload.tool_call_id)) {
|
|
620
|
+
assistantMessageEventId = messageEventId;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
tracker = assistantMessageEventId ? this.turnTrackers.get(assistantMessageEventId) : undefined;
|
|
625
|
+
}
|
|
626
|
+
if (!tracker || !assistantMessageEventId)
|
|
627
|
+
return;
|
|
628
|
+
if (tracker.received.some((result) => result.tool_call_id === payload.tool_call_id))
|
|
629
|
+
return;
|
|
630
|
+
tracker.received.push({
|
|
631
|
+
tool_call_id: payload.tool_call_id,
|
|
632
|
+
tool_name: payload.tool_name,
|
|
633
|
+
result: { content: payload.result, is_error: payload.is_error },
|
|
634
|
+
is_error: payload.is_error,
|
|
635
|
+
});
|
|
636
|
+
// If all tools have completed, emit TOOL_RESULTS_AGGREGATED
|
|
637
|
+
if (tracker.received.length === tracker.expectedCount) {
|
|
638
|
+
this.turnTrackers.delete(assistantMessageEventId);
|
|
639
|
+
this._emit({
|
|
640
|
+
actor_id: "coder_agent",
|
|
641
|
+
type: "TOOL_RESULTS_AGGREGATED",
|
|
642
|
+
payload: {
|
|
643
|
+
assistant_message_event_id: assistantMessageEventId,
|
|
644
|
+
tool_call_count: tracker.expectedCount,
|
|
645
|
+
any_error: tracker.received.some((r) => r.is_error),
|
|
646
|
+
},
|
|
647
|
+
caused_by: event.event_id,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// ─── TOOL_RESULTS_AGGREGATED ────────────────────────────────────────────
|
|
652
|
+
async _onToolResultsAggregated(event) {
|
|
653
|
+
const payload = event.payload;
|
|
654
|
+
if (this._shouldInterrupt()) {
|
|
655
|
+
// Emit turn end before completion when interrupted
|
|
656
|
+
this._emit({
|
|
657
|
+
actor_id: "coder_agent",
|
|
658
|
+
type: "AGENT_TURN_END",
|
|
659
|
+
payload: { tool_calls_count: payload.tool_call_count },
|
|
660
|
+
caused_by: event.event_id,
|
|
661
|
+
});
|
|
662
|
+
this._emit({
|
|
663
|
+
actor_id: "coder_agent",
|
|
664
|
+
type: "AGENT_TURN_COMPLETED",
|
|
665
|
+
payload: { reason: "aborted" },
|
|
666
|
+
caused_by: event.event_id,
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
// Emit turn end to signal completion of this tool-call turn
|
|
671
|
+
this._emit({
|
|
672
|
+
actor_id: "coder_agent",
|
|
673
|
+
type: "AGENT_TURN_END",
|
|
674
|
+
payload: { tool_calls_count: payload.tool_call_count },
|
|
675
|
+
caused_by: event.event_id,
|
|
676
|
+
});
|
|
677
|
+
// Loop detection: if the model has called the exact same set of tools
|
|
678
|
+
// (by name) in too many consecutive rounds, break the loop by completing
|
|
679
|
+
// the turn instead of requesting another LLM round.
|
|
680
|
+
if (this._isInToolLoop()) {
|
|
681
|
+
this._emit({
|
|
682
|
+
actor_id: "coder_agent",
|
|
683
|
+
type: "AGENT_TURN_COMPLETED",
|
|
684
|
+
payload: { reason: "loop_detected" },
|
|
685
|
+
caused_by: event.event_id,
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// Kick off the next turn with the tool results
|
|
690
|
+
this._emit({
|
|
691
|
+
actor_id: "coder_agent",
|
|
692
|
+
type: "AGENT_TURN_REQUESTED",
|
|
693
|
+
payload: { reason: "tool_results" },
|
|
694
|
+
caused_by: event.event_id,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
// ─── SESSION_BOUNDARY_INFERRED ──────────────────────────────────────────
|
|
698
|
+
/**
|
|
699
|
+
* When a session split occurs, refresh the projection to point to the new
|
|
700
|
+
* session. The new session's start_event_id is set to the current USER_MESSAGE,
|
|
701
|
+
* so buildContext() will include the user's request + tool results but exclude
|
|
702
|
+
* old conversation history. This lets the model continue working in the same
|
|
703
|
+
* turn with a clean context.
|
|
704
|
+
*/
|
|
705
|
+
_onSessionBoundaryInferred(_event) {
|
|
706
|
+
if (!this.config.sessionManager)
|
|
707
|
+
return;
|
|
708
|
+
const newProjection = this.config.sessionManager.getActiveSession();
|
|
709
|
+
if (newProjection) {
|
|
710
|
+
this.config.projection = newProjection;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Check whether the model is stuck in a loop — calling the exact same set
|
|
715
|
+
* of tool names in too many consecutive rounds.
|
|
716
|
+
*/
|
|
717
|
+
_isInToolLoop() {
|
|
718
|
+
const sigs = this._toolRoundSignatures;
|
|
719
|
+
if (sigs.length < MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS)
|
|
720
|
+
return false;
|
|
721
|
+
const recent = sigs.slice(-MAX_CONSECUTIVE_IDENTICAL_TOOL_ROUNDS);
|
|
722
|
+
const first = recent[0].join(",");
|
|
723
|
+
return recent.every((sig) => sig.join(",") === first);
|
|
724
|
+
}
|
|
725
|
+
// ─── AGENT_TURN_COMPLETED ───────────────────────────────────────────────
|
|
726
|
+
//
|
|
727
|
+
// Drain the followUp queue (if any) or let the reactor go idle.
|
|
728
|
+
// Also triggers compaction check after a non-error turn.
|
|
729
|
+
_onAgentTurnCompleted(event) {
|
|
730
|
+
const payload = event.payload;
|
|
731
|
+
// If abort() was called, discard any pending follow-ups so the reactor stops.
|
|
732
|
+
// _abortedByUser is set by _onUserInterrupt when the interrupt has no new content.
|
|
733
|
+
if (this._abortedByUser) {
|
|
734
|
+
this.followUpQueue = [];
|
|
735
|
+
this._abortedByUser = false;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (this._scheduleRetryForCompletedError(event)) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
// If a steer or follow-up message is queued, deliver it as a new USER_MESSAGE
|
|
742
|
+
// so the turn cycle restarts.
|
|
743
|
+
if (this.followUpQueue.length > 0) {
|
|
744
|
+
// Reset abort controller for the new turn — the previous turn's interrupt
|
|
745
|
+
// has already been handled.
|
|
746
|
+
this.abortController = new AbortController();
|
|
747
|
+
const next = this.followUpQueue.shift();
|
|
748
|
+
this._emit({
|
|
749
|
+
actor_id: "user",
|
|
750
|
+
type: "USER_MESSAGE",
|
|
751
|
+
payload: { content: next.content, images: next.images },
|
|
752
|
+
caused_by: event.event_id,
|
|
753
|
+
});
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// Check compaction policy after a non-aborted turn
|
|
757
|
+
if (payload.reason !== "aborted") {
|
|
758
|
+
this._checkCompaction(event);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// ─── LLM_CALL_FAILED ────────────────────────────────────────────────────
|
|
762
|
+
async _onLlmCallFailed(event) {
|
|
763
|
+
const payload = event.payload;
|
|
764
|
+
const retryResult = this._scheduleRetry(event, payload.error, payload.retryable);
|
|
765
|
+
if (retryResult === "scheduled")
|
|
766
|
+
return;
|
|
767
|
+
const attempt = this._attemptCount(event.event_id);
|
|
768
|
+
// Emit turn end before completion
|
|
769
|
+
this._emit({
|
|
770
|
+
actor_id: "coder_agent",
|
|
771
|
+
type: "AGENT_TURN_END",
|
|
772
|
+
payload: { tool_calls_count: 0 },
|
|
773
|
+
caused_by: event.event_id,
|
|
774
|
+
});
|
|
775
|
+
this._emit({
|
|
776
|
+
actor_id: "coder_agent",
|
|
777
|
+
type: "AGENT_TURN_COMPLETED",
|
|
778
|
+
payload: {
|
|
779
|
+
reason: "error",
|
|
780
|
+
error_message: retryResult === "max_attempts"
|
|
781
|
+
? `Max retries (${this.retryPolicy.maxAttempts}) exceeded: ${payload.error}`
|
|
782
|
+
: retryResult === "backoff_exhausted"
|
|
783
|
+
? `Retry backoff exhausted: ${payload.error}`
|
|
784
|
+
: payload.error,
|
|
785
|
+
},
|
|
786
|
+
caused_by: event.event_id,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
_scheduleRetryForCompletedError(event) {
|
|
790
|
+
if (this.config.retryAssistantErrorCompletions === false)
|
|
791
|
+
return false;
|
|
792
|
+
const payload = event.payload;
|
|
793
|
+
if (payload.reason !== "error")
|
|
794
|
+
return false;
|
|
795
|
+
if (this._errorAlreadyHandledByLlmFailure(event))
|
|
796
|
+
return false;
|
|
797
|
+
const errorMessage = payload.error_message ?? "Agent turn completed with error";
|
|
798
|
+
return this._scheduleRetry(event, errorMessage, this.retryPolicy.isRetryable({ message: errorMessage })) === "scheduled";
|
|
799
|
+
}
|
|
800
|
+
_errorAlreadyHandledByLlmFailure(event) {
|
|
801
|
+
const causedBy = event.caused_by ? this.config.store.get(event.caused_by) : undefined;
|
|
802
|
+
return causedBy?.type === "LLM_CALL_FAILED";
|
|
803
|
+
}
|
|
804
|
+
_scheduleRetry(event, error, retryable) {
|
|
805
|
+
const attempt = this._attemptCount(event.event_id);
|
|
806
|
+
if (!retryable || attempt >= this.retryPolicy.maxAttempts) {
|
|
807
|
+
return retryable ? "max_attempts" : "not_retryable";
|
|
808
|
+
}
|
|
809
|
+
const nextAttempt = attempt + 1;
|
|
810
|
+
const delayMs = this.retryPolicy.nextDelayMs(nextAttempt);
|
|
811
|
+
if (delayMs === null) {
|
|
812
|
+
return "backoff_exhausted";
|
|
813
|
+
}
|
|
814
|
+
const retryScheduled = this._emit({
|
|
815
|
+
actor_id: "runtime",
|
|
816
|
+
type: "RETRY_SCHEDULED",
|
|
817
|
+
payload: {
|
|
818
|
+
attempt: nextAttempt,
|
|
819
|
+
max_attempts: this.retryPolicy.maxAttempts,
|
|
820
|
+
delay_ms: delayMs,
|
|
821
|
+
error_message: error,
|
|
822
|
+
},
|
|
823
|
+
caused_by: event.event_id,
|
|
824
|
+
});
|
|
825
|
+
const timeout = setTimeout(() => {
|
|
826
|
+
this.retryTimers.delete(retryScheduled.event_id);
|
|
827
|
+
if (!this._shouldInterrupt()) {
|
|
828
|
+
this._emit({
|
|
829
|
+
actor_id: "coder_agent",
|
|
830
|
+
type: "AGENT_TURN_REQUESTED",
|
|
831
|
+
payload: { reason: "retry", retry_attempt: nextAttempt },
|
|
832
|
+
caused_by: retryScheduled.event_id,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}, delayMs);
|
|
836
|
+
this.retryTimers.set(retryScheduled.event_id, {
|
|
837
|
+
timeout,
|
|
838
|
+
attempt: nextAttempt,
|
|
839
|
+
errorMessage: error,
|
|
840
|
+
});
|
|
841
|
+
return "scheduled";
|
|
842
|
+
}
|
|
843
|
+
/** Walk causal chain back to find the highest retry_attempt seen. */
|
|
844
|
+
_attemptCount(eventId) {
|
|
845
|
+
const chain = this.config.store.getCausalChain(eventId);
|
|
846
|
+
let attempt = 0;
|
|
847
|
+
for (const e of chain) {
|
|
848
|
+
if (e.type === "AGENT_TURN_REQUESTED") {
|
|
849
|
+
const p = e.payload;
|
|
850
|
+
if (p.retry_attempt !== undefined && p.retry_attempt > attempt) {
|
|
851
|
+
attempt = p.retry_attempt;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return attempt;
|
|
856
|
+
}
|
|
857
|
+
// ─── USER_APPROVAL ──────────────────────────────────────────────────────
|
|
858
|
+
async _onUserApproval(event) {
|
|
859
|
+
const payload = event.payload;
|
|
860
|
+
const pending = this._pendingApprovals.get(payload.intent_event_id);
|
|
861
|
+
if (pending) {
|
|
862
|
+
this._pendingApprovals.delete(payload.intent_event_id);
|
|
863
|
+
pending.resolve(true);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// ─── USER_REJECTION ─────────────────────────────────────────────────────
|
|
867
|
+
async _onUserRejection(event) {
|
|
868
|
+
const payload = event.payload;
|
|
869
|
+
const pending = this._pendingApprovals.get(payload.intent_event_id);
|
|
870
|
+
if (pending) {
|
|
871
|
+
this._pendingApprovals.delete(payload.intent_event_id);
|
|
872
|
+
pending.resolve(false);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// ─── COMPACTION ─────────────────────────────────────────────────────────
|
|
876
|
+
async _onCompactionRequested(event) {
|
|
877
|
+
if (this.compactionAbort)
|
|
878
|
+
return; // Already running
|
|
879
|
+
const payload = event.payload;
|
|
880
|
+
const abortController = new AbortController();
|
|
881
|
+
this.compactionAbort = abortController;
|
|
882
|
+
this._emit({
|
|
883
|
+
actor_id: "compactor",
|
|
884
|
+
type: "COMPACTION_START",
|
|
885
|
+
payload: { token_count: payload.token_count },
|
|
886
|
+
caused_by: event.event_id,
|
|
887
|
+
});
|
|
888
|
+
try {
|
|
889
|
+
const outcome = await this.compactionPolicy.compact(payload.reason, abortController.signal);
|
|
890
|
+
this._emit({
|
|
891
|
+
actor_id: "compactor",
|
|
892
|
+
type: "COMPACTION_END",
|
|
893
|
+
payload: {
|
|
894
|
+
summary: outcome.summary,
|
|
895
|
+
first_kept_event_id: outcome.first_kept_event_id,
|
|
896
|
+
tokens_before: outcome.tokens_before,
|
|
897
|
+
tokens_after: outcome.tokens_after,
|
|
898
|
+
},
|
|
899
|
+
caused_by: event.event_id,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
904
|
+
if (abortController.signal.aborted) {
|
|
905
|
+
this._emit({
|
|
906
|
+
actor_id: "compactor",
|
|
907
|
+
type: "COMPACTION_ABORTED",
|
|
908
|
+
payload: {
|
|
909
|
+
reason: "user_cancelled",
|
|
910
|
+
message: msg,
|
|
911
|
+
token_count: payload.token_count,
|
|
912
|
+
},
|
|
913
|
+
caused_by: event.event_id,
|
|
914
|
+
});
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
this._emit({
|
|
918
|
+
actor_id: "compactor",
|
|
919
|
+
type: "COMPACTION_END",
|
|
920
|
+
payload: {
|
|
921
|
+
summary: `Compaction failed: ${msg}`,
|
|
922
|
+
first_kept_event_id: "",
|
|
923
|
+
tokens_before: 0,
|
|
924
|
+
},
|
|
925
|
+
caused_by: event.event_id,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
this.compactionAbort = undefined;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Check compaction policy after a non-error turn. Emits COMPACTION_REQUESTED if
|
|
934
|
+
* threshold or overflow conditions are met.
|
|
935
|
+
*/
|
|
936
|
+
_checkCompaction(turnCompletedEvent) {
|
|
937
|
+
const policy = this.compactionPolicy;
|
|
938
|
+
// Walk the causal chain to find the AGENT_MESSAGE_END that produced this turn completion.
|
|
939
|
+
// This avoids mutable state (_lastAssistantMessageEvent) that races with synchronous event dispatch.
|
|
940
|
+
const lastAssistant = this._findLastAssistantMessage(turnCompletedEvent.event_id);
|
|
941
|
+
// Overflow takes priority — when it fires we skip the threshold check entirely.
|
|
942
|
+
// This prevents both "overflow" and "threshold" COMPACTION_REQUESTED events from
|
|
943
|
+
// being emitted in the same turn (overflow implies context is over threshold too).
|
|
944
|
+
if (policy.isOverflow(lastAssistant)) {
|
|
945
|
+
const tokens = policy.estimateContextTokens();
|
|
946
|
+
this._emit({
|
|
947
|
+
actor_id: "runtime",
|
|
948
|
+
type: "COMPACTION_REQUESTED",
|
|
949
|
+
payload: { reason: "overflow", token_count: tokens },
|
|
950
|
+
caused_by: turnCompletedEvent.event_id,
|
|
951
|
+
});
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const tokens = policy.estimateContextTokens();
|
|
955
|
+
const window = policy.contextWindow();
|
|
956
|
+
if (window > 0 && window !== Number.MAX_SAFE_INTEGER && tokens > window * policy.threshold()) {
|
|
957
|
+
this._emit({
|
|
958
|
+
actor_id: "runtime",
|
|
959
|
+
type: "COMPACTION_REQUESTED",
|
|
960
|
+
payload: { reason: "threshold", token_count: tokens },
|
|
961
|
+
caused_by: turnCompletedEvent.event_id,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Walk causal chain back from the given event to find the most recent AGENT_MESSAGE_END.
|
|
967
|
+
* Replaces the mutable _lastAssistantMessageEvent field which races with synchronous dispatch.
|
|
968
|
+
*/
|
|
969
|
+
_findLastAssistantMessage(eventId) {
|
|
970
|
+
const chain = this.config.store.getCausalChain(eventId);
|
|
971
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
972
|
+
if (chain[i].type === "AGENT_MESSAGE_END")
|
|
973
|
+
return chain[i];
|
|
974
|
+
}
|
|
975
|
+
return undefined;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function stringifyToolUpdate(partial) {
|
|
979
|
+
const text = partial.content
|
|
980
|
+
.filter((block) => {
|
|
981
|
+
return block.type === "text" && "text" in block && typeof block.text === "string";
|
|
982
|
+
})
|
|
983
|
+
.map((block) => block.text)
|
|
984
|
+
.join("\n");
|
|
985
|
+
if (text)
|
|
986
|
+
return text;
|
|
987
|
+
if (partial.content.length > 0)
|
|
988
|
+
return JSON.stringify(partial.content);
|
|
989
|
+
if (partial.details !== undefined)
|
|
990
|
+
return JSON.stringify(partial.details);
|
|
991
|
+
return "";
|
|
992
|
+
}
|