@stigmer/runner 3.15.0 → 3.15.2-dev.20260913223433
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 +2 -4
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +14 -0
- package/dist/__test-utils__/execution-record-fixture.js +4 -0
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/__test-utils__/file-review-projection.d.ts +40 -0
- package/dist/__test-utils__/file-review-projection.js +136 -0
- package/dist/__test-utils__/file-review-projection.js.map +1 -0
- package/dist/__test-utils__/harness-contract/types.d.ts +23 -1
- package/dist/__test-utils__/harness-contract/types.js +15 -0
- package/dist/__test-utils__/harness-contract/types.js.map +1 -1
- package/dist/__test-utils__/hermetic-activity.d.ts +44 -12
- package/dist/__test-utils__/hermetic-activity.js +52 -14
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- package/dist/__test-utils__/model-registry-fixture.d.ts +32 -7
- package/dist/__test-utils__/model-registry-fixture.js +31 -7
- package/dist/__test-utils__/model-registry-fixture.js.map +1 -1
- package/dist/__test-utils__/turn-input-fixture.d.ts +1 -1
- package/dist/__test-utils__/turn-input-fixture.js +1 -1
- package/dist/__test-utils__/turn-input-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +2 -2
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +45 -3
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +0 -9
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +2 -41
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/adapter.d.ts +7 -5
- package/dist/activities/execute-cursor/adapter.js +7 -5
- package/dist/activities/execute-cursor/adapter.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +5 -2
- package/dist/activities/execute-cursor/approval-policy.js +5 -2
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +16 -8
- package/dist/activities/execute-cursor/approval-state.js +6 -3
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/cas-observations.d.ts +14 -2
- package/dist/activities/execute-cursor/cas-observations.js +19 -2
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -1
- package/dist/activities/execute-cursor/cursor-capabilities.d.ts +8 -4
- package/dist/activities/execute-cursor/cursor-capabilities.js +21 -4
- package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +12 -20
- package/dist/activities/execute-cursor/message-translator.js +19 -32
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +48 -58
- package/dist/activities/execute-cursor/prompt-builder.js +76 -95
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +27 -55
- package/dist/activities/execute-cursor/turn-boundary.js +30 -90
- package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
- package/dist/activities/execute-cursor/turn-settle.d.ts +11 -7
- package/dist/activities/execute-cursor/turn-settle.js +37 -49
- package/dist/activities/execute-cursor/turn-settle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-setup.d.ts +16 -24
- package/dist/activities/execute-cursor/turn-setup.js +29 -59
- package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.d.ts +3 -8
- package/dist/activities/execute-cursor/turn-stream.js +9 -22
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-cursor/turn.js +5 -1
- package/dist/activities/execute-cursor/turn.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.d.ts +93 -0
- package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js +348 -0
- package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +170 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +225 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.d.ts +24 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js +41 -0
- package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.d.ts +54 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js +72 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +187 -23
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +238 -27
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
- package/dist/activities/execute-deep-agent/adapter.d.ts +41 -0
- package/dist/activities/execute-deep-agent/adapter.js +78 -0
- package/dist/activities/execute-deep-agent/adapter.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +8 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +9 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -1
- package/dist/activities/execute-deep-agent/deep-agent-capabilities.d.ts +31 -0
- package/dist/activities/execute-deep-agent/deep-agent-capabilities.js +40 -0
- package/dist/activities/execute-deep-agent/deep-agent-capabilities.js.map +1 -0
- package/dist/activities/execute-deep-agent/execution-state.d.ts +4 -11
- package/dist/activities/execute-deep-agent/execution-state.js +4 -12
- package/dist/activities/execute-deep-agent/execution-state.js.map +1 -1
- package/dist/activities/execute-deep-agent/hitl.d.ts +49 -47
- package/dist/activities/execute-deep-agent/hitl.js +77 -103
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +51 -23
- package/dist/activities/execute-deep-agent/prompt-builder.js +115 -76
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +18 -32
- package/dist/activities/execute-deep-agent/status-builder-shared.js +10 -65
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +10 -8
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +12 -14
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-tracker.d.ts +36 -13
- package/dist/activities/execute-deep-agent/subagent-tracker.js +76 -39
- package/dist/activities/execute-deep-agent/subagent-tracker.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -28
- package/dist/activities/execute-deep-agent/subagent-transformer.js +29 -82
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +3 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js +2 -2
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.d.ts +42 -0
- package/dist/activities/execute-deep-agent/turn-settle.js +146 -0
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -0
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +212 -0
- package/dist/activities/execute-deep-agent/turn-setup.js +430 -0
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -0
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +97 -0
- package/dist/activities/execute-deep-agent/turn-stream.js +242 -0
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -0
- package/dist/activities/execute-deep-agent/turn.d.ts +42 -0
- package/dist/activities/execute-deep-agent/turn.js +116 -0
- package/dist/activities/execute-deep-agent/turn.js.map +1 -0
- package/dist/activities/execute-deep-agent/v3-event-recorder.d.ts +6 -5
- package/dist/activities/execute-deep-agent/v3-event-recorder.js +6 -5
- package/dist/activities/execute-deep-agent/v3-event-recorder.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-events.d.ts +1 -1
- package/dist/activities/execute-deep-agent/v3-events.js +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +60 -17
- package/dist/activities/execute-deep-agent/v3-status-builder.js +72 -57
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/harness/approval-decisions.d.ts +61 -0
- package/dist/harness/approval-decisions.js +102 -0
- package/dist/harness/approval-decisions.js.map +1 -0
- package/dist/harness/capabilities.d.ts +16 -5
- package/dist/harness/capabilities.js +9 -0
- package/dist/harness/capabilities.js.map +1 -1
- package/dist/harness/capture.d.ts +144 -0
- package/dist/harness/capture.js +219 -0
- package/dist/harness/capture.js.map +1 -0
- package/dist/harness/persist-chokepoint.d.ts +19 -4
- package/dist/harness/persist-chokepoint.js +17 -4
- package/dist/harness/persist-chokepoint.js.map +1 -1
- package/dist/harness/run-turn.js +115 -29
- package/dist/harness/run-turn.js.map +1 -1
- package/dist/harness/terminal-table.d.ts +40 -11
- package/dist/harness/terminal-table.js +58 -19
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/turn-context.d.ts +141 -76
- package/dist/harness/turn-context.js +194 -99
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/harness/types.d.ts +65 -12
- package/dist/harness/types.js +8 -2
- package/dist/harness/types.js.map +1 -1
- package/dist/harness-adapters.d.ts +12 -8
- package/dist/harness-adapters.js +17 -9
- package/dist/harness-adapters.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +1 -1
- package/dist/middleware/approval-gate.js +1 -1
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +39 -0
- package/dist/middleware/{cost-cap.js → cost-advisory.js} +37 -55
- package/dist/middleware/cost-advisory.js.map +1 -0
- package/dist/middleware/index.d.ts +23 -12
- package/dist/middleware/index.js +26 -18
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +3 -2
- package/dist/runner-manager.js +1 -5
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.js +1 -5
- package/dist/runner.js.map +1 -1
- package/dist/shared/attachment-resolver.d.ts +41 -18
- package/dist/shared/attachment-resolver.js +178 -46
- package/dist/shared/attachment-resolver.js.map +1 -1
- package/dist/shared/attachment-zip.d.ts +85 -0
- package/dist/shared/attachment-zip.js +185 -0
- package/dist/shared/attachment-zip.js.map +1 -0
- package/dist/shared/connect-backfill.js +5 -4
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/cost-guard.d.ts +3 -3
- package/dist/shared/cost-guard.js +3 -3
- package/dist/shared/execution-status-writer.d.ts +5 -5
- package/dist/shared/execution-status-writer.js +5 -5
- package/dist/shared/extract-structured-output.d.ts +8 -7
- package/dist/shared/extract-structured-output.js +8 -7
- package/dist/shared/extract-structured-output.js.map +1 -1
- package/dist/shared/filereview/cas-progress.d.ts +1 -18
- package/dist/shared/filereview/cas-progress.js +4 -0
- package/dist/shared/filereview/cas-progress.js.map +1 -1
- package/dist/shared/filereview/cas-touched.d.ts +56 -0
- package/dist/shared/filereview/cas-touched.js +63 -0
- package/dist/shared/filereview/cas-touched.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +2 -1
- package/dist/shared/filereview/index.js +2 -1
- package/dist/shared/filereview/index.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +4 -5
- package/dist/shared/mcp-resolver.js +4 -5
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/persist-decision.d.ts +3 -4
- package/dist/shared/persist-decision.js +3 -4
- package/dist/shared/persist-decision.js.map +1 -1
- package/dist/shared/plan-mode-permissions.d.ts +2 -2
- package/dist/shared/plan-mode-permissions.js +2 -2
- package/dist/shared/plan-mode-prompt.d.ts +1 -1
- package/dist/shared/plan-mode-prompt.js +1 -1
- package/dist/shared/prompt-sections.d.ts +163 -0
- package/dist/shared/prompt-sections.js +164 -0
- package/dist/shared/prompt-sections.js.map +1 -0
- package/dist/shared/skill-resolver.d.ts +4 -2
- package/dist/shared/skill-resolver.js +4 -2
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +32 -0
- package/dist/shared/tool-rounds.js +43 -0
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/tool-row.d.ts +34 -0
- package/dist/shared/tool-row.js +52 -0
- package/dist/shared/tool-row.js.map +1 -1
- package/dist/shared/workspace/session-provision.d.ts +3 -3
- package/dist/shared/workspace/session-provision.js +3 -3
- package/dist/shared/workspace/stigmer-link.d.ts +3 -3
- package/dist/shared/workspace/stigmer-link.js +3 -3
- package/dist/shared/workspace/writeback-coordinator.d.ts +15 -22
- package/dist/shared/workspace/writeback-coordinator.js +16 -53
- package/dist/shared/workspace/writeback-coordinator.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/package.json +4 -4
- package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +18 -2
- package/src/__test-utils__/file-review-projection.ts +156 -0
- package/src/__test-utils__/git-workspace-fixture.ts +58 -0
- package/src/__test-utils__/harness-boot-order-child.ts +7 -2
- package/src/__test-utils__/harness-contract/contract.ts +33 -20
- package/src/__test-utils__/harness-contract/recording-sink.ts +11 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +328 -10
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +38 -6
- package/src/__test-utils__/harness-contract/types.ts +27 -3
- package/src/__test-utils__/hermetic-activity.ts +52 -14
- package/src/__test-utils__/model-registry-fixture.ts +32 -7
- package/src/__test-utils__/module-specifiers.ts +24 -0
- package/src/__test-utils__/turn-input-fixture.ts +2 -2
- package/src/__tests__/harness-boot-order.test.ts +9 -2
- package/src/activities/call-agent.ts +2 -2
- package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +47 -3
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +2 -48
- package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +52 -27
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +25 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.everything.prompt.md +166 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.enhanced.plan-mode.prompt.md +186 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.recovery.prompt.md +191 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.hitl.reinvocation.prompt.md +13 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.prefixed.prompt.md +32 -0
- package/src/activities/execute-cursor/__tests__/goldens/prompt.resumed.raw.prompt.md +1 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +4 -0
- package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +24 -4
- package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +6 -5
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +9 -0
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +11 -8
- package/src/activities/execute-cursor/__tests__/prompt-goldens.test.ts +229 -0
- package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +3 -3
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +2 -2
- package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +78 -234
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +1 -26
- package/src/activities/execute-cursor/adapter.ts +7 -5
- package/src/activities/execute-cursor/approval-policy.ts +5 -2
- package/src/activities/execute-cursor/approval-state.ts +16 -8
- package/src/activities/execute-cursor/cas-observations.ts +20 -2
- package/src/activities/execute-cursor/cursor-capabilities.ts +24 -5
- package/src/activities/execute-cursor/message-translator.ts +29 -41
- package/src/activities/execute-cursor/prompt-builder.ts +99 -157
- package/src/activities/execute-cursor/turn-boundary.ts +34 -140
- package/src/activities/execute-cursor/turn-settle.ts +37 -51
- package/src/activities/execute-cursor/turn-setup.ts +30 -70
- package/src/activities/execute-cursor/turn-stream.ts +8 -31
- package/src/activities/execute-cursor/turn.ts +5 -1
- package/src/activities/execute-deep-agent/__test-utils__/contract-subject.ts +391 -0
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +313 -0
- package/src/activities/execute-deep-agent/__test-utils__/hitl-script.ts +48 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model-module.ts +85 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +383 -36
- package/src/activities/execute-deep-agent/__tests__/adapter-graph-is-sdk-free.test.ts +76 -0
- package/src/activities/execute-deep-agent/__tests__/adapter-is-temporal-free.test.ts +47 -0
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +80 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +1 -1
- package/src/activities/execute-deep-agent/__tests__/execution-state-extended.test.ts +0 -2
- package/src/activities/execute-deep-agent/__tests__/execution-state.test.ts +0 -3
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +172 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +167 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.minimal.prompt.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +178 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +158 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +210 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn1.status.json +74 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn2.status.json +104 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve.turn2.status.json +78 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +138 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/gate.turn1.status.json +68 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +154 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +116 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +69 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +67 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/plain-turn.status.json +47 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/platform-stop.status.json +68 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +280 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/reject.turn2.status.json +77 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn2.status.json +99 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn3.status.json +109 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/skip.turn2.status.json +76 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.text-fallback.status.json +51 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tier2.status.json +51 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/structured-output.tool-strategy.status.json +56 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sub-agent-delegation.status.json +90 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/tool-call.status.json +70 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/worker-shutdown.status.json +71 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/harness-contract.test.ts +144 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-approve-all-lease.test.ts +184 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-round-trips.test.ts +203 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-sequential-gates.test.ts +170 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +175 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +206 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/plain-turn.test.ts +142 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/platform-stop.test.ts +126 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +139 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +122 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/structured-output.test.ts +236 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/sub-agent-delegation.test.ts +161 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/tool-call.test.ts +157 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/workspace-lock-timeout.test.ts +152 -0
- package/src/activities/execute-deep-agent/__tests__/hitl.test.ts +50 -122
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +8 -6
- package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +2 -2
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +35 -35
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +247 -0
- package/src/activities/execute-deep-agent/__tests__/scripted-model.test.ts +329 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +1 -20
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +78 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +1 -1
- package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +2 -2
- package/src/activities/execute-deep-agent/__tests__/subagent-tracker.test.ts +57 -31
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +19 -79
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +15 -15
- package/src/activities/execute-deep-agent/__tests__/summarization-verification.test.ts +1 -1
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +391 -0
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +343 -38
- package/src/activities/execute-deep-agent/__tests__/vision-input.test.ts +5 -5
- package/src/activities/execute-deep-agent/adapter.ts +94 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +11 -0
- package/src/activities/execute-deep-agent/deep-agent-capabilities.ts +43 -0
- package/src/activities/execute-deep-agent/execution-state.ts +4 -13
- package/src/activities/execute-deep-agent/hitl.ts +100 -123
- package/src/activities/execute-deep-agent/prompt-builder.ts +141 -115
- package/src/activities/execute-deep-agent/status-builder-shared.ts +21 -84
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +13 -19
- package/src/activities/execute-deep-agent/subagent-tracker.ts +77 -42
- package/src/activities/execute-deep-agent/subagent-transformer.ts +46 -105
- package/src/activities/execute-deep-agent/subagent-wiring.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +164 -0
- package/src/activities/execute-deep-agent/turn-setup.ts +541 -0
- package/src/activities/execute-deep-agent/turn-stream.ts +313 -0
- package/src/activities/execute-deep-agent/turn.ts +131 -0
- package/src/activities/execute-deep-agent/v3-event-recorder.ts +6 -5
- package/src/activities/execute-deep-agent/v3-events.ts +1 -1
- package/src/activities/execute-deep-agent/v3-status-builder.ts +84 -63
- package/src/harness/__tests__/approval-decisions.test.ts +116 -0
- package/src/harness/__tests__/capture.test.ts +268 -0
- package/src/harness/__tests__/mount-skills.test.ts +169 -0
- package/src/harness/__tests__/persist-chokepoint.test.ts +38 -2
- package/src/harness/__tests__/run-turn-deterministic.test.ts +94 -0
- package/src/harness/__tests__/run-turn.test.ts +39 -23
- package/src/harness/__tests__/turn-attachments-vision.test.ts +80 -0
- package/src/harness/__tests__/turn-context.test.ts +73 -26
- package/src/harness/approval-decisions.ts +104 -0
- package/src/harness/capabilities.ts +16 -5
- package/src/harness/capture.ts +301 -0
- package/src/harness/persist-chokepoint.ts +24 -4
- package/src/harness/run-turn.ts +120 -29
- package/src/harness/terminal-table.ts +61 -20
- package/src/harness/turn-context.ts +208 -111
- package/src/harness/types.ts +66 -12
- package/src/harness-adapters.ts +17 -9
- package/src/middleware/__tests__/approval-gate.test.ts +1 -1
- package/src/middleware/__tests__/cost-advisory.test.ts +113 -0
- package/src/middleware/approval-gate.ts +2 -2
- package/src/middleware/{cost-cap.ts → cost-advisory.ts} +40 -70
- package/src/middleware/index.ts +29 -29
- package/src/middleware/types.ts +3 -2
- package/src/runner-manager.ts +0 -8
- package/src/runner.ts +0 -8
- package/src/shared/__tests__/attachment-naming.test.ts +6 -19
- package/src/shared/__tests__/attachment-resolver.test.ts +158 -8
- package/src/shared/__tests__/attachment-zip.test.ts +236 -0
- package/src/shared/__tests__/bedrock-adapter.test.ts +1 -1
- package/src/shared/__tests__/blueprint-resolver.test.ts +43 -0
- package/src/shared/__tests__/cost-guard.test.ts +3 -3
- package/src/shared/__tests__/prompt-sections.test.ts +167 -0
- package/src/shared/__tests__/secret-leak-scan.test.ts +2 -2
- package/src/{activities/execute-deep-agent → shared}/__tests__/stamp-flowed-rows.test.ts +33 -10
- package/src/shared/__tests__/vertex-adapter.test.ts +1 -1
- package/src/shared/attachment-resolver.ts +218 -55
- package/src/shared/attachment-zip.ts +281 -0
- package/src/shared/connect-backfill.ts +9 -11
- package/src/shared/cost-guard.ts +3 -3
- package/src/shared/execution-status-writer.ts +5 -5
- package/src/shared/extract-structured-output.ts +8 -7
- package/src/shared/filereview/__tests__/capture.test.ts +86 -4
- package/src/shared/filereview/__tests__/cas-progress.test.ts +2 -1
- package/src/shared/filereview/cas-progress.ts +5 -19
- package/src/shared/filereview/cas-touched.ts +89 -0
- package/src/shared/filereview/index.ts +4 -2
- package/src/shared/mcp-resolver.ts +4 -5
- package/src/shared/persist-decision.ts +3 -4
- package/src/shared/plan-mode-permissions.ts +2 -2
- package/src/shared/plan-mode-prompt.ts +1 -1
- package/src/shared/prompt-sections.ts +256 -0
- package/src/shared/skill-resolver.ts +4 -2
- package/src/shared/tool-rounds.ts +48 -0
- package/src/shared/tool-row.ts +61 -0
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +7 -22
- package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +20 -61
- package/src/shared/workspace/session-provision.ts +3 -3
- package/src/shared/workspace/stigmer-link.ts +3 -3
- package/src/shared/workspace/writeback-coordinator.ts +16 -64
- package/src/tools/index.ts +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +0 -142
- package/dist/activities/execute-cursor/capture-flow.js +0 -285
- package/dist/activities/execute-cursor/capture-flow.js.map +0 -1
- package/dist/activities/execute-cursor/command-provenance.d.ts +0 -48
- package/dist/activities/execute-cursor/command-provenance.js +0 -38
- package/dist/activities/execute-cursor/command-provenance.js.map +0 -1
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +0 -134
- package/dist/activities/execute-deep-agent/attachment-injector.js +0 -386
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +0 -1
- package/dist/activities/execute-deep-agent/command-provenance.d.ts +0 -61
- package/dist/activities/execute-deep-agent/command-provenance.js +0 -72
- package/dist/activities/execute-deep-agent/command-provenance.js.map +0 -1
- package/dist/activities/execute-deep-agent/environment.d.ts +0 -24
- package/dist/activities/execute-deep-agent/environment.js +0 -58
- package/dist/activities/execute-deep-agent/environment.js.map +0 -1
- package/dist/activities/execute-deep-agent/event-recorder.d.ts +0 -21
- package/dist/activities/execute-deep-agent/event-recorder.js +0 -67
- package/dist/activities/execute-deep-agent/event-recorder.js.map +0 -1
- package/dist/activities/execute-deep-agent/index.d.ts +0 -15
- package/dist/activities/execute-deep-agent/index.js +0 -918
- package/dist/activities/execute-deep-agent/index.js.map +0 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +0 -28
- package/dist/activities/execute-deep-agent/mcp-gate.js +0 -22
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +0 -1
- package/dist/activities/execute-deep-agent/post-stream.d.ts +0 -23
- package/dist/activities/execute-deep-agent/post-stream.js +0 -71
- package/dist/activities/execute-deep-agent/post-stream.js.map +0 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +0 -136
- package/dist/activities/execute-deep-agent/setup.js +0 -767
- package/dist/activities/execute-deep-agent/setup.js.map +0 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +0 -36
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +0 -56
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +0 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +0 -93
- package/dist/activities/execute-deep-agent/status-builder.js +0 -365
- package/dist/activities/execute-deep-agent/status-builder.js.map +0 -1
- package/dist/activities/execute-deep-agent/streaming-terminal.d.ts +0 -21
- package/dist/activities/execute-deep-agent/streaming-terminal.js +0 -80
- package/dist/activities/execute-deep-agent/streaming-terminal.js.map +0 -1
- package/dist/activities/execute-deep-agent/streaming-v3.d.ts +0 -13
- package/dist/activities/execute-deep-agent/streaming-v3.js +0 -174
- package/dist/activities/execute-deep-agent/streaming-v3.js.map +0 -1
- package/dist/activities/execute-deep-agent/streaming.d.ts +0 -81
- package/dist/activities/execute-deep-agent/streaming.js +0 -160
- package/dist/activities/execute-deep-agent/streaming.js.map +0 -1
- package/dist/middleware/cost-cap.d.ts +0 -22
- package/dist/middleware/cost-cap.js.map +0 -1
- package/dist/middleware/graceful-stop.d.ts +0 -17
- package/dist/middleware/graceful-stop.js +0 -63
- package/dist/middleware/graceful-stop.js.map +0 -1
- package/dist/shared/skill-writer.d.ts +0 -75
- package/dist/shared/skill-writer.js +0 -207
- package/dist/shared/skill-writer.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +0 -1032
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +0 -240
- package/src/activities/execute-cursor/__tests__/progress-substrate.test.ts +0 -169
- package/src/activities/execute-cursor/capture-flow.ts +0 -379
- package/src/activities/execute-cursor/command-provenance.ts +0 -73
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +0 -1161
- package/src/activities/execute-deep-agent/__tests__/command-provenance.test.ts +0 -252
- package/src/activities/execute-deep-agent/__tests__/environment.test.ts +0 -108
- package/src/activities/execute-deep-agent/__tests__/event-recorder.test.ts +0 -150
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +0 -226
- package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +0 -318
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +0 -351
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +0 -293
- package/src/activities/execute-deep-agent/__tests__/index.test.ts +0 -99
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +0 -44
- package/src/activities/execute-deep-agent/__tests__/post-stream.test.ts +0 -112
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +0 -358
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +0 -1933
- package/src/activities/execute-deep-agent/__tests__/streaming-terminal.test.ts +0 -40
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +0 -597
- package/src/activities/execute-deep-agent/__tests__/streaming.test.ts +0 -508
- package/src/activities/execute-deep-agent/attachment-injector.ts +0 -627
- package/src/activities/execute-deep-agent/command-provenance.ts +0 -102
- package/src/activities/execute-deep-agent/environment.ts +0 -76
- package/src/activities/execute-deep-agent/event-recorder.ts +0 -95
- package/src/activities/execute-deep-agent/index.ts +0 -1072
- package/src/activities/execute-deep-agent/mcp-gate.ts +0 -37
- package/src/activities/execute-deep-agent/post-stream.ts +0 -109
- package/src/activities/execute-deep-agent/setup.ts +0 -1081
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +0 -64
- package/src/activities/execute-deep-agent/status-builder.ts +0 -481
- package/src/activities/execute-deep-agent/streaming-terminal.ts +0 -106
- package/src/activities/execute-deep-agent/streaming-v3.ts +0 -277
- package/src/activities/execute-deep-agent/streaming.ts +0 -313
- package/src/middleware/__tests__/cost-cap.test.ts +0 -192
- package/src/middleware/__tests__/graceful-stop.test.ts +0 -105
- package/src/middleware/graceful-stop.ts +0 -86
- package/src/shared/__tests__/skill-writer.test.ts +0 -372
- package/src/shared/skill-writer.ts +0 -266
|
@@ -1,1933 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { create } from "@bufbuild/protobuf";
|
|
3
|
-
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
|
-
import {
|
|
5
|
-
AgentMessageSchema,
|
|
6
|
-
ToolCallSchema,
|
|
7
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
8
|
-
import {
|
|
9
|
-
ApprovalAction,
|
|
10
|
-
ExecutionPhase,
|
|
11
|
-
MessageType,
|
|
12
|
-
ToolCallStatus,
|
|
13
|
-
ToolKind,
|
|
14
|
-
TodoStatus,
|
|
15
|
-
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
16
|
-
import { StatusBuilder, type StreamEvent, type ApprovalPolicyProvider } from "../status-builder.js";
|
|
17
|
-
import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
|
|
18
|
-
|
|
19
|
-
function makeBuilder() {
|
|
20
|
-
const status = create(AgentExecutionStatusSchema, {});
|
|
21
|
-
return new StatusBuilder("exec-test", status);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function chatStreamEvent(
|
|
25
|
-
runId: string,
|
|
26
|
-
content: string | Record<string, unknown>[],
|
|
27
|
-
metadata?: Record<string, unknown>,
|
|
28
|
-
): StreamEvent {
|
|
29
|
-
return {
|
|
30
|
-
event: "on_chat_model_stream",
|
|
31
|
-
name: "ChatAnthropic",
|
|
32
|
-
run_id: runId,
|
|
33
|
-
data: { chunk: { content } },
|
|
34
|
-
metadata,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function chatEndEvent(
|
|
39
|
-
runId: string,
|
|
40
|
-
usageMetadata?: Record<string, unknown>,
|
|
41
|
-
): StreamEvent {
|
|
42
|
-
return {
|
|
43
|
-
event: "on_chat_model_end",
|
|
44
|
-
name: "ChatAnthropic",
|
|
45
|
-
run_id: runId,
|
|
46
|
-
data: {
|
|
47
|
-
output: {
|
|
48
|
-
content: "final text",
|
|
49
|
-
usage_metadata: usageMetadata,
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function toolStartEvent(
|
|
56
|
-
runId: string,
|
|
57
|
-
toolName: string,
|
|
58
|
-
input?: Record<string, unknown>,
|
|
59
|
-
): StreamEvent {
|
|
60
|
-
return {
|
|
61
|
-
event: "on_tool_start",
|
|
62
|
-
name: toolName,
|
|
63
|
-
run_id: runId,
|
|
64
|
-
data: { input: input ?? {} },
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function toolEndEvent(
|
|
69
|
-
runId: string,
|
|
70
|
-
output: unknown,
|
|
71
|
-
): StreamEvent {
|
|
72
|
-
return {
|
|
73
|
-
event: "on_tool_end",
|
|
74
|
-
name: "some_tool",
|
|
75
|
-
run_id: runId,
|
|
76
|
-
data: { output },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
describe("StatusBuilder", () => {
|
|
81
|
-
describe("initialization", () => {
|
|
82
|
-
it("sets phase to IN_PROGRESS", () => {
|
|
83
|
-
const sb = makeBuilder();
|
|
84
|
-
expect(sb.currentStatus.phase).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("sets startedAt timestamp", () => {
|
|
88
|
-
const sb = makeBuilder();
|
|
89
|
-
expect(sb.currentStatus.startedAt).toBeTruthy();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("starts with forceNextUpdate = false", () => {
|
|
93
|
-
expect(makeBuilder().forceNextUpdate).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe("on_chat_model_stream — text tokens", () => {
|
|
98
|
-
it("creates an AI message and appends text", () => {
|
|
99
|
-
const sb = makeBuilder();
|
|
100
|
-
sb.processEvent(chatStreamEvent("run-1", "Hello"));
|
|
101
|
-
sb.processEvent(chatStreamEvent("run-1", " world"));
|
|
102
|
-
|
|
103
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
104
|
-
const msg = sb.currentStatus.messages[0];
|
|
105
|
-
expect(msg.type).toBe(MessageType.MESSAGE_AI);
|
|
106
|
-
expect(msg.content).toBe("Hello world");
|
|
107
|
-
expect(msg.isStreaming).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("handles content block array with text blocks", () => {
|
|
111
|
-
const sb = makeBuilder();
|
|
112
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
113
|
-
{ type: "text", text: "block text" },
|
|
114
|
-
]));
|
|
115
|
-
|
|
116
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
117
|
-
expect(sb.currentStatus.messages[0].content).toBe("block text");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("ignores empty string content", () => {
|
|
121
|
-
const sb = makeBuilder();
|
|
122
|
-
sb.processEvent(chatStreamEvent("run-1", ""));
|
|
123
|
-
|
|
124
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("ignores events with no chunk data", () => {
|
|
128
|
-
const sb = makeBuilder();
|
|
129
|
-
sb.processEvent({
|
|
130
|
-
event: "on_chat_model_stream",
|
|
131
|
-
run_id: "run-1",
|
|
132
|
-
data: {},
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("on_chat_model_stream — thinking blocks", () => {
|
|
140
|
-
it("creates a THINKING message for thinking content", () => {
|
|
141
|
-
const sb = makeBuilder();
|
|
142
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
143
|
-
{ type: "thinking", thinking: "Let me reason..." },
|
|
144
|
-
]));
|
|
145
|
-
|
|
146
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
147
|
-
const msg = sb.currentStatus.messages[0];
|
|
148
|
-
expect(msg.type).toBe(MessageType.MESSAGE_THINKING);
|
|
149
|
-
expect(msg.content).toBe("Let me reason...");
|
|
150
|
-
expect(msg.isStreaming).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("appends to existing thinking message in same namespace", () => {
|
|
154
|
-
const sb = makeBuilder();
|
|
155
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
156
|
-
{ type: "thinking", thinking: "Step 1. " },
|
|
157
|
-
]));
|
|
158
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
159
|
-
{ type: "thinking", thinking: "Step 2." },
|
|
160
|
-
]));
|
|
161
|
-
|
|
162
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
163
|
-
expect(sb.currentStatus.messages[0].content).toBe("Step 1. Step 2.");
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("handles mixed thinking and text in same event", () => {
|
|
167
|
-
const sb = makeBuilder();
|
|
168
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
169
|
-
{ type: "thinking", thinking: "hmm..." },
|
|
170
|
-
{ type: "text", text: "answer" },
|
|
171
|
-
]));
|
|
172
|
-
|
|
173
|
-
expect(sb.currentStatus.messages).toHaveLength(2);
|
|
174
|
-
expect(sb.currentStatus.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
|
|
175
|
-
expect(sb.currentStatus.messages[1].type).toBe(MessageType.MESSAGE_AI);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
describe("turn boundary detection", () => {
|
|
180
|
-
it("creates new message when run_id changes", () => {
|
|
181
|
-
const sb = makeBuilder();
|
|
182
|
-
sb.processEvent(chatStreamEvent("run-1", "First turn"));
|
|
183
|
-
sb.processEvent(chatStreamEvent("run-2", "Second turn"));
|
|
184
|
-
|
|
185
|
-
expect(sb.currentStatus.messages).toHaveLength(2);
|
|
186
|
-
expect(sb.currentStatus.messages[0].content).toBe("First turn");
|
|
187
|
-
expect(sb.currentStatus.messages[0].isStreaming).toBe(false);
|
|
188
|
-
expect(sb.currentStatus.messages[1].content).toBe("Second turn");
|
|
189
|
-
expect(sb.currentStatus.messages[1].isStreaming).toBe(true);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("reuses same message for same run_id", () => {
|
|
193
|
-
const sb = makeBuilder();
|
|
194
|
-
sb.processEvent(chatStreamEvent("run-1", "a"));
|
|
195
|
-
sb.processEvent(chatStreamEvent("run-1", "b"));
|
|
196
|
-
|
|
197
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
198
|
-
expect(sb.currentStatus.messages[0].content).toBe("ab");
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("on_chat_model_end", () => {
|
|
203
|
-
it("finalizes message streaming flag", () => {
|
|
204
|
-
const sb = makeBuilder();
|
|
205
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
206
|
-
expect(sb.currentStatus.messages[0].isStreaming).toBe(true);
|
|
207
|
-
|
|
208
|
-
sb.processEvent(chatEndEvent("run-1"));
|
|
209
|
-
expect(sb.currentStatus.messages[0].isStreaming).toBe(false);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("accumulates usage metadata", () => {
|
|
213
|
-
const sb = makeBuilder();
|
|
214
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
215
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
216
|
-
input_tokens: 100,
|
|
217
|
-
output_tokens: 50,
|
|
218
|
-
cache_read_input_tokens: 10,
|
|
219
|
-
cache_creation_input_tokens: 5,
|
|
220
|
-
}));
|
|
221
|
-
|
|
222
|
-
const usage = sb.currentStatus.streamingUsage;
|
|
223
|
-
expect(usage).toBeDefined();
|
|
224
|
-
expect(usage!.inputTokens).toBe(100n);
|
|
225
|
-
expect(usage!.outputTokens).toBe(50n);
|
|
226
|
-
expect(usage!.cacheReadTokens).toBe(10n);
|
|
227
|
-
expect(usage!.cacheWriteTokens).toBe(5n);
|
|
228
|
-
expect(usage!.totalTokens).toBe(165n);
|
|
229
|
-
expect(usage!.turnCount).toBe(1);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("accumulates across multiple turns", () => {
|
|
233
|
-
const sb = makeBuilder();
|
|
234
|
-
sb.processEvent(chatStreamEvent("run-1", "turn 1"));
|
|
235
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
236
|
-
input_tokens: 100,
|
|
237
|
-
output_tokens: 50,
|
|
238
|
-
}));
|
|
239
|
-
sb.processEvent(chatStreamEvent("run-2", "turn 2"));
|
|
240
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
241
|
-
input_tokens: 200,
|
|
242
|
-
output_tokens: 100,
|
|
243
|
-
}));
|
|
244
|
-
|
|
245
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
246
|
-
expect(usage.inputTokens).toBe(300n);
|
|
247
|
-
expect(usage.outputTokens).toBe(150n);
|
|
248
|
-
expect(usage.turnCount).toBe(2);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("on_tool_start", () => {
|
|
253
|
-
it("creates a RUNNING tool call on the current AI message", () => {
|
|
254
|
-
const sb = makeBuilder();
|
|
255
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll read a file"));
|
|
256
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read", { path: "/foo.ts" }));
|
|
257
|
-
|
|
258
|
-
const msg = sb.currentStatus.messages[0];
|
|
259
|
-
expect(msg.toolCalls).toHaveLength(1);
|
|
260
|
-
expect(msg.toolCalls[0].id).toBe("tool-run-1");
|
|
261
|
-
expect(msg.toolCalls[0].name).toBe("read");
|
|
262
|
-
expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
263
|
-
expect(msg.toolCalls[0].startedAt).toBeTruthy();
|
|
264
|
-
expect(msg.toolCalls[0].args).toEqual({ path: "/foo.ts" });
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("sets forceNextUpdate", () => {
|
|
268
|
-
const sb = makeBuilder();
|
|
269
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
270
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
271
|
-
|
|
272
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("creates AI message for tool-only turn when no prior AI message exists", () => {
|
|
276
|
-
const sb = makeBuilder();
|
|
277
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
278
|
-
|
|
279
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
280
|
-
expect(sb.currentStatus.messages[0].toolCalls).toHaveLength(1);
|
|
281
|
-
expect(sb.currentStatus.messages[0].toolCalls[0].name).toBe("read");
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe("on_tool_end", () => {
|
|
286
|
-
it("marks tool call as COMPLETED with result", () => {
|
|
287
|
-
const sb = makeBuilder();
|
|
288
|
-
sb.processEvent(chatStreamEvent("run-1", "reading file"));
|
|
289
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
290
|
-
sb.processEvent(toolEndEvent("tool-run-1", "file contents here"));
|
|
291
|
-
|
|
292
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
293
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
294
|
-
expect(tc.result).toBe("file contents here");
|
|
295
|
-
expect(tc.completedAt).toBeTruthy();
|
|
296
|
-
expect(tc.isStreaming).toBe(false);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("marks tool call as FAILED when output has error", () => {
|
|
300
|
-
const sb = makeBuilder();
|
|
301
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
302
|
-
sb.processEvent(toolStartEvent("tool-run-1", "write"));
|
|
303
|
-
sb.processEvent(toolEndEvent("tool-run-1", { error: "permission denied" }));
|
|
304
|
-
|
|
305
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
306
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
|
|
307
|
-
expect(tc.error).toBe("permission denied");
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it("stores the full result faithfully (no builder-level truncation)", () => {
|
|
311
|
-
// Size-bounding is owned by the persist chokepoint (offload + enforce),
|
|
312
|
-
// not the builder. The builder must reflect the stream verbatim so binary
|
|
313
|
-
// content (e.g. a screenshot's base64) survives intact for offload.
|
|
314
|
-
const sb = makeBuilder();
|
|
315
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
316
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
317
|
-
|
|
318
|
-
const longResult = "x".repeat(60_000);
|
|
319
|
-
sb.processEvent(toolEndEvent("tool-run-1", longResult));
|
|
320
|
-
|
|
321
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
322
|
-
expect(tc.result).toBe(longResult);
|
|
323
|
-
expect(tc.result).not.toContain("[truncated:");
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("sets forceNextUpdate", () => {
|
|
327
|
-
const sb = makeBuilder();
|
|
328
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
329
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
330
|
-
sb.clearForceFlag();
|
|
331
|
-
sb.processEvent(toolEndEvent("tool-run-1", "done"));
|
|
332
|
-
|
|
333
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it("ignores unknown tool run_id", () => {
|
|
337
|
-
const sb = makeBuilder();
|
|
338
|
-
sb.processEvent(toolEndEvent("unknown-run", "result"));
|
|
339
|
-
// Should not throw
|
|
340
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("handles object output with content field", () => {
|
|
344
|
-
const sb = makeBuilder();
|
|
345
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
346
|
-
sb.processEvent(toolStartEvent("tool-run-1", "shell"));
|
|
347
|
-
sb.processEvent(toolEndEvent("tool-run-1", { content: "stdout output" }));
|
|
348
|
-
|
|
349
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
350
|
-
expect(tc.result).toBe("stdout output");
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
describe("unknown events", () => {
|
|
355
|
-
it("silently ignores unhandled event types", () => {
|
|
356
|
-
const sb = makeBuilder();
|
|
357
|
-
sb.processEvent({
|
|
358
|
-
event: "on_chain_start",
|
|
359
|
-
run_id: "run-1",
|
|
360
|
-
data: {},
|
|
361
|
-
});
|
|
362
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
describe("clearForceFlag", () => {
|
|
367
|
-
it("resets forceNextUpdate to false", () => {
|
|
368
|
-
const sb = makeBuilder();
|
|
369
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
370
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read"));
|
|
371
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
372
|
-
|
|
373
|
-
sb.clearForceFlag();
|
|
374
|
-
expect(sb.forceNextUpdate).toBe(false);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe("addArtifact", () => {
|
|
379
|
-
it("appends a new artifact and sets forceNextUpdate", () => {
|
|
380
|
-
const sb = makeBuilder();
|
|
381
|
-
sb.clearForceFlag();
|
|
382
|
-
|
|
383
|
-
sb.addArtifact({
|
|
384
|
-
name: "report.txt",
|
|
385
|
-
sandboxPath: "workspace/report.txt",
|
|
386
|
-
kind: 0,
|
|
387
|
-
sizeBytes: 100n,
|
|
388
|
-
storageKey: "artifacts/exec-1/report.txt",
|
|
389
|
-
createdAt: "2026-05-19T12:00:00Z",
|
|
390
|
-
expiresAt: "",
|
|
391
|
-
entries: [],
|
|
392
|
-
contentHash: "abc123",
|
|
393
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
|
|
394
|
-
} as any);
|
|
395
|
-
|
|
396
|
-
expect(sb.currentStatus.artifacts).toHaveLength(1);
|
|
397
|
-
expect(sb.currentStatus.artifacts[0].name).toBe("report.txt");
|
|
398
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("deduplicates by sandboxPath — skips same contentHash", () => {
|
|
402
|
-
const sb = makeBuilder();
|
|
403
|
-
const artifact = {
|
|
404
|
-
name: "f.txt",
|
|
405
|
-
sandboxPath: "ws/f.txt",
|
|
406
|
-
kind: 0,
|
|
407
|
-
sizeBytes: 10n,
|
|
408
|
-
storageKey: "k",
|
|
409
|
-
createdAt: "",
|
|
410
|
-
expiresAt: "",
|
|
411
|
-
entries: [],
|
|
412
|
-
contentHash: "hash1",
|
|
413
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
|
|
414
|
-
} as any;
|
|
415
|
-
|
|
416
|
-
sb.addArtifact(artifact);
|
|
417
|
-
sb.clearForceFlag();
|
|
418
|
-
sb.addArtifact({ ...artifact });
|
|
419
|
-
|
|
420
|
-
expect(sb.currentStatus.artifacts).toHaveLength(1);
|
|
421
|
-
expect(sb.forceNextUpdate).toBe(false);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("replaces artifact when contentHash changes", () => {
|
|
425
|
-
const sb = makeBuilder();
|
|
426
|
-
const artifact = {
|
|
427
|
-
name: "f.txt",
|
|
428
|
-
sandboxPath: "ws/f.txt",
|
|
429
|
-
kind: 0,
|
|
430
|
-
sizeBytes: 10n,
|
|
431
|
-
storageKey: "k",
|
|
432
|
-
createdAt: "",
|
|
433
|
-
expiresAt: "",
|
|
434
|
-
entries: [],
|
|
435
|
-
contentHash: "hash1",
|
|
436
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.ExecutionArtifact",
|
|
437
|
-
} as any;
|
|
438
|
-
|
|
439
|
-
sb.addArtifact(artifact);
|
|
440
|
-
sb.clearForceFlag();
|
|
441
|
-
sb.addArtifact({ ...artifact, contentHash: "hash2", sizeBytes: 20n });
|
|
442
|
-
|
|
443
|
-
expect(sb.currentStatus.artifacts).toHaveLength(1);
|
|
444
|
-
expect(sb.currentStatus.artifacts[0].contentHash).toBe("hash2");
|
|
445
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
446
|
-
});
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
describe("addWriteBack", () => {
|
|
450
|
-
it("appends a new writeback entry and sets forceNextUpdate", () => {
|
|
451
|
-
const sb = makeBuilder();
|
|
452
|
-
sb.clearForceFlag();
|
|
453
|
-
|
|
454
|
-
sb.addWriteBack({
|
|
455
|
-
workspaceEntryName: "my-app",
|
|
456
|
-
branchName: "stigmer/a1b2c3d4",
|
|
457
|
-
baseBranch: "main",
|
|
458
|
-
commitSha: "abc",
|
|
459
|
-
pullRequestUrl: "",
|
|
460
|
-
pullRequestNumber: 0,
|
|
461
|
-
diffSummary: "1 file changed",
|
|
462
|
-
phase: 1,
|
|
463
|
-
error: "",
|
|
464
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
|
|
465
|
-
} as any);
|
|
466
|
-
|
|
467
|
-
expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(1);
|
|
468
|
-
expect(sb.forceNextUpdate).toBe(true);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
it("upserts by workspaceEntryName", () => {
|
|
472
|
-
const sb = makeBuilder();
|
|
473
|
-
|
|
474
|
-
sb.addWriteBack({
|
|
475
|
-
workspaceEntryName: "my-app",
|
|
476
|
-
branchName: "stigmer/a1b2",
|
|
477
|
-
baseBranch: "main",
|
|
478
|
-
commitSha: "sha1",
|
|
479
|
-
pullRequestUrl: "",
|
|
480
|
-
pullRequestNumber: 0,
|
|
481
|
-
diffSummary: "",
|
|
482
|
-
phase: 1,
|
|
483
|
-
error: "",
|
|
484
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
|
|
485
|
-
} as any);
|
|
486
|
-
|
|
487
|
-
sb.addWriteBack({
|
|
488
|
-
workspaceEntryName: "my-app",
|
|
489
|
-
branchName: "stigmer/a1b2",
|
|
490
|
-
baseBranch: "main",
|
|
491
|
-
commitSha: "sha2",
|
|
492
|
-
pullRequestUrl: "https://github.com/pr/1",
|
|
493
|
-
pullRequestNumber: 1,
|
|
494
|
-
diffSummary: "2 files changed",
|
|
495
|
-
phase: 3,
|
|
496
|
-
error: "",
|
|
497
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
|
|
498
|
-
} as any);
|
|
499
|
-
|
|
500
|
-
expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(1);
|
|
501
|
-
expect(sb.currentStatus.workspaceWriteBacks[0].commitSha).toBe("sha2");
|
|
502
|
-
expect(sb.currentStatus.workspaceWriteBacks[0].phase).toBe(3);
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
it("tracks multiple workspace entries independently", () => {
|
|
506
|
-
const sb = makeBuilder();
|
|
507
|
-
|
|
508
|
-
sb.addWriteBack({
|
|
509
|
-
workspaceEntryName: "frontend",
|
|
510
|
-
branchName: "stigmer/x",
|
|
511
|
-
baseBranch: "main",
|
|
512
|
-
commitSha: "a",
|
|
513
|
-
pullRequestUrl: "",
|
|
514
|
-
pullRequestNumber: 0,
|
|
515
|
-
diffSummary: "",
|
|
516
|
-
phase: 1,
|
|
517
|
-
error: "",
|
|
518
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
|
|
519
|
-
} as any);
|
|
520
|
-
|
|
521
|
-
sb.addWriteBack({
|
|
522
|
-
workspaceEntryName: "backend",
|
|
523
|
-
branchName: "stigmer/x",
|
|
524
|
-
baseBranch: "develop",
|
|
525
|
-
commitSha: "b",
|
|
526
|
-
pullRequestUrl: "",
|
|
527
|
-
pullRequestNumber: 0,
|
|
528
|
-
diffSummary: "",
|
|
529
|
-
phase: 2,
|
|
530
|
-
error: "",
|
|
531
|
-
$typeName: "ai.stigmer.agentic.agentexecution.v1.WorkspaceWriteBack",
|
|
532
|
-
} as any);
|
|
533
|
-
|
|
534
|
-
expect(sb.currentStatus.workspaceWriteBacks).toHaveLength(2);
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
539
|
-
// Approval provider integration
|
|
540
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
541
|
-
|
|
542
|
-
describe("approval provider integration", () => {
|
|
543
|
-
function makeApprovalProvider(
|
|
544
|
-
overrides: Partial<ApprovalPolicyProvider> = {},
|
|
545
|
-
): ApprovalPolicyProvider {
|
|
546
|
-
return {
|
|
547
|
-
policies: new Map(),
|
|
548
|
-
toolServerMap: new Map(),
|
|
549
|
-
globalBypass: false,
|
|
550
|
-
...overrides,
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function policyFor(
|
|
555
|
-
serverSlug: string,
|
|
556
|
-
toolName: string,
|
|
557
|
-
message = "Approve?",
|
|
558
|
-
): [string, MergedToolPolicy] {
|
|
559
|
-
return [
|
|
560
|
-
`${serverSlug}/${toolName}`,
|
|
561
|
-
{
|
|
562
|
-
toolName,
|
|
563
|
-
mcpServerSlug: serverSlug,
|
|
564
|
-
requiresApproval: true,
|
|
565
|
-
approvalMessage: message,
|
|
566
|
-
source: "classifier_default",
|
|
567
|
-
},
|
|
568
|
-
];
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
it("sets tool status to WAITING_APPROVAL when policy requires it", () => {
|
|
572
|
-
const sb = makeBuilder();
|
|
573
|
-
const [key, policy] = policyFor("github", "create_issue");
|
|
574
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
575
|
-
policies: new Map([[key, policy]]),
|
|
576
|
-
toolServerMap: new Map([["create_issue", "github"]]),
|
|
577
|
-
}));
|
|
578
|
-
|
|
579
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll create an issue"));
|
|
580
|
-
sb.processEvent(toolStartEvent("tool-1", "create_issue", { title: "Bug fix" }));
|
|
581
|
-
|
|
582
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
583
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
584
|
-
expect(tc.requiresApproval).toBe(true);
|
|
585
|
-
expect(tc.approvalMessage).toBe("Approve?");
|
|
586
|
-
expect(tc.mcpServerSlug).toBe("github");
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
it("sets phase to WAITING_FOR_APPROVAL when tool requires approval", () => {
|
|
590
|
-
const sb = makeBuilder();
|
|
591
|
-
const [key, policy] = policyFor("github", "push");
|
|
592
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
593
|
-
policies: new Map([[key, policy]]),
|
|
594
|
-
toolServerMap: new Map([["push", "github"]]),
|
|
595
|
-
}));
|
|
596
|
-
|
|
597
|
-
sb.processEvent(chatStreamEvent("run-1", "pushing"));
|
|
598
|
-
sb.processEvent(toolStartEvent("tool-1", "push"));
|
|
599
|
-
|
|
600
|
-
expect(sb.currentStatus.phase).toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("does not require approval under the global bypass (spec.auto_approve_all)", () => {
|
|
604
|
-
const sb = makeBuilder();
|
|
605
|
-
const [key, policy] = policyFor("github", "delete_repo");
|
|
606
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
607
|
-
policies: new Map([[key, policy]]),
|
|
608
|
-
toolServerMap: new Map([["delete_repo", "github"]]),
|
|
609
|
-
globalBypass: true,
|
|
610
|
-
}));
|
|
611
|
-
|
|
612
|
-
sb.processEvent(chatStreamEvent("run-1", "deleting"));
|
|
613
|
-
sb.processEvent(toolStartEvent("tool-1", "delete_repo"));
|
|
614
|
-
|
|
615
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
616
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
617
|
-
expect(tc.requiresApproval).toBe(false);
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it("sets mcpServerSlug even under the global bypass (spec.auto_approve_all)", () => {
|
|
621
|
-
const sb = makeBuilder();
|
|
622
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
623
|
-
toolServerMap: new Map([["echo", "test-mcp-server"]]),
|
|
624
|
-
globalBypass: true,
|
|
625
|
-
}));
|
|
626
|
-
|
|
627
|
-
sb.processEvent(chatStreamEvent("run-1", "echoing"));
|
|
628
|
-
sb.processEvent(toolStartEvent("tool-1", "echo", { input: "hello" }));
|
|
629
|
-
|
|
630
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
631
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
632
|
-
expect(tc.requiresApproval).toBe(false);
|
|
633
|
-
expect(tc.mcpServerSlug).toBe("test-mcp-server");
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
it("does not require approval for tools without policies", () => {
|
|
637
|
-
const sb = makeBuilder();
|
|
638
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
639
|
-
policies: new Map(),
|
|
640
|
-
toolServerMap: new Map([["read", "filesystem"]]),
|
|
641
|
-
}));
|
|
642
|
-
|
|
643
|
-
sb.processEvent(chatStreamEvent("run-1", "reading"));
|
|
644
|
-
sb.processEvent(toolStartEvent("tool-1", "read"));
|
|
645
|
-
|
|
646
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
647
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
648
|
-
expect(tc.mcpServerSlug).toBe("filesystem");
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it("does not require approval for tools not in toolServerMap", () => {
|
|
652
|
-
const sb = makeBuilder();
|
|
653
|
-
const [key, policy] = policyFor("github", "create_pr");
|
|
654
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
655
|
-
policies: new Map([[key, policy]]),
|
|
656
|
-
toolServerMap: new Map(),
|
|
657
|
-
}));
|
|
658
|
-
|
|
659
|
-
sb.processEvent(chatStreamEvent("run-1", "creating PR"));
|
|
660
|
-
sb.processEvent(toolStartEvent("tool-1", "create_pr"));
|
|
661
|
-
|
|
662
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
663
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it("resolves {{args.field}} placeholders in approval message", () => {
|
|
667
|
-
const sb = makeBuilder();
|
|
668
|
-
const [key, policy] = policyFor(
|
|
669
|
-
"github", "create_issue",
|
|
670
|
-
"Create issue '{{args.title}}' in {{args.repo}}?",
|
|
671
|
-
);
|
|
672
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
673
|
-
policies: new Map([[key, policy]]),
|
|
674
|
-
toolServerMap: new Map([["create_issue", "github"]]),
|
|
675
|
-
}));
|
|
676
|
-
|
|
677
|
-
sb.processEvent(chatStreamEvent("run-1", "creating issue"));
|
|
678
|
-
sb.processEvent(toolStartEvent("tool-1", "create_issue", {
|
|
679
|
-
title: "Fix crash",
|
|
680
|
-
repo: "stigmer/stigmer",
|
|
681
|
-
}));
|
|
682
|
-
|
|
683
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
684
|
-
expect(tc.approvalMessage).toBe("Create issue 'Fix crash' in stigmer/stigmer?");
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it("sets approvalRequestedAt when approval required", () => {
|
|
688
|
-
const sb = makeBuilder();
|
|
689
|
-
const [key, policy] = policyFor("db", "drop_table");
|
|
690
|
-
sb.setApprovalProvider(makeApprovalProvider({
|
|
691
|
-
policies: new Map([[key, policy]]),
|
|
692
|
-
toolServerMap: new Map([["drop_table", "db"]]),
|
|
693
|
-
}));
|
|
694
|
-
|
|
695
|
-
sb.processEvent(chatStreamEvent("run-1", "dropping"));
|
|
696
|
-
sb.processEvent(toolStartEvent("tool-1", "drop_table"));
|
|
697
|
-
|
|
698
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
699
|
-
expect(tc.approvalRequestedAt).toBeTruthy();
|
|
700
|
-
expect(tc.approvalRequestedAt).toContain("T");
|
|
701
|
-
});
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
705
|
-
// Args sanitization (argsPreview)
|
|
706
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
707
|
-
|
|
708
|
-
describe("args preview sanitization", () => {
|
|
709
|
-
function builderWithApproval(
|
|
710
|
-
toolName: string,
|
|
711
|
-
serverSlug: string,
|
|
712
|
-
): StatusBuilder {
|
|
713
|
-
const sb = makeBuilder();
|
|
714
|
-
sb.setApprovalProvider({
|
|
715
|
-
policies: new Map([
|
|
716
|
-
[`${serverSlug}/${toolName}`, {
|
|
717
|
-
toolName,
|
|
718
|
-
mcpServerSlug: serverSlug,
|
|
719
|
-
requiresApproval: true,
|
|
720
|
-
approvalMessage: "Approve?",
|
|
721
|
-
source: "classifier_default",
|
|
722
|
-
}],
|
|
723
|
-
]),
|
|
724
|
-
toolServerMap: new Map([[toolName, serverSlug]]),
|
|
725
|
-
globalBypass: false,
|
|
726
|
-
});
|
|
727
|
-
return sb;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
it("redacts sensitive arg keys", () => {
|
|
731
|
-
const sb = builderWithApproval("connect", "db");
|
|
732
|
-
sb.processEvent(chatStreamEvent("run-1", "connecting"));
|
|
733
|
-
sb.processEvent(toolStartEvent("tool-1", "connect", {
|
|
734
|
-
host: "localhost",
|
|
735
|
-
password: "super-secret",
|
|
736
|
-
token: "jwt-token",
|
|
737
|
-
}));
|
|
738
|
-
|
|
739
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
740
|
-
const preview = JSON.parse(tc.argsPreview);
|
|
741
|
-
expect(preview.host).toBe("localhost");
|
|
742
|
-
expect(preview.password).toBe("[REDACTED]");
|
|
743
|
-
expect(preview.token).toBe("[REDACTED]");
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it("redacts case-insensitively", () => {
|
|
747
|
-
const sb = builderWithApproval("call", "api");
|
|
748
|
-
sb.processEvent(chatStreamEvent("run-1", "calling"));
|
|
749
|
-
sb.processEvent(toolStartEvent("tool-1", "call", {
|
|
750
|
-
Authorization: "Bearer xyz",
|
|
751
|
-
API_KEY: "key-123",
|
|
752
|
-
}));
|
|
753
|
-
|
|
754
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
755
|
-
const preview = JSON.parse(tc.argsPreview);
|
|
756
|
-
expect(preview.Authorization).toBe("[REDACTED]");
|
|
757
|
-
expect(preview.API_KEY).toBe("[REDACTED]");
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
it("truncates long args preview", () => {
|
|
761
|
-
const sb = builderWithApproval("write", "fs");
|
|
762
|
-
sb.processEvent(chatStreamEvent("run-1", "writing"));
|
|
763
|
-
sb.processEvent(toolStartEvent("tool-1", "write", {
|
|
764
|
-
content: "x".repeat(1000),
|
|
765
|
-
}));
|
|
766
|
-
|
|
767
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
768
|
-
expect(tc.argsPreview.length).toBeLessThanOrEqual(501);
|
|
769
|
-
expect(tc.argsPreview.endsWith("…")).toBe(true);
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it("does not set argsPreview for non-approval tools", () => {
|
|
773
|
-
const sb = makeBuilder();
|
|
774
|
-
sb.processEvent(chatStreamEvent("run-1", "reading"));
|
|
775
|
-
sb.processEvent(toolStartEvent("tool-1", "read", { path: "/foo" }));
|
|
776
|
-
|
|
777
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
778
|
-
expect(tc.argsPreview).toBe("");
|
|
779
|
-
});
|
|
780
|
-
|
|
781
|
-
it("sets argsPreview as empty when args serialization fails", () => {
|
|
782
|
-
const sb = builderWithApproval("call", "api");
|
|
783
|
-
sb.processEvent(chatStreamEvent("run-1", "calling"));
|
|
784
|
-
|
|
785
|
-
const circular: Record<string, unknown> = {};
|
|
786
|
-
circular.self = circular;
|
|
787
|
-
sb.processEvent(toolStartEvent("tool-1", "call", circular));
|
|
788
|
-
|
|
789
|
-
const tc = sb.currentStatus.messages[0].toolCalls[0];
|
|
790
|
-
expect(tc.argsPreview).toBe("");
|
|
791
|
-
});
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
795
|
-
// Namespace extraction
|
|
796
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
797
|
-
|
|
798
|
-
describe("namespace extraction", () => {
|
|
799
|
-
it("extracts namespace from langgraph_checkpoint_ns", () => {
|
|
800
|
-
const sb = makeBuilder();
|
|
801
|
-
sb.processEvent({
|
|
802
|
-
event: "on_chat_model_stream",
|
|
803
|
-
name: "ChatAnthropic",
|
|
804
|
-
run_id: "run-ns-1",
|
|
805
|
-
data: { chunk: { content: "hello" } },
|
|
806
|
-
metadata: { langgraph_checkpoint_ns: "tools:task-123|agent_node:inner" },
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it("extracts namespace from checkpoint_ns fallback", () => {
|
|
813
|
-
const sb = makeBuilder();
|
|
814
|
-
sb.processEvent({
|
|
815
|
-
event: "on_chat_model_stream",
|
|
816
|
-
name: "ChatAnthropic",
|
|
817
|
-
run_id: "run-ns-2",
|
|
818
|
-
data: { chunk: { content: "hello" } },
|
|
819
|
-
metadata: { checkpoint_ns: "some-namespace" },
|
|
820
|
-
});
|
|
821
|
-
|
|
822
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
it("uses empty namespace when no metadata", () => {
|
|
826
|
-
const sb = makeBuilder();
|
|
827
|
-
sb.processEvent({
|
|
828
|
-
event: "on_chat_model_stream",
|
|
829
|
-
run_id: "run-ns-3",
|
|
830
|
-
data: { chunk: { content: "hello" } },
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
it("different namespaces create separate message trees", () => {
|
|
837
|
-
const sb = makeBuilder();
|
|
838
|
-
|
|
839
|
-
sb.processEvent({
|
|
840
|
-
event: "on_chat_model_stream",
|
|
841
|
-
name: "ChatAnthropic",
|
|
842
|
-
run_id: "main-run",
|
|
843
|
-
data: { chunk: { content: "main text" } },
|
|
844
|
-
metadata: {},
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
sb.processEvent({
|
|
848
|
-
event: "on_chat_model_stream",
|
|
849
|
-
name: "ChatAnthropic",
|
|
850
|
-
run_id: "sub-run",
|
|
851
|
-
data: { chunk: { content: "sub text" } },
|
|
852
|
-
metadata: { langgraph_checkpoint_ns: "tools:task-1|agent_node:inner" },
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
expect(sb.currentStatus.messages).toHaveLength(2);
|
|
856
|
-
expect(sb.currentStatus.messages[0].content).toBe("main text");
|
|
857
|
-
expect(sb.currentStatus.messages[1].content).toBe("sub text");
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
it("same namespace reuses message within the same run_id", () => {
|
|
861
|
-
const sb = makeBuilder();
|
|
862
|
-
const ns = "tools:task-1|agent_node:inner";
|
|
863
|
-
|
|
864
|
-
sb.processEvent({
|
|
865
|
-
event: "on_chat_model_stream",
|
|
866
|
-
name: "ChatAnthropic",
|
|
867
|
-
run_id: "sub-run-1",
|
|
868
|
-
data: { chunk: { content: "part 1" } },
|
|
869
|
-
metadata: { langgraph_checkpoint_ns: ns },
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
sb.processEvent({
|
|
873
|
-
event: "on_chat_model_stream",
|
|
874
|
-
name: "ChatAnthropic",
|
|
875
|
-
run_id: "sub-run-1",
|
|
876
|
-
data: { chunk: { content: " part 2" } },
|
|
877
|
-
metadata: { langgraph_checkpoint_ns: ns },
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
881
|
-
expect(sb.currentStatus.messages[0].content).toBe("part 1 part 2");
|
|
882
|
-
});
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
886
|
-
// Usage accumulation edge cases
|
|
887
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
888
|
-
|
|
889
|
-
describe("usage accumulation edge cases", () => {
|
|
890
|
-
it("handles missing usage_metadata gracefully", () => {
|
|
891
|
-
const sb = makeBuilder();
|
|
892
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
893
|
-
sb.processEvent(chatEndEvent("run-1"));
|
|
894
|
-
|
|
895
|
-
expect(sb.currentStatus.streamingUsage).toBeUndefined();
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
it("handles usage with zero tokens", () => {
|
|
899
|
-
const sb = makeBuilder();
|
|
900
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
901
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
902
|
-
input_tokens: 0,
|
|
903
|
-
output_tokens: 0,
|
|
904
|
-
}));
|
|
905
|
-
|
|
906
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
907
|
-
expect(usage.inputTokens).toBe(0n);
|
|
908
|
-
expect(usage.outputTokens).toBe(0n);
|
|
909
|
-
expect(usage.turnCount).toBe(1);
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
it("handles non-numeric token values gracefully", () => {
|
|
913
|
-
const sb = makeBuilder();
|
|
914
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
915
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
916
|
-
input_tokens: "not a number",
|
|
917
|
-
output_tokens: null,
|
|
918
|
-
cache_read_input_tokens: undefined,
|
|
919
|
-
}));
|
|
920
|
-
|
|
921
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
922
|
-
expect(usage.inputTokens).toBe(0n);
|
|
923
|
-
expect(usage.outputTokens).toBe(0n);
|
|
924
|
-
expect(usage.cacheReadTokens).toBe(0n);
|
|
925
|
-
expect(usage.turnCount).toBe(1);
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
it("handles bigint token values", () => {
|
|
929
|
-
const sb = makeBuilder();
|
|
930
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
931
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
932
|
-
input_tokens: 100n,
|
|
933
|
-
output_tokens: 50n,
|
|
934
|
-
}));
|
|
935
|
-
|
|
936
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
937
|
-
expect(usage.inputTokens).toBe(100n);
|
|
938
|
-
expect(usage.outputTokens).toBe(50n);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
it("computes totalTokens as sum of all token fields", () => {
|
|
942
|
-
const sb = makeBuilder();
|
|
943
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
944
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
945
|
-
input_tokens: 100,
|
|
946
|
-
output_tokens: 50,
|
|
947
|
-
cache_read_input_tokens: 20,
|
|
948
|
-
cache_creation_input_tokens: 10,
|
|
949
|
-
}));
|
|
950
|
-
|
|
951
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
952
|
-
expect(usage.totalTokens).toBe(180n);
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it("usage from data.usage_metadata takes precedence when output missing", () => {
|
|
956
|
-
const sb = makeBuilder();
|
|
957
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
958
|
-
|
|
959
|
-
sb.processEvent({
|
|
960
|
-
event: "on_chat_model_end",
|
|
961
|
-
name: "ChatAnthropic",
|
|
962
|
-
run_id: "run-1",
|
|
963
|
-
data: {
|
|
964
|
-
output: {},
|
|
965
|
-
usage_metadata: {
|
|
966
|
-
input_tokens: 42,
|
|
967
|
-
output_tokens: 13,
|
|
968
|
-
},
|
|
969
|
-
},
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
973
|
-
expect(usage.inputTokens).toBe(42n);
|
|
974
|
-
expect(usage.outputTokens).toBe(13n);
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
it("sets observedAt timestamp on usage snapshot", () => {
|
|
978
|
-
const sb = makeBuilder();
|
|
979
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
980
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
981
|
-
input_tokens: 10,
|
|
982
|
-
output_tokens: 5,
|
|
983
|
-
}));
|
|
984
|
-
|
|
985
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
986
|
-
expect(usage.observedAt).toBeTruthy();
|
|
987
|
-
expect(usage.observedAt).toContain("T");
|
|
988
|
-
});
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
992
|
-
// Error handling in event handlers
|
|
993
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
994
|
-
|
|
995
|
-
describe("event handler error resilience", () => {
|
|
996
|
-
it("does not throw when handler encounters an error", () => {
|
|
997
|
-
const sb = makeBuilder();
|
|
998
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
999
|
-
|
|
1000
|
-
sb.processEvent({
|
|
1001
|
-
event: "on_chat_model_stream",
|
|
1002
|
-
run_id: "run-err",
|
|
1003
|
-
data: { chunk: { content: { toString() { throw new Error("boom"); } } } },
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
errorSpy.mockRestore();
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
it("logs error with execution context on handler failure", () => {
|
|
1010
|
-
const sb = makeBuilder();
|
|
1011
|
-
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
1012
|
-
|
|
1013
|
-
// Need a tool call in the map so handleToolEnd reaches the code
|
|
1014
|
-
// that accesses event.data.output (which we make throw)
|
|
1015
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1016
|
-
sb.processEvent(toolStartEvent("bad-run", "read"));
|
|
1017
|
-
|
|
1018
|
-
sb.processEvent({
|
|
1019
|
-
event: "on_tool_end",
|
|
1020
|
-
name: "read",
|
|
1021
|
-
run_id: "bad-run",
|
|
1022
|
-
data: { get output() { throw new Error("property access failed"); } } as any,
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
expect(errorSpy).toHaveBeenCalled();
|
|
1026
|
-
const logMsg = errorSpy.mock.calls[0][0] as string;
|
|
1027
|
-
expect(logMsg).toContain("exec-test");
|
|
1028
|
-
expect(logMsg).toContain("on_tool_end");
|
|
1029
|
-
expect(logMsg).toContain("bad-run");
|
|
1030
|
-
|
|
1031
|
-
errorSpy.mockRestore();
|
|
1032
|
-
});
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1036
|
-
// Tool result extraction edge cases
|
|
1037
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1038
|
-
|
|
1039
|
-
describe("tool result extraction", () => {
|
|
1040
|
-
it("extracts string result directly", () => {
|
|
1041
|
-
const sb = makeBuilder();
|
|
1042
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1043
|
-
sb.processEvent(toolStartEvent("tool-1", "read"));
|
|
1044
|
-
sb.processEvent(toolEndEvent("tool-1", "file contents"));
|
|
1045
|
-
|
|
1046
|
-
expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("file contents");
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
it("extracts content from object result", () => {
|
|
1050
|
-
const sb = makeBuilder();
|
|
1051
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1052
|
-
sb.processEvent(toolStartEvent("tool-1", "shell"));
|
|
1053
|
-
sb.processEvent(toolEndEvent("tool-1", { content: "stdout output" }));
|
|
1054
|
-
|
|
1055
|
-
expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("stdout output");
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
it("JSON-serializes object without content field", () => {
|
|
1059
|
-
const sb = makeBuilder();
|
|
1060
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1061
|
-
sb.processEvent(toolStartEvent("tool-1", "api"));
|
|
1062
|
-
sb.processEvent(toolEndEvent("tool-1", { status: 200, body: "ok" }));
|
|
1063
|
-
|
|
1064
|
-
const result = sb.currentStatus.messages[0].toolCalls[0].result;
|
|
1065
|
-
expect(JSON.parse(result)).toEqual({ status: 200, body: "ok" });
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
it("JSON-serializes full data when output is undefined", () => {
|
|
1069
|
-
const sb = makeBuilder();
|
|
1070
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1071
|
-
sb.processEvent(toolStartEvent("tool-1", "noop"));
|
|
1072
|
-
sb.processEvent({
|
|
1073
|
-
event: "on_tool_end",
|
|
1074
|
-
name: "noop",
|
|
1075
|
-
run_id: "tool-1",
|
|
1076
|
-
data: { something: "else" },
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
const result = sb.currentStatus.messages[0].toolCalls[0].result;
|
|
1080
|
-
const parsed = JSON.parse(result);
|
|
1081
|
-
expect(parsed.something).toBe("else");
|
|
1082
|
-
});
|
|
1083
|
-
|
|
1084
|
-
it("returns serialization error for circular references", () => {
|
|
1085
|
-
const sb = makeBuilder();
|
|
1086
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1087
|
-
sb.processEvent(toolStartEvent("tool-1", "bad"));
|
|
1088
|
-
|
|
1089
|
-
const circular: Record<string, unknown> = { a: 1 };
|
|
1090
|
-
circular.self = circular;
|
|
1091
|
-
|
|
1092
|
-
sb.processEvent({
|
|
1093
|
-
event: "on_tool_end",
|
|
1094
|
-
name: "bad",
|
|
1095
|
-
run_id: "tool-1",
|
|
1096
|
-
data: { output: circular },
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
expect(sb.currentStatus.messages[0].toolCalls[0].result).toBe("[serialization error]");
|
|
1100
|
-
});
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1104
|
-
// Multiple concurrent tool calls
|
|
1105
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1106
|
-
|
|
1107
|
-
describe("multiple concurrent tool calls", () => {
|
|
1108
|
-
it("tracks multiple running tools on the same message", () => {
|
|
1109
|
-
const sb = makeBuilder();
|
|
1110
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll do two things"));
|
|
1111
|
-
sb.processEvent(toolStartEvent("tool-a", "read"));
|
|
1112
|
-
sb.processEvent(toolStartEvent("tool-b", "search"));
|
|
1113
|
-
|
|
1114
|
-
const msg = sb.currentStatus.messages[0];
|
|
1115
|
-
expect(msg.toolCalls).toHaveLength(2);
|
|
1116
|
-
expect(msg.toolCalls[0].name).toBe("read");
|
|
1117
|
-
expect(msg.toolCalls[1].name).toBe("search");
|
|
1118
|
-
expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
1119
|
-
expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
it("completes tools independently", () => {
|
|
1123
|
-
const sb = makeBuilder();
|
|
1124
|
-
sb.processEvent(chatStreamEvent("run-1", "parallel work"));
|
|
1125
|
-
sb.processEvent(toolStartEvent("tool-a", "read"));
|
|
1126
|
-
sb.processEvent(toolStartEvent("tool-b", "search"));
|
|
1127
|
-
|
|
1128
|
-
sb.processEvent(toolEndEvent("tool-a", "file data"));
|
|
1129
|
-
|
|
1130
|
-
const msg = sb.currentStatus.messages[0];
|
|
1131
|
-
expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1132
|
-
expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
|
|
1133
|
-
|
|
1134
|
-
sb.processEvent(toolEndEvent("tool-b", "search results"));
|
|
1135
|
-
expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
it("handles interleaved tool start and end events", () => {
|
|
1139
|
-
const sb = makeBuilder();
|
|
1140
|
-
sb.processEvent(chatStreamEvent("run-1", "working"));
|
|
1141
|
-
sb.processEvent(toolStartEvent("tool-1", "read"));
|
|
1142
|
-
sb.processEvent(toolEndEvent("tool-1", "data 1"));
|
|
1143
|
-
sb.processEvent(toolStartEvent("tool-2", "write"));
|
|
1144
|
-
sb.processEvent(toolEndEvent("tool-2", "done"));
|
|
1145
|
-
|
|
1146
|
-
const msg = sb.currentStatus.messages[0];
|
|
1147
|
-
expect(msg.toolCalls).toHaveLength(2);
|
|
1148
|
-
expect(msg.toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1149
|
-
expect(msg.toolCalls[1].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1150
|
-
});
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1154
|
-
// ExecutionState lifecycle
|
|
1155
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1156
|
-
|
|
1157
|
-
describe("execution state lifecycle", () => {
|
|
1158
|
-
it("tool start times are cleaned up on tool end", () => {
|
|
1159
|
-
const sb = makeBuilder();
|
|
1160
|
-
sb.processEvent(chatStreamEvent("run-1", "text"));
|
|
1161
|
-
sb.processEvent(toolStartEvent("tool-1", "read"));
|
|
1162
|
-
sb.processEvent(toolEndEvent("tool-1", "done"));
|
|
1163
|
-
|
|
1164
|
-
// Internal state should have cleaned up the start time
|
|
1165
|
-
// Verified by the fact that there's no leak — the TS implementation
|
|
1166
|
-
// deletes from toolStartTimes in handleToolEnd
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
it("handles chat_model_end without prior stream (no message exists)", () => {
|
|
1170
|
-
const sb = makeBuilder();
|
|
1171
|
-
sb.processEvent(chatEndEvent("orphan-run", {
|
|
1172
|
-
input_tokens: 50,
|
|
1173
|
-
output_tokens: 25,
|
|
1174
|
-
}));
|
|
1175
|
-
|
|
1176
|
-
// Usage should still accumulate even without a prior message
|
|
1177
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
1178
|
-
expect(usage.inputTokens).toBe(50n);
|
|
1179
|
-
expect(usage.outputTokens).toBe(25n);
|
|
1180
|
-
expect(usage.turnCount).toBe(1);
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
it("handles tool_end for unknown run_id gracefully", () => {
|
|
1184
|
-
const sb = makeBuilder();
|
|
1185
|
-
sb.processEvent(toolEndEvent("never-started", "result"));
|
|
1186
|
-
// Should not throw, no messages or tool calls created
|
|
1187
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
it("creates AI message for tool_start without prior text stream", () => {
|
|
1191
|
-
const sb = makeBuilder();
|
|
1192
|
-
sb.processEvent(toolStartEvent("orphan-tool", "read"));
|
|
1193
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
1194
|
-
expect(sb.currentStatus.messages[0].toolCalls).toHaveLength(1);
|
|
1195
|
-
expect(sb.currentStatus.messages[0].toolCalls[0].name).toBe("read");
|
|
1196
|
-
});
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1200
|
-
// Thinking + text interleaving
|
|
1201
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1202
|
-
|
|
1203
|
-
describe("thinking and text interleaving", () => {
|
|
1204
|
-
it("creates separate messages for thinking and text in sequence", () => {
|
|
1205
|
-
const sb = makeBuilder();
|
|
1206
|
-
|
|
1207
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1208
|
-
{ type: "thinking", thinking: "Let me think..." },
|
|
1209
|
-
]));
|
|
1210
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1211
|
-
{ type: "text", text: "Here is my answer" },
|
|
1212
|
-
]));
|
|
1213
|
-
|
|
1214
|
-
expect(sb.currentStatus.messages).toHaveLength(2);
|
|
1215
|
-
expect(sb.currentStatus.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
|
|
1216
|
-
expect(sb.currentStatus.messages[0].content).toBe("Let me think...");
|
|
1217
|
-
expect(sb.currentStatus.messages[1].type).toBe(MessageType.MESSAGE_AI);
|
|
1218
|
-
expect(sb.currentStatus.messages[1].content).toBe("Here is my answer");
|
|
1219
|
-
});
|
|
1220
|
-
|
|
1221
|
-
it("multiple thinking blocks accumulate in same thinking message", () => {
|
|
1222
|
-
const sb = makeBuilder();
|
|
1223
|
-
|
|
1224
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1225
|
-
{ type: "thinking", thinking: "Step 1. " },
|
|
1226
|
-
]));
|
|
1227
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1228
|
-
{ type: "thinking", thinking: "Step 2. " },
|
|
1229
|
-
]));
|
|
1230
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1231
|
-
{ type: "thinking", thinking: "Step 3." },
|
|
1232
|
-
]));
|
|
1233
|
-
|
|
1234
|
-
expect(sb.currentStatus.messages).toHaveLength(1);
|
|
1235
|
-
expect(sb.currentStatus.messages[0].content).toBe("Step 1. Step 2. Step 3.");
|
|
1236
|
-
});
|
|
1237
|
-
|
|
1238
|
-
it("thinking from different namespaces creates separate thinking messages", () => {
|
|
1239
|
-
const sb = makeBuilder();
|
|
1240
|
-
|
|
1241
|
-
sb.processEvent({
|
|
1242
|
-
event: "on_chat_model_stream",
|
|
1243
|
-
name: "ChatAnthropic",
|
|
1244
|
-
run_id: "run-main",
|
|
1245
|
-
data: { chunk: { content: [{ type: "thinking", thinking: "main thinking" }] } },
|
|
1246
|
-
metadata: {},
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
sb.processEvent({
|
|
1250
|
-
event: "on_chat_model_stream",
|
|
1251
|
-
name: "ChatAnthropic",
|
|
1252
|
-
run_id: "run-sub",
|
|
1253
|
-
data: { chunk: { content: [{ type: "thinking", thinking: "sub thinking" }] } },
|
|
1254
|
-
metadata: { langgraph_checkpoint_ns: "tools:task-1" },
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
expect(sb.currentStatus.messages).toHaveLength(2);
|
|
1258
|
-
expect(sb.currentStatus.messages[0].content).toBe("main thinking");
|
|
1259
|
-
expect(sb.currentStatus.messages[1].content).toBe("sub thinking");
|
|
1260
|
-
});
|
|
1261
|
-
});
|
|
1262
|
-
|
|
1263
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1264
|
-
// Content block edge cases
|
|
1265
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
1266
|
-
|
|
1267
|
-
describe("content block edge cases", () => {
|
|
1268
|
-
it("ignores non-text non-thinking blocks", () => {
|
|
1269
|
-
const sb = makeBuilder();
|
|
1270
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1271
|
-
{ type: "image_url", image_url: "https://example.com/img.png" },
|
|
1272
|
-
]));
|
|
1273
|
-
|
|
1274
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
it("handles null content blocks gracefully", () => {
|
|
1278
|
-
const sb = makeBuilder();
|
|
1279
|
-
sb.processEvent(chatStreamEvent("run-1", [null as any]));
|
|
1280
|
-
|
|
1281
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
it("handles empty array content", () => {
|
|
1285
|
-
const sb = makeBuilder();
|
|
1286
|
-
sb.processEvent(chatStreamEvent("run-1", []));
|
|
1287
|
-
|
|
1288
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
it("handles numeric content gracefully", () => {
|
|
1292
|
-
const sb = makeBuilder();
|
|
1293
|
-
sb.processEvent(chatStreamEvent("run-1", 42 as any));
|
|
1294
|
-
|
|
1295
|
-
expect(sb.currentStatus.messages).toHaveLength(0);
|
|
1296
|
-
});
|
|
1297
|
-
});
|
|
1298
|
-
|
|
1299
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1300
|
-
// GOLDEN SEQUENCES
|
|
1301
|
-
//
|
|
1302
|
-
// Realistic multi-event conversation sequences that assert on the
|
|
1303
|
-
// complete AgentExecutionStatus shape. These serve as the regression
|
|
1304
|
-
// contract for V3StatusBuilder in Phase 2 of the v3 streaming migration.
|
|
1305
|
-
//
|
|
1306
|
-
// Each sequence represents a real-world conversation pattern and
|
|
1307
|
-
// validates the final proto output end-to-end through StatusBuilder.
|
|
1308
|
-
// ═══════════════════════════════════════════════════════════════════════
|
|
1309
|
-
|
|
1310
|
-
describe("golden sequences", () => {
|
|
1311
|
-
|
|
1312
|
-
describe("plain chat — 2-turn text-only conversation", () => {
|
|
1313
|
-
it("produces 2 AI messages with accumulated usage", () => {
|
|
1314
|
-
const sb = makeBuilder();
|
|
1315
|
-
|
|
1316
|
-
// Turn 1: stream tokens → end
|
|
1317
|
-
sb.processEvent(chatStreamEvent("run-1", "Hello, "));
|
|
1318
|
-
sb.processEvent(chatStreamEvent("run-1", "I can help "));
|
|
1319
|
-
sb.processEvent(chatStreamEvent("run-1", "with that."));
|
|
1320
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1321
|
-
input_tokens: 50,
|
|
1322
|
-
output_tokens: 12,
|
|
1323
|
-
}));
|
|
1324
|
-
|
|
1325
|
-
// Turn 2: stream tokens → end
|
|
1326
|
-
sb.processEvent(chatStreamEvent("run-2", "Here is "));
|
|
1327
|
-
sb.processEvent(chatStreamEvent("run-2", "more detail."));
|
|
1328
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1329
|
-
input_tokens: 80,
|
|
1330
|
-
output_tokens: 8,
|
|
1331
|
-
}));
|
|
1332
|
-
|
|
1333
|
-
const status = sb.currentStatus;
|
|
1334
|
-
|
|
1335
|
-
// 2 AI messages, both finished streaming
|
|
1336
|
-
expect(status.messages).toHaveLength(2);
|
|
1337
|
-
expect(status.messages[0].type).toBe(MessageType.MESSAGE_AI);
|
|
1338
|
-
expect(status.messages[0].content).toBe("Hello, I can help with that.");
|
|
1339
|
-
expect(status.messages[0].isStreaming).toBe(false);
|
|
1340
|
-
expect(status.messages[0].toolCalls).toHaveLength(0);
|
|
1341
|
-
|
|
1342
|
-
expect(status.messages[1].type).toBe(MessageType.MESSAGE_AI);
|
|
1343
|
-
expect(status.messages[1].content).toBe("Here is more detail.");
|
|
1344
|
-
expect(status.messages[1].isStreaming).toBe(false);
|
|
1345
|
-
expect(status.messages[1].toolCalls).toHaveLength(0);
|
|
1346
|
-
|
|
1347
|
-
// Usage accumulated across turns
|
|
1348
|
-
const usage = status.streamingUsage!;
|
|
1349
|
-
expect(usage.inputTokens).toBe(130n);
|
|
1350
|
-
expect(usage.outputTokens).toBe(20n);
|
|
1351
|
-
expect(usage.turnCount).toBe(2);
|
|
1352
|
-
expect(usage.totalTokens).toBe(150n);
|
|
1353
|
-
|
|
1354
|
-
// Phase stays IN_PROGRESS (terminal phase set by caller)
|
|
1355
|
-
expect(status.phase).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
1356
|
-
});
|
|
1357
|
-
});
|
|
1358
|
-
|
|
1359
|
-
describe("Anthropic thinking + text", () => {
|
|
1360
|
-
it("produces THINKING then AI messages with correct turn boundaries", () => {
|
|
1361
|
-
const sb = makeBuilder();
|
|
1362
|
-
|
|
1363
|
-
// Turn 1: thinking blocks → text blocks (same run_id)
|
|
1364
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1365
|
-
{ type: "thinking", thinking: "Let me analyze " },
|
|
1366
|
-
]));
|
|
1367
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1368
|
-
{ type: "thinking", thinking: "this problem carefully." },
|
|
1369
|
-
]));
|
|
1370
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1371
|
-
{ type: "text", text: "Based on my analysis, " },
|
|
1372
|
-
]));
|
|
1373
|
-
sb.processEvent(chatStreamEvent("run-1", [
|
|
1374
|
-
{ type: "text", text: "here is the answer." },
|
|
1375
|
-
]));
|
|
1376
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1377
|
-
input_tokens: 200,
|
|
1378
|
-
output_tokens: 80,
|
|
1379
|
-
cache_read_input_tokens: 50,
|
|
1380
|
-
}));
|
|
1381
|
-
|
|
1382
|
-
// Turn 2: new run_id, text only (follow-up)
|
|
1383
|
-
sb.processEvent(chatStreamEvent("run-2", "Let me elaborate."));
|
|
1384
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1385
|
-
input_tokens: 300,
|
|
1386
|
-
output_tokens: 40,
|
|
1387
|
-
}));
|
|
1388
|
-
|
|
1389
|
-
const status = sb.currentStatus;
|
|
1390
|
-
|
|
1391
|
-
// 3 messages: thinking, AI (turn 1), AI (turn 2)
|
|
1392
|
-
expect(status.messages).toHaveLength(3);
|
|
1393
|
-
|
|
1394
|
-
expect(status.messages[0].type).toBe(MessageType.MESSAGE_THINKING);
|
|
1395
|
-
expect(status.messages[0].content).toBe("Let me analyze this problem carefully.");
|
|
1396
|
-
expect(status.messages[0].isStreaming).toBe(true); // thinking messages aren't finalized by chat_model_end
|
|
1397
|
-
|
|
1398
|
-
expect(status.messages[1].type).toBe(MessageType.MESSAGE_AI);
|
|
1399
|
-
expect(status.messages[1].content).toBe("Based on my analysis, here is the answer.");
|
|
1400
|
-
expect(status.messages[1].isStreaming).toBe(false);
|
|
1401
|
-
|
|
1402
|
-
expect(status.messages[2].type).toBe(MessageType.MESSAGE_AI);
|
|
1403
|
-
expect(status.messages[2].content).toBe("Let me elaborate.");
|
|
1404
|
-
expect(status.messages[2].isStreaming).toBe(false);
|
|
1405
|
-
|
|
1406
|
-
// Usage across both turns with cache tokens
|
|
1407
|
-
const usage = status.streamingUsage!;
|
|
1408
|
-
expect(usage.inputTokens).toBe(500n);
|
|
1409
|
-
expect(usage.outputTokens).toBe(120n);
|
|
1410
|
-
expect(usage.cacheReadTokens).toBe(50n);
|
|
1411
|
-
expect(usage.turnCount).toBe(2);
|
|
1412
|
-
expect(usage.totalTokens).toBe(670n);
|
|
1413
|
-
});
|
|
1414
|
-
});
|
|
1415
|
-
|
|
1416
|
-
describe("single tool call — ReAct pattern", () => {
|
|
1417
|
-
it("produces AI message with tool call followed by response", () => {
|
|
1418
|
-
const sb = makeBuilder();
|
|
1419
|
-
|
|
1420
|
-
// Turn 1: model decides to call a tool
|
|
1421
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll read the file for you."));
|
|
1422
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1423
|
-
input_tokens: 100,
|
|
1424
|
-
output_tokens: 15,
|
|
1425
|
-
}));
|
|
1426
|
-
|
|
1427
|
-
// Tool execution
|
|
1428
|
-
sb.processEvent(toolStartEvent("tool-run-1", "read_file", { path: "/src/main.ts" }));
|
|
1429
|
-
sb.processEvent(toolEndEvent("tool-run-1", "export function main() { console.log('hello'); }"));
|
|
1430
|
-
|
|
1431
|
-
// Turn 2: model responds with tool result
|
|
1432
|
-
sb.processEvent(chatStreamEvent("run-2", "The file contains a main function that logs 'hello'."));
|
|
1433
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1434
|
-
input_tokens: 200,
|
|
1435
|
-
output_tokens: 20,
|
|
1436
|
-
}));
|
|
1437
|
-
|
|
1438
|
-
const status = sb.currentStatus;
|
|
1439
|
-
|
|
1440
|
-
// 2 messages: AI with tool call, AI response
|
|
1441
|
-
expect(status.messages).toHaveLength(2);
|
|
1442
|
-
|
|
1443
|
-
const firstMsg = status.messages[0];
|
|
1444
|
-
expect(firstMsg.type).toBe(MessageType.MESSAGE_AI);
|
|
1445
|
-
expect(firstMsg.content).toBe("I'll read the file for you.");
|
|
1446
|
-
expect(firstMsg.isStreaming).toBe(false);
|
|
1447
|
-
expect(firstMsg.toolCalls).toHaveLength(1);
|
|
1448
|
-
|
|
1449
|
-
const tc = firstMsg.toolCalls[0];
|
|
1450
|
-
expect(tc.id).toBe("tool-run-1");
|
|
1451
|
-
expect(tc.name).toBe("read_file");
|
|
1452
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1453
|
-
expect(tc.args).toEqual({ path: "/src/main.ts" });
|
|
1454
|
-
expect(tc.result).toBe("export function main() { console.log('hello'); }");
|
|
1455
|
-
expect(tc.error).toBe("");
|
|
1456
|
-
expect(tc.isStreaming).toBe(false);
|
|
1457
|
-
expect(tc.startedAt).toBeTruthy();
|
|
1458
|
-
expect(tc.completedAt).toBeTruthy();
|
|
1459
|
-
|
|
1460
|
-
const secondMsg = status.messages[1];
|
|
1461
|
-
expect(secondMsg.type).toBe(MessageType.MESSAGE_AI);
|
|
1462
|
-
expect(secondMsg.content).toBe("The file contains a main function that logs 'hello'.");
|
|
1463
|
-
expect(secondMsg.toolCalls).toHaveLength(0);
|
|
1464
|
-
|
|
1465
|
-
// Usage accumulated across both LLM turns
|
|
1466
|
-
const usage = status.streamingUsage!;
|
|
1467
|
-
expect(usage.inputTokens).toBe(300n);
|
|
1468
|
-
expect(usage.outputTokens).toBe(35n);
|
|
1469
|
-
expect(usage.turnCount).toBe(2);
|
|
1470
|
-
});
|
|
1471
|
-
});
|
|
1472
|
-
|
|
1473
|
-
describe("tool error — failed tool call", () => {
|
|
1474
|
-
it("marks tool as FAILED with error string", () => {
|
|
1475
|
-
const sb = makeBuilder();
|
|
1476
|
-
|
|
1477
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll write to the file."));
|
|
1478
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1479
|
-
input_tokens: 50,
|
|
1480
|
-
output_tokens: 10,
|
|
1481
|
-
}));
|
|
1482
|
-
|
|
1483
|
-
sb.processEvent(toolStartEvent("tool-run-1", "write_file", {
|
|
1484
|
-
path: "/etc/passwd",
|
|
1485
|
-
content: "malicious",
|
|
1486
|
-
}));
|
|
1487
|
-
sb.processEvent(toolEndEvent("tool-run-1", {
|
|
1488
|
-
error: "EACCES: permission denied, open '/etc/passwd'",
|
|
1489
|
-
}));
|
|
1490
|
-
|
|
1491
|
-
// Model recovers with error explanation
|
|
1492
|
-
sb.processEvent(chatStreamEvent("run-2", "I don't have permission to write to that file."));
|
|
1493
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1494
|
-
input_tokens: 120,
|
|
1495
|
-
output_tokens: 18,
|
|
1496
|
-
}));
|
|
1497
|
-
|
|
1498
|
-
const status = sb.currentStatus;
|
|
1499
|
-
|
|
1500
|
-
expect(status.messages).toHaveLength(2);
|
|
1501
|
-
|
|
1502
|
-
const tc = status.messages[0].toolCalls[0];
|
|
1503
|
-
expect(tc.name).toBe("write_file");
|
|
1504
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
|
|
1505
|
-
expect(tc.error).toBe("EACCES: permission denied, open '/etc/passwd'");
|
|
1506
|
-
expect(tc.result).toBe("");
|
|
1507
|
-
expect(tc.completedAt).toBeTruthy();
|
|
1508
|
-
|
|
1509
|
-
expect(status.messages[1].content).toBe(
|
|
1510
|
-
"I don't have permission to write to that file.",
|
|
1511
|
-
);
|
|
1512
|
-
|
|
1513
|
-
const usage = status.streamingUsage!;
|
|
1514
|
-
expect(usage.turnCount).toBe(2);
|
|
1515
|
-
});
|
|
1516
|
-
});
|
|
1517
|
-
|
|
1518
|
-
describe("multi-tool concurrent — parallel tool execution", () => {
|
|
1519
|
-
it("tracks concurrent tools independently on same message", () => {
|
|
1520
|
-
const sb = makeBuilder();
|
|
1521
|
-
|
|
1522
|
-
// Model decides to call two tools in parallel
|
|
1523
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll search both files."));
|
|
1524
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1525
|
-
input_tokens: 60,
|
|
1526
|
-
output_tokens: 10,
|
|
1527
|
-
}));
|
|
1528
|
-
|
|
1529
|
-
sb.processEvent(toolStartEvent("tool-a", "read_file", { path: "/a.ts" }));
|
|
1530
|
-
sb.processEvent(toolStartEvent("tool-b", "read_file", { path: "/b.ts" }));
|
|
1531
|
-
|
|
1532
|
-
// Tool B finishes first (out of order)
|
|
1533
|
-
sb.processEvent(toolEndEvent("tool-b", "content of b.ts"));
|
|
1534
|
-
sb.processEvent(toolEndEvent("tool-a", "content of a.ts"));
|
|
1535
|
-
|
|
1536
|
-
// Model synthesizes results
|
|
1537
|
-
sb.processEvent(chatStreamEvent("run-2", "Both files have been read."));
|
|
1538
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1539
|
-
input_tokens: 180,
|
|
1540
|
-
output_tokens: 12,
|
|
1541
|
-
}));
|
|
1542
|
-
|
|
1543
|
-
const status = sb.currentStatus;
|
|
1544
|
-
|
|
1545
|
-
expect(status.messages).toHaveLength(2);
|
|
1546
|
-
|
|
1547
|
-
const toolMsg = status.messages[0];
|
|
1548
|
-
expect(toolMsg.toolCalls).toHaveLength(2);
|
|
1549
|
-
|
|
1550
|
-
const tcA = toolMsg.toolCalls.find(t => t.id === "tool-a")!;
|
|
1551
|
-
const tcB = toolMsg.toolCalls.find(t => t.id === "tool-b")!;
|
|
1552
|
-
|
|
1553
|
-
expect(tcA.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1554
|
-
expect(tcA.result).toBe("content of a.ts");
|
|
1555
|
-
expect(tcB.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1556
|
-
expect(tcB.result).toBe("content of b.ts");
|
|
1557
|
-
|
|
1558
|
-
// Both completed independently
|
|
1559
|
-
expect(tcA.completedAt).toBeTruthy();
|
|
1560
|
-
expect(tcB.completedAt).toBeTruthy();
|
|
1561
|
-
});
|
|
1562
|
-
});
|
|
1563
|
-
|
|
1564
|
-
describe("HITL approval gate — tool requiring approval", () => {
|
|
1565
|
-
it("transitions phase to WAITING_FOR_APPROVAL with approval fields", () => {
|
|
1566
|
-
const sb = makeBuilder();
|
|
1567
|
-
|
|
1568
|
-
sb.setApprovalProvider({
|
|
1569
|
-
policies: new Map([
|
|
1570
|
-
["github/create_pull_request", {
|
|
1571
|
-
toolName: "create_pull_request",
|
|
1572
|
-
mcpServerSlug: "github",
|
|
1573
|
-
requiresApproval: true,
|
|
1574
|
-
approvalMessage: "Create PR '{{args.title}}' in {{args.repo}}?",
|
|
1575
|
-
source: "classifier_default",
|
|
1576
|
-
}],
|
|
1577
|
-
]),
|
|
1578
|
-
toolServerMap: new Map([["create_pull_request", "github"]]),
|
|
1579
|
-
globalBypass: false,
|
|
1580
|
-
});
|
|
1581
|
-
|
|
1582
|
-
// Model streams text then calls the approval-gated tool
|
|
1583
|
-
sb.processEvent(chatStreamEvent("run-1", "I'll create a pull request."));
|
|
1584
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1585
|
-
input_tokens: 90,
|
|
1586
|
-
output_tokens: 12,
|
|
1587
|
-
}));
|
|
1588
|
-
|
|
1589
|
-
sb.processEvent(toolStartEvent("tool-pr-1", "create_pull_request", {
|
|
1590
|
-
title: "Fix login bug",
|
|
1591
|
-
repo: "stigmer/stigmer",
|
|
1592
|
-
}));
|
|
1593
|
-
|
|
1594
|
-
const status = sb.currentStatus;
|
|
1595
|
-
|
|
1596
|
-
// Phase transitioned to waiting
|
|
1597
|
-
expect(status.phase).toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
|
|
1598
|
-
|
|
1599
|
-
const tc = status.messages[0].toolCalls[0];
|
|
1600
|
-
expect(tc.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
1601
|
-
expect(tc.requiresApproval).toBe(true);
|
|
1602
|
-
expect(tc.approvalMessage).toBe(
|
|
1603
|
-
"Create PR 'Fix login bug' in stigmer/stigmer?",
|
|
1604
|
-
);
|
|
1605
|
-
expect(tc.mcpServerSlug).toBe("github");
|
|
1606
|
-
expect(tc.approvalRequestedAt).toBeTruthy();
|
|
1607
|
-
expect(tc.argsPreview).toBeTruthy();
|
|
1608
|
-
|
|
1609
|
-
// Args preview has sensitive fields redacted
|
|
1610
|
-
const preview = JSON.parse(tc.argsPreview);
|
|
1611
|
-
expect(preview.title).toBe("Fix login bug");
|
|
1612
|
-
expect(preview.repo).toBe("stigmer/stigmer");
|
|
1613
|
-
});
|
|
1614
|
-
});
|
|
1615
|
-
|
|
1616
|
-
describe("usage accumulation — 3-turn with cache tokens", () => {
|
|
1617
|
-
it("accumulates input, output, cache read/write across 3 turns", () => {
|
|
1618
|
-
const sb = makeBuilder();
|
|
1619
|
-
|
|
1620
|
-
// Turn 1: initial prompt
|
|
1621
|
-
sb.processEvent(chatStreamEvent("run-1", "First."));
|
|
1622
|
-
sb.processEvent(chatEndEvent("run-1", {
|
|
1623
|
-
input_tokens: 100,
|
|
1624
|
-
output_tokens: 5,
|
|
1625
|
-
cache_read_input_tokens: 0,
|
|
1626
|
-
cache_creation_input_tokens: 80,
|
|
1627
|
-
}));
|
|
1628
|
-
|
|
1629
|
-
// Turn 2: tool call turn
|
|
1630
|
-
sb.processEvent(chatStreamEvent("run-2", "Reading."));
|
|
1631
|
-
sb.processEvent(chatEndEvent("run-2", {
|
|
1632
|
-
input_tokens: 200,
|
|
1633
|
-
output_tokens: 8,
|
|
1634
|
-
cache_read_input_tokens: 80,
|
|
1635
|
-
cache_creation_input_tokens: 0,
|
|
1636
|
-
}));
|
|
1637
|
-
|
|
1638
|
-
// Turn 3: final response
|
|
1639
|
-
sb.processEvent(chatStreamEvent("run-3", "Done."));
|
|
1640
|
-
sb.processEvent(chatEndEvent("run-3", {
|
|
1641
|
-
input_tokens: 350,
|
|
1642
|
-
output_tokens: 30,
|
|
1643
|
-
cache_read_input_tokens: 80,
|
|
1644
|
-
cache_creation_input_tokens: 20,
|
|
1645
|
-
}));
|
|
1646
|
-
|
|
1647
|
-
const usage = sb.currentStatus.streamingUsage!;
|
|
1648
|
-
|
|
1649
|
-
expect(usage.inputTokens).toBe(650n);
|
|
1650
|
-
expect(usage.outputTokens).toBe(43n);
|
|
1651
|
-
expect(usage.cacheReadTokens).toBe(160n);
|
|
1652
|
-
expect(usage.cacheWriteTokens).toBe(100n);
|
|
1653
|
-
expect(usage.totalTokens).toBe(953n);
|
|
1654
|
-
expect(usage.turnCount).toBe(3);
|
|
1655
|
-
expect(usage.observedAt).toBeTruthy();
|
|
1656
|
-
});
|
|
1657
|
-
});
|
|
1658
|
-
|
|
1659
|
-
describe("namespace isolation — parent + subagent", () => {
|
|
1660
|
-
it("creates separate message trees by namespace", () => {
|
|
1661
|
-
const sb = makeBuilder();
|
|
1662
|
-
|
|
1663
|
-
const parentNs = "";
|
|
1664
|
-
const subagentNs = "tools:delegate-123|agent_node:researcher";
|
|
1665
|
-
|
|
1666
|
-
// Parent agent starts
|
|
1667
|
-
sb.processEvent({
|
|
1668
|
-
event: "on_chat_model_stream",
|
|
1669
|
-
name: "ChatAnthropic",
|
|
1670
|
-
run_id: "parent-run-1",
|
|
1671
|
-
data: { chunk: { content: "I'll delegate this research task." } },
|
|
1672
|
-
metadata: {},
|
|
1673
|
-
});
|
|
1674
|
-
sb.processEvent({
|
|
1675
|
-
event: "on_chat_model_end",
|
|
1676
|
-
name: "ChatAnthropic",
|
|
1677
|
-
run_id: "parent-run-1",
|
|
1678
|
-
data: {
|
|
1679
|
-
output: { content: "done" },
|
|
1680
|
-
usage_metadata: { input_tokens: 100, output_tokens: 15 },
|
|
1681
|
-
},
|
|
1682
|
-
});
|
|
1683
|
-
|
|
1684
|
-
// Subagent thinking
|
|
1685
|
-
sb.processEvent({
|
|
1686
|
-
event: "on_chat_model_stream",
|
|
1687
|
-
name: "ChatAnthropic",
|
|
1688
|
-
run_id: "sub-run-1",
|
|
1689
|
-
data: { chunk: { content: [{ type: "thinking", thinking: "Researching the topic..." }] } },
|
|
1690
|
-
metadata: { langgraph_checkpoint_ns: subagentNs },
|
|
1691
|
-
});
|
|
1692
|
-
|
|
1693
|
-
// Subagent text response
|
|
1694
|
-
sb.processEvent({
|
|
1695
|
-
event: "on_chat_model_stream",
|
|
1696
|
-
name: "ChatAnthropic",
|
|
1697
|
-
run_id: "sub-run-1",
|
|
1698
|
-
data: { chunk: { content: [{ type: "text", text: "I found the following results." }] } },
|
|
1699
|
-
metadata: { langgraph_checkpoint_ns: subagentNs },
|
|
1700
|
-
});
|
|
1701
|
-
sb.processEvent({
|
|
1702
|
-
event: "on_chat_model_end",
|
|
1703
|
-
name: "ChatAnthropic",
|
|
1704
|
-
run_id: "sub-run-1",
|
|
1705
|
-
data: {
|
|
1706
|
-
output: { content: "done" },
|
|
1707
|
-
usage_metadata: { input_tokens: 150, output_tokens: 25 },
|
|
1708
|
-
},
|
|
1709
|
-
metadata: { langgraph_checkpoint_ns: subagentNs },
|
|
1710
|
-
});
|
|
1711
|
-
|
|
1712
|
-
// Subagent tool call
|
|
1713
|
-
sb.processEvent({
|
|
1714
|
-
event: "on_tool_start",
|
|
1715
|
-
name: "web_search",
|
|
1716
|
-
run_id: "sub-tool-1",
|
|
1717
|
-
data: { input: { query: "LangGraph v3 streaming" } },
|
|
1718
|
-
metadata: { langgraph_checkpoint_ns: subagentNs },
|
|
1719
|
-
});
|
|
1720
|
-
sb.processEvent({
|
|
1721
|
-
event: "on_tool_end",
|
|
1722
|
-
name: "web_search",
|
|
1723
|
-
run_id: "sub-tool-1",
|
|
1724
|
-
data: { output: "3 results found" },
|
|
1725
|
-
metadata: { langgraph_checkpoint_ns: subagentNs },
|
|
1726
|
-
});
|
|
1727
|
-
|
|
1728
|
-
// Parent resumes after delegation
|
|
1729
|
-
sb.processEvent({
|
|
1730
|
-
event: "on_chat_model_stream",
|
|
1731
|
-
name: "ChatAnthropic",
|
|
1732
|
-
run_id: "parent-run-2",
|
|
1733
|
-
data: { chunk: { content: "The research is complete." } },
|
|
1734
|
-
metadata: {},
|
|
1735
|
-
});
|
|
1736
|
-
sb.processEvent({
|
|
1737
|
-
event: "on_chat_model_end",
|
|
1738
|
-
name: "ChatAnthropic",
|
|
1739
|
-
run_id: "parent-run-2",
|
|
1740
|
-
data: {
|
|
1741
|
-
output: { content: "done" },
|
|
1742
|
-
usage_metadata: { input_tokens: 400, output_tokens: 10 },
|
|
1743
|
-
},
|
|
1744
|
-
});
|
|
1745
|
-
|
|
1746
|
-
const status = sb.currentStatus;
|
|
1747
|
-
|
|
1748
|
-
// 5 messages: parent-text, sub-thinking, sub-text, parent-text-2
|
|
1749
|
-
// (sub-tool attaches to sub-text message)
|
|
1750
|
-
expect(status.messages).toHaveLength(4);
|
|
1751
|
-
|
|
1752
|
-
// Parent turn 1
|
|
1753
|
-
expect(status.messages[0].type).toBe(MessageType.MESSAGE_AI);
|
|
1754
|
-
expect(status.messages[0].content).toBe("I'll delegate this research task.");
|
|
1755
|
-
expect(status.messages[0].toolCalls).toHaveLength(0);
|
|
1756
|
-
|
|
1757
|
-
// Subagent thinking
|
|
1758
|
-
expect(status.messages[1].type).toBe(MessageType.MESSAGE_THINKING);
|
|
1759
|
-
expect(status.messages[1].content).toBe("Researching the topic...");
|
|
1760
|
-
|
|
1761
|
-
// Subagent text + tool call
|
|
1762
|
-
expect(status.messages[2].type).toBe(MessageType.MESSAGE_AI);
|
|
1763
|
-
expect(status.messages[2].content).toBe("I found the following results.");
|
|
1764
|
-
expect(status.messages[2].toolCalls).toHaveLength(1);
|
|
1765
|
-
expect(status.messages[2].toolCalls[0].name).toBe("web_search");
|
|
1766
|
-
expect(status.messages[2].toolCalls[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1767
|
-
expect(status.messages[2].toolCalls[0].result).toBe("3 results found");
|
|
1768
|
-
|
|
1769
|
-
// Parent turn 2
|
|
1770
|
-
expect(status.messages[3].type).toBe(MessageType.MESSAGE_AI);
|
|
1771
|
-
expect(status.messages[3].content).toBe("The research is complete.");
|
|
1772
|
-
|
|
1773
|
-
// Usage accumulated across ALL namespaces
|
|
1774
|
-
const usage = status.streamingUsage!;
|
|
1775
|
-
expect(usage.inputTokens).toBe(650n);
|
|
1776
|
-
expect(usage.outputTokens).toBe(50n);
|
|
1777
|
-
expect(usage.turnCount).toBe(3);
|
|
1778
|
-
});
|
|
1779
|
-
});
|
|
1780
|
-
});
|
|
1781
|
-
|
|
1782
|
-
// Durable-checkpoint resume on the legacy v2 path. The activity seeds the
|
|
1783
|
-
// builder from the persisted transcript (see seedStatusFromExecution in
|
|
1784
|
-
// index.ts); the resumed stream re-emits the gated tool's lifecycle with a
|
|
1785
|
-
// FRESH LangGraph run_id that does not match the seeded tool_call_id. The
|
|
1786
|
-
// builder must reconcile by name so the existing call is resolved in place,
|
|
1787
|
-
// preserving history with no duplicate (the documented v2 fallback; v3, the
|
|
1788
|
-
// default, matches by the exact tool_call_id).
|
|
1789
|
-
describe("resume reconciliation (durable checkpoint)", () => {
|
|
1790
|
-
const GATED_ID = "toolu_seeded_01";
|
|
1791
|
-
|
|
1792
|
-
function seededBuilder(): StatusBuilder {
|
|
1793
|
-
const status = create(AgentExecutionStatusSchema, {
|
|
1794
|
-
phase: ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
1795
|
-
messages: [
|
|
1796
|
-
create(AgentMessageSchema, {
|
|
1797
|
-
type: MessageType.MESSAGE_AI,
|
|
1798
|
-
content: "I'll call the gated tool.",
|
|
1799
|
-
toolCalls: [
|
|
1800
|
-
create(ToolCallSchema, {
|
|
1801
|
-
id: GATED_ID,
|
|
1802
|
-
name: "dangerous_tool",
|
|
1803
|
-
status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
|
|
1804
|
-
requiresApproval: true,
|
|
1805
|
-
approvalAction: ApprovalAction.APPROVE,
|
|
1806
|
-
mcpServerSlug: "my-server",
|
|
1807
|
-
}),
|
|
1808
|
-
],
|
|
1809
|
-
}),
|
|
1810
|
-
],
|
|
1811
|
-
});
|
|
1812
|
-
const sb = new StatusBuilder("exec-resume", status);
|
|
1813
|
-
sb.setApprovalProvider({
|
|
1814
|
-
policies: new Map([
|
|
1815
|
-
["my-server/dangerous_tool", {
|
|
1816
|
-
toolName: "dangerous_tool",
|
|
1817
|
-
mcpServerSlug: "my-server",
|
|
1818
|
-
requiresApproval: true,
|
|
1819
|
-
approvalMessage: "Execute dangerous_tool",
|
|
1820
|
-
source: "classifier_default",
|
|
1821
|
-
}],
|
|
1822
|
-
]),
|
|
1823
|
-
toolServerMap: new Map([["dangerous_tool", "my-server"]]),
|
|
1824
|
-
globalBypass: false,
|
|
1825
|
-
});
|
|
1826
|
-
return sb;
|
|
1827
|
-
}
|
|
1828
|
-
|
|
1829
|
-
it("resolves the seeded gated tool call in place — no duplicate, history kept", () => {
|
|
1830
|
-
const sb = seededBuilder();
|
|
1831
|
-
|
|
1832
|
-
// Resumed lifecycle: fresh run_id, no tool_call_id available in v2.
|
|
1833
|
-
sb.processEvent(toolStartEvent("run-fresh-99", "dangerous_tool", { target: "prod" }));
|
|
1834
|
-
sb.processEvent(toolEndEvent("run-fresh-99", "tool executed ok"));
|
|
1835
|
-
|
|
1836
|
-
// Following assistant turn appends after the seeded message.
|
|
1837
|
-
sb.processEvent(chatStreamEvent("llm-after", "All done."));
|
|
1838
|
-
sb.processEvent(chatEndEvent("llm-after"));
|
|
1839
|
-
|
|
1840
|
-
const status = sb.currentStatus;
|
|
1841
|
-
|
|
1842
|
-
const gated = status.messages
|
|
1843
|
-
.flatMap(m => m.toolCalls)
|
|
1844
|
-
.filter(tc => tc.id === GATED_ID);
|
|
1845
|
-
expect(gated).toHaveLength(1);
|
|
1846
|
-
expect(gated[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
1847
|
-
expect(gated[0].result).toContain("tool executed ok");
|
|
1848
|
-
|
|
1849
|
-
const allText = status.messages.map(m => m.content).join("\n");
|
|
1850
|
-
expect(allText).toContain("I'll call the gated tool.");
|
|
1851
|
-
expect(allText).toContain("All done.");
|
|
1852
|
-
|
|
1853
|
-
// No re-gate: the resumed tool must not flip the run back to waiting.
|
|
1854
|
-
expect(status.phase).not.toBe(ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL);
|
|
1855
|
-
});
|
|
1856
|
-
});
|
|
1857
|
-
|
|
1858
|
-
// ── Todo extraction (write_todos → status.todos) ─────────────────
|
|
1859
|
-
//
|
|
1860
|
-
// v2 parity with V3StatusBuilder: a completed write_todos projects into
|
|
1861
|
-
// status.todos through the same shared mapper; a failed one does not.
|
|
1862
|
-
|
|
1863
|
-
describe("todo extraction", () => {
|
|
1864
|
-
it("projects a completed write_todos into status.todos", () => {
|
|
1865
|
-
const sb = makeBuilder();
|
|
1866
|
-
sb.processEvent(
|
|
1867
|
-
toolStartEvent("run-1", "write_todos", {
|
|
1868
|
-
todos: [
|
|
1869
|
-
{ content: "Step one", status: "in_progress" },
|
|
1870
|
-
{ content: "Step two", status: "pending" },
|
|
1871
|
-
],
|
|
1872
|
-
}),
|
|
1873
|
-
);
|
|
1874
|
-
sb.processEvent(toolEndEvent("run-1", "Todos updated"));
|
|
1875
|
-
|
|
1876
|
-
const todos = sb.currentStatus.todos;
|
|
1877
|
-
expect(Object.keys(todos)).toEqual(["todo-0", "todo-1"]);
|
|
1878
|
-
expect(todos["todo-0"].content).toBe("Step one");
|
|
1879
|
-
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
|
|
1880
|
-
expect(todos["todo-1"].status).toBe(TodoStatus.TODO_PENDING);
|
|
1881
|
-
});
|
|
1882
|
-
|
|
1883
|
-
it("keeps the write_todos ToolCall in messages stamped ToolKind.TODO", () => {
|
|
1884
|
-
const sb = makeBuilder();
|
|
1885
|
-
sb.processEvent(
|
|
1886
|
-
toolStartEvent("run-1", "write_todos", {
|
|
1887
|
-
todos: [{ content: "A", status: "pending" }],
|
|
1888
|
-
}),
|
|
1889
|
-
);
|
|
1890
|
-
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1891
|
-
|
|
1892
|
-
const tc = sb.currentStatus.messages
|
|
1893
|
-
.flatMap((m) => m.toolCalls)
|
|
1894
|
-
.find((t) => t.name === "write_todos");
|
|
1895
|
-
expect(tc).toBeDefined();
|
|
1896
|
-
expect(tc!.toolKind).toBe(ToolKind.TODO);
|
|
1897
|
-
});
|
|
1898
|
-
|
|
1899
|
-
it("does not project on a failed write_todos (Command never ran)", () => {
|
|
1900
|
-
const sb = makeBuilder();
|
|
1901
|
-
sb.processEvent(
|
|
1902
|
-
toolStartEvent("run-1", "write_todos", {
|
|
1903
|
-
todos: [{ content: "A", status: "pending" }],
|
|
1904
|
-
}),
|
|
1905
|
-
);
|
|
1906
|
-
sb.processEvent(toolEndEvent("run-1", { error: "boom" }));
|
|
1907
|
-
expect(Object.keys(sb.currentStatus.todos)).toHaveLength(0);
|
|
1908
|
-
});
|
|
1909
|
-
|
|
1910
|
-
it("full-replaces the map on a subsequent write_todos", () => {
|
|
1911
|
-
const sb = makeBuilder();
|
|
1912
|
-
sb.processEvent(
|
|
1913
|
-
toolStartEvent("run-1", "write_todos", {
|
|
1914
|
-
todos: [
|
|
1915
|
-
{ content: "one", status: "completed" },
|
|
1916
|
-
{ content: "two", status: "in_progress" },
|
|
1917
|
-
],
|
|
1918
|
-
}),
|
|
1919
|
-
);
|
|
1920
|
-
sb.processEvent(toolEndEvent("run-1", "ok"));
|
|
1921
|
-
sb.processEvent(
|
|
1922
|
-
toolStartEvent("run-2", "write_todos", {
|
|
1923
|
-
todos: [{ content: "two", status: "completed" }],
|
|
1924
|
-
}),
|
|
1925
|
-
);
|
|
1926
|
-
sb.processEvent(toolEndEvent("run-2", "ok"));
|
|
1927
|
-
|
|
1928
|
-
const todos = sb.currentStatus.todos;
|
|
1929
|
-
expect(Object.keys(todos)).toEqual(["todo-0"]);
|
|
1930
|
-
expect(todos["todo-0"].status).toBe(TodoStatus.TODO_COMPLETED);
|
|
1931
|
-
});
|
|
1932
|
-
});
|
|
1933
|
-
});
|