@cuongtran001/kanna 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc-BLk1CZKB.js → arc-DXtN3EWJ.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-B01ZQYte.js → architectureDiagram-3BPJPVTR-CIGqW1z-.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-diL6a3cq.js → blockDiagram-GPEHLZMM-BafNS_H6.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-BY1verL8.js → c4Diagram-AAUBKEIU-YdAgRSis.js} +1 -1
- package/dist/client/assets/channel-BEYTrUAm.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-psdZuajw.js → chunk-2J33WTMH-eiP5y5G-.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-CrwQVBc5.js → chunk-4BX2VUAB-CvLGzPMF.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-9-aj4jKO.js → chunk-55IACEB6-Y-S9Fb8A.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-1FNK9_nB.js → chunk-727SXJPM-6sJH1RI9.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-DFhvd_Ek.js → chunk-AQP2D5EJ-hV1r_IXO.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-B7MLrDa7.js → chunk-FMBD7UC4-CC3L28S5.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-D4Df05QE.js → chunk-ND2GUHAM-BZ3czhdH.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-Iq17Dznm.js → chunk-QZHKN3VN-Bjyd6Yr-.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-CvnF5Ud6.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-CvnF5Ud6.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-BmaZhtXy.js → cose-bilkent-S5V4N54A-CfxdDghE.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-BCmpfnfP.js → dagre-BM42HDAG-B8CddsbB.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-BIEcKowS.js → diagram-2AECGRRQ-F0yS0fVB.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-CRBABBfv.js → diagram-5GNKFQAL-cW0i0qlh.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-BRxUk16N.js → diagram-KO2AKTUF-BzAKMeJT.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DPBs4xl1.js → diagram-LMA3HP47-qdC4XqIl.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-BE9rgVv8.js → diagram-OG6HWLK6-CGcpi6Dh.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-D3vgA1xJ.js → erDiagram-TEJ5UH35-CxNTDicM.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-Cgu-8XXC.js → flowDiagram-I6XJVG4X-qkGa5TqY.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CgjXVyTR.js → ganttDiagram-6RSMTGT7-D9mVPoQi.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-zD5D8B-i.js → gitGraphDiagram-PVQCEYII-DA0grAXe.js} +1 -1
- package/dist/client/assets/{index-DBTdhMbJ.js → index-BqUY1Bm4.js} +1 -1
- package/dist/client/assets/index-CLsOWXff.css +32 -0
- package/dist/client/assets/{index-zQLLeAfl.js → index-TU1k6ov6.js} +253 -243
- package/dist/client/assets/{infoDiagram-5YYISTIA-BaFHqT4u.js → infoDiagram-5YYISTIA-CGljtLs3.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-2yAC1br2.js → ishikawaDiagram-YF4QCWOH-CtC0xrmG.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-DacQ-ND5.js → journeyDiagram-JHISSGLW-URLnVZeN.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-Qlbwm3z4.js → kanban-definition-UN3LZRKU-C2Q6aqQh.js} +1 -1
- package/dist/client/assets/{linear-DeafqpgO.js → linear-ByKHtsIx.js} +1 -1
- package/dist/client/assets/{mermaid.core-KYiCPJDj.js → mermaid.core-Dl_f2KLc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-B1V-T6d3.js → mindmap-definition-RKZ34NQL-CaJ9IDLI.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-DOF9OFRu.js → pieDiagram-4H26LBE5-mmYaf2Oa.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-lH1mB7ko.js → quadrantDiagram-W4KKPZXB-B_PO6OCC.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-BaTsJ_sS.js → requirementDiagram-4Y6WPE33-M4huwJt1.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-Pfxib5iI.js → sankeyDiagram-5OEKKPKP-B0hb1sWh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-BQMEKkBC.js → sequenceDiagram-3UESZ5HK-BAqSptnA.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-CuldNnzp.js → stateDiagram-AJRCARHV-DTNkvPi5.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CVN3oGTF.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-DP7u0YAg.js → timeline-definition-PNZ67QCA-MgAjHz8Q.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN-QlrjpNeQ.js → vennDiagram-CIIHVFJN-DmKoO51W.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-BqdJ_sAM.js → wardley-L42UT6IY-CWcyHnYs.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Wi3kaQ5X.js → wardleyDiagram-YWT4CUSO-BGIGmFP9.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-D-KXMyCG.js → xychartDiagram-2RQKCTM6-D20YIvEl.js} +1 -1
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent-coordinator-types.ts +158 -0
- package/src/server/agent-coordinator.ts +919 -0
- package/src/server/agent-deps-builders.ts +425 -0
- package/src/server/agent.oauth-rotation.test.ts +101 -0
- package/src/server/agent.test.ts +24 -8
- package/src/server/agent.ts +35 -4963
- package/src/server/auto-continue/read-model.test.ts +2 -2
- package/src/server/auto-continue/read-model.ts +4 -1
- package/src/server/claude-autocontinue-commands.test.ts +494 -0
- package/src/server/claude-autocontinue-commands.ts +256 -0
- package/src/server/claude-cancel-handler.test.ts +472 -0
- package/src/server/claude-cancel-handler.ts +241 -0
- package/src/server/claude-chat-management.test.ts +395 -0
- package/src/server/claude-chat-management.ts +251 -0
- package/src/server/claude-harness-stream.test.ts +273 -0
- package/src/server/claude-harness-stream.ts +158 -0
- package/src/server/claude-loop-orch-commands.test.ts +579 -0
- package/src/server/claude-loop-orch-commands.ts +466 -0
- package/src/server/claude-message-normalizer.test.ts +213 -0
- package/src/server/claude-message-normalizer.ts +436 -0
- package/src/server/claude-prompt-helpers.test.ts +248 -0
- package/src/server/claude-prompt-helpers.ts +139 -0
- package/src/server/claude-sdk-queue.test.ts +169 -0
- package/src/server/claude-sdk-queue.ts +82 -0
- package/src/server/claude-send-command.test.ts +414 -0
- package/src/server/claude-send-command.ts +420 -0
- package/src/server/claude-session-config-helpers.test.ts +279 -0
- package/src/server/claude-session-config-helpers.ts +151 -0
- package/src/server/claude-session-config.test.ts +250 -0
- package/src/server/claude-session-config.ts +159 -0
- package/src/server/claude-session-error-handler.test.ts +498 -0
- package/src/server/claude-session-error-handler.ts +361 -0
- package/src/server/claude-session-lifecycle.test.ts +669 -0
- package/src/server/claude-session-lifecycle.ts +283 -0
- package/src/server/claude-session-rebuild.test.ts +224 -0
- package/src/server/claude-session-rebuild.ts +101 -0
- package/src/server/claude-session-runner.test.ts +523 -0
- package/src/server/claude-session-runner.ts +521 -0
- package/src/server/claude-session-spawner.test.ts +401 -0
- package/src/server/claude-session-spawner.ts +401 -0
- package/src/server/claude-session-start.test.ts +205 -0
- package/src/server/claude-session-start.ts +277 -0
- package/src/server/claude-session-state-queries.test.ts +344 -0
- package/src/server/claude-session-state-queries.ts +160 -0
- package/src/server/claude-session-state.ts +104 -0
- package/src/server/claude-slash-commands.test.ts +277 -0
- package/src/server/claude-slash-commands.ts +234 -0
- package/src/server/claude-spawn-helpers.test.ts +221 -0
- package/src/server/claude-spawn-helpers.ts +179 -0
- package/src/server/claude-steer-log.test.ts +243 -0
- package/src/server/claude-steer-log.ts +58 -0
- package/src/server/claude-subagent-tool-response.test.ts +213 -0
- package/src/server/claude-subagent-tool-response.ts +165 -0
- package/src/server/claude-subagent-wiring.test.ts +263 -0
- package/src/server/claude-subagent-wiring.ts +324 -0
- package/src/server/claude-tool-respond.test.ts +321 -0
- package/src/server/claude-tool-respond.ts +142 -0
- package/src/server/claude-turn-runner.test.ts +310 -0
- package/src/server/claude-turn-runner.ts +228 -0
- package/src/server/claude-turn-starter.test.ts +266 -0
- package/src/server/claude-turn-starter.ts +566 -0
- package/src/server/claude-usage-math.test.ts +169 -0
- package/src/server/claude-usage-math.ts +132 -0
- package/src/server/diff-store-branch-ops.adapter.test.ts +332 -0
- package/src/server/diff-store-branch-ops.adapter.ts +602 -0
- package/src/server/diff-store-commit-ops.adapter.test.ts +358 -0
- package/src/server/diff-store-commit-ops.adapter.ts +258 -0
- package/src/server/diff-store-errors.ts +85 -0
- package/src/server/diff-store-file-ops.adapter.ts +212 -0
- package/src/server/diff-store-git-branch.adapter.ts +686 -0
- package/src/server/diff-store-io.adapter.ts +27 -0
- package/src/server/diff-store-parse.test.ts +212 -0
- package/src/server/diff-store-parse.ts +122 -0
- package/src/server/diff-store-state.ts +82 -0
- package/src/server/diff-store.ts +76 -1854
- package/src/server/event-store-apply.test.ts +280 -0
- package/src/server/event-store-apply.ts +132 -0
- package/src/server/event-store-chat-lifecycle.test.ts +408 -0
- package/src/server/event-store-chat-lifecycle.ts +441 -0
- package/src/server/event-store-entity-write.ts +356 -0
- package/src/server/event-store-helpers.test.ts +230 -0
- package/src/server/event-store-helpers.ts +235 -0
- package/src/server/event-store-init.ts +276 -0
- package/src/server/event-store-messages.adapter.test.ts +365 -0
- package/src/server/event-store-messages.adapter.ts +213 -0
- package/src/server/event-store-orch-subscription.ts +124 -0
- package/src/server/event-store-orch.test.ts +462 -0
- package/src/server/event-store-orch.ts +312 -0
- package/src/server/event-store-peripheral-events.adapter.test.ts +254 -0
- package/src/server/event-store-peripheral-events.adapter.ts +105 -0
- package/src/server/event-store-snapshot.test.ts +597 -0
- package/src/server/event-store-snapshot.ts +658 -0
- package/src/server/event-store-subagent.test.ts +408 -0
- package/src/server/event-store-subagent.ts +198 -0
- package/src/server/event-store-tool-requests.test.ts +210 -0
- package/src/server/event-store-tool-requests.ts +131 -0
- package/src/server/event-store-transcript-write.adapter.ts +285 -0
- package/src/server/event-store-write-ops.test.ts +554 -0
- package/src/server/event-store-write-ops.ts +529 -0
- package/src/server/event-store.test.ts +35 -0
- package/src/server/event-store.ts +251 -2284
- package/src/server/events.ts +3 -0
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/read-models.ts +26 -1
- package/src/server/subagent-orchestrator.ts +3 -0
- package/src/server/ws-router-agent-ctrl.test.ts +187 -0
- package/src/server/ws-router-agent-ctrl.ts +137 -0
- package/src/server/ws-router-broadcast.ts +488 -0
- package/src/server/ws-router-chat.test.ts +461 -0
- package/src/server/ws-router-chat.ts +279 -0
- package/src/server/ws-router-defaults.test.ts +283 -0
- package/src/server/ws-router-defaults.ts +434 -0
- package/src/server/ws-router-diff.test.ts +167 -0
- package/src/server/ws-router-diff.ts +250 -0
- package/src/server/ws-router-envelope.ts +329 -0
- package/src/server/ws-router-misc.test.ts +422 -0
- package/src/server/ws-router-misc.ts +265 -0
- package/src/server/ws-router-orch.test.ts +190 -0
- package/src/server/ws-router-orch.ts +93 -0
- package/src/server/ws-router-project.test.ts +403 -0
- package/src/server/ws-router-project.ts +256 -0
- package/src/server/ws-router-push.test.ts +193 -0
- package/src/server/ws-router-push.ts +120 -0
- package/src/server/ws-router-settings.test.ts +173 -0
- package/src/server/ws-router-settings.ts +416 -0
- package/src/server/ws-router-skills.test.ts +194 -0
- package/src/server/ws-router-skills.ts +204 -0
- package/src/server/ws-router-utils.ts +224 -0
- package/src/server/ws-router.ts +339 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/loop-progress.test.ts +106 -0
- package/src/shared/loop-progress.ts +88 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/provider-model-types.ts +437 -0
- package/src/shared/subagent-types.ts +198 -0
- package/src/shared/tool-call-types.ts +314 -0
- package/src/shared/transcript-types.ts +270 -0
- package/src/shared/types.ts +27 -1668
- package/dist/client/assets/channel-BMkhjiLI.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-C5Kg1XOD.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-C5Kg1XOD.js +0 -1
- package/dist/client/assets/index-H6C-f2Vp.css +0 -32
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ca2U11af.js +0 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
isClaudeSteerLoggingEnabled,
|
|
4
|
+
isSendToStartingProfilingEnabled,
|
|
5
|
+
elapsedProfileMs,
|
|
6
|
+
logClaudeSteer,
|
|
7
|
+
logSendToStartingProfile,
|
|
8
|
+
type SendToStartingProfile,
|
|
9
|
+
} from "./claude-steer-log"
|
|
10
|
+
import { log } from "../shared/log"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function withEnv(key: string, value: string | undefined, fn: () => void) {
|
|
17
|
+
const previous = process.env[key]
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
delete process.env[key]
|
|
20
|
+
} else {
|
|
21
|
+
process.env[key] = value
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
fn()
|
|
25
|
+
} finally {
|
|
26
|
+
if (previous === undefined) {
|
|
27
|
+
delete process.env[key]
|
|
28
|
+
} else {
|
|
29
|
+
process.env[key] = previous
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// isClaudeSteerLoggingEnabled
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe("isClaudeSteerLoggingEnabled", () => {
|
|
39
|
+
it("returns false when env var is unset", () => {
|
|
40
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", undefined, () => {
|
|
41
|
+
expect(isClaudeSteerLoggingEnabled()).toBe(false)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("returns false when env var is '0'", () => {
|
|
46
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", "0", () => {
|
|
47
|
+
expect(isClaudeSteerLoggingEnabled()).toBe(false)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it("returns false when env var is any non-'1' string", () => {
|
|
52
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", "true", () => {
|
|
53
|
+
expect(isClaudeSteerLoggingEnabled()).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("returns true when env var is '1'", () => {
|
|
58
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", "1", () => {
|
|
59
|
+
expect(isClaudeSteerLoggingEnabled()).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// isSendToStartingProfilingEnabled
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("isSendToStartingProfilingEnabled", () => {
|
|
69
|
+
it("returns false when env var is unset", () => {
|
|
70
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", undefined, () => {
|
|
71
|
+
expect(isSendToStartingProfilingEnabled()).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("returns true when env var is '1'", () => {
|
|
76
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", "1", () => {
|
|
77
|
+
expect(isSendToStartingProfilingEnabled()).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("returns false when env var is '0'", () => {
|
|
82
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", "0", () => {
|
|
83
|
+
expect(isSendToStartingProfilingEnabled()).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// elapsedProfileMs
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
describe("elapsedProfileMs", () => {
|
|
93
|
+
it("returns a non-negative number", () => {
|
|
94
|
+
const start = performance.now()
|
|
95
|
+
const elapsed = elapsedProfileMs(start)
|
|
96
|
+
expect(elapsed).toBeGreaterThanOrEqual(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("returns a number (not NaN)", () => {
|
|
100
|
+
const elapsed = elapsedProfileMs(performance.now())
|
|
101
|
+
expect(Number.isNaN(elapsed)).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("returns a value with at most 1 decimal place", () => {
|
|
105
|
+
const elapsed = elapsedProfileMs(performance.now() - 100)
|
|
106
|
+
// toFixed(1) means at most one decimal digit
|
|
107
|
+
expect(elapsed.toString()).toMatch(/^\d+(\.\d)?$/)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("returns a larger value for an earlier startedAt", () => {
|
|
111
|
+
const earlier = performance.now() - 200
|
|
112
|
+
const later = performance.now() - 50
|
|
113
|
+
expect(elapsedProfileMs(earlier)).toBeGreaterThanOrEqual(elapsedProfileMs(later))
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// logClaudeSteer
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
describe("logClaudeSteer", () => {
|
|
122
|
+
it("does not call log.info when logging is disabled", () => {
|
|
123
|
+
const spy = mock(() => {})
|
|
124
|
+
const original = log.info
|
|
125
|
+
log.info = spy
|
|
126
|
+
try {
|
|
127
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", undefined, () => {
|
|
128
|
+
logClaudeSteer("test_stage", { foo: "bar" })
|
|
129
|
+
})
|
|
130
|
+
expect(spy).not.toHaveBeenCalled()
|
|
131
|
+
} finally {
|
|
132
|
+
log.info = original
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it("calls log.info with stage and details when enabled", () => {
|
|
137
|
+
const calls: Parameters<typeof log.info>[] = []
|
|
138
|
+
const original = log.info
|
|
139
|
+
log.info = (...args: Parameters<typeof log.info>) => { calls.push(args) }
|
|
140
|
+
try {
|
|
141
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", "1", () => {
|
|
142
|
+
logClaudeSteer("my_stage", { key: "val" })
|
|
143
|
+
})
|
|
144
|
+
expect(calls.length).toBe(1)
|
|
145
|
+
const [prefix, json] = calls[0]
|
|
146
|
+
expect(prefix).toBe("[kanna/claude-steer]")
|
|
147
|
+
const parsed = JSON.parse(json as string)
|
|
148
|
+
expect(parsed.stage).toBe("my_stage")
|
|
149
|
+
expect(parsed.key).toBe("val")
|
|
150
|
+
} finally {
|
|
151
|
+
log.info = original
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it("calls log.info with only stage when details omitted", () => {
|
|
156
|
+
const calls: Parameters<typeof log.info>[] = []
|
|
157
|
+
const original = log.info
|
|
158
|
+
log.info = (...args: Parameters<typeof log.info>) => { calls.push(args) }
|
|
159
|
+
try {
|
|
160
|
+
withEnv("KANNA_LOG_CLAUDE_STEER", "1", () => {
|
|
161
|
+
logClaudeSteer("no_details")
|
|
162
|
+
})
|
|
163
|
+
expect(calls.length).toBe(1)
|
|
164
|
+
const [, json] = calls[0]
|
|
165
|
+
const parsed = JSON.parse(json as string)
|
|
166
|
+
expect(parsed.stage).toBe("no_details")
|
|
167
|
+
} finally {
|
|
168
|
+
log.info = original
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// logSendToStartingProfile
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
describe("logSendToStartingProfile", () => {
|
|
178
|
+
it("is a no-op when profile is null", () => {
|
|
179
|
+
const spy = mock(() => {})
|
|
180
|
+
const original = log.info
|
|
181
|
+
log.info = spy
|
|
182
|
+
try {
|
|
183
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", "1", () => {
|
|
184
|
+
logSendToStartingProfile(null, "some_stage")
|
|
185
|
+
})
|
|
186
|
+
expect(spy).not.toHaveBeenCalled()
|
|
187
|
+
} finally {
|
|
188
|
+
log.info = original
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("is a no-op when profile is undefined", () => {
|
|
193
|
+
const spy = mock(() => {})
|
|
194
|
+
const original = log.info
|
|
195
|
+
log.info = spy
|
|
196
|
+
try {
|
|
197
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", "1", () => {
|
|
198
|
+
logSendToStartingProfile(undefined, "some_stage")
|
|
199
|
+
})
|
|
200
|
+
expect(spy).not.toHaveBeenCalled()
|
|
201
|
+
} finally {
|
|
202
|
+
log.info = original
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("is a no-op when profiling env var is not set", () => {
|
|
207
|
+
const spy = mock(() => {})
|
|
208
|
+
const original = log.info
|
|
209
|
+
log.info = spy
|
|
210
|
+
try {
|
|
211
|
+
const profile: SendToStartingProfile = { traceId: "abc", startedAt: performance.now() }
|
|
212
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", undefined, () => {
|
|
213
|
+
logSendToStartingProfile(profile, "some_stage")
|
|
214
|
+
})
|
|
215
|
+
expect(spy).not.toHaveBeenCalled()
|
|
216
|
+
} finally {
|
|
217
|
+
log.info = original
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("logs traceId, stage, and elapsedMs when profile is provided and enabled", () => {
|
|
222
|
+
const calls: Parameters<typeof log.info>[] = []
|
|
223
|
+
const original = log.info
|
|
224
|
+
log.info = (...args: Parameters<typeof log.info>) => { calls.push(args) }
|
|
225
|
+
try {
|
|
226
|
+
const profile: SendToStartingProfile = { traceId: "trace-123", startedAt: performance.now() - 50 }
|
|
227
|
+
withEnv("KANNA_PROFILE_SEND_TO_STARTING", "1", () => {
|
|
228
|
+
logSendToStartingProfile(profile, "my_stage", { extra: 42 })
|
|
229
|
+
})
|
|
230
|
+
expect(calls.length).toBe(1)
|
|
231
|
+
const [prefix, json] = calls[0]
|
|
232
|
+
expect(prefix).toBe("[kanna/send->starting][server]")
|
|
233
|
+
const parsed = JSON.parse(json as string)
|
|
234
|
+
expect(parsed.traceId).toBe("trace-123")
|
|
235
|
+
expect(parsed.stage).toBe("my_stage")
|
|
236
|
+
expect(typeof parsed.elapsedMs).toBe("number")
|
|
237
|
+
expect(parsed.elapsedMs).toBeGreaterThanOrEqual(0)
|
|
238
|
+
expect(parsed.extra).toBe(42)
|
|
239
|
+
} finally {
|
|
240
|
+
log.info = original
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { log } from "../shared/log"
|
|
2
|
+
import type { AgentProvider, ModelOptions } from "../shared/types"
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Steer-logging and send-to-starting profiling helpers
|
|
6
|
+
// Extracted from agent.ts — pure env/math utilities, no AgentCoordinator dep.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export interface SendToStartingProfile {
|
|
10
|
+
traceId: string
|
|
11
|
+
startedAt: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isClaudeSteerLoggingEnabled() {
|
|
15
|
+
return process.env.KANNA_LOG_CLAUDE_STEER === "1"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function logClaudeSteer(stage: string, details?: Record<string, unknown>) {
|
|
19
|
+
if (!isClaudeSteerLoggingEnabled()) return
|
|
20
|
+
log.info("[kanna/claude-steer]", JSON.stringify({
|
|
21
|
+
stage,
|
|
22
|
+
...details,
|
|
23
|
+
}))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SendMessageOptions {
|
|
27
|
+
provider?: AgentProvider
|
|
28
|
+
model?: string
|
|
29
|
+
modelOptions?: ModelOptions
|
|
30
|
+
effort?: string
|
|
31
|
+
planMode?: boolean
|
|
32
|
+
autoContinue?: { scheduleId: string }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isSendToStartingProfilingEnabled() {
|
|
36
|
+
return process.env.KANNA_PROFILE_SEND_TO_STARTING === "1"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function elapsedProfileMs(startedAt: number) {
|
|
40
|
+
return Number((performance.now() - startedAt).toFixed(1))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function logSendToStartingProfile(
|
|
44
|
+
profile: SendToStartingProfile | null | undefined,
|
|
45
|
+
stage: string,
|
|
46
|
+
details?: Record<string, unknown>
|
|
47
|
+
) {
|
|
48
|
+
if (!profile || !isSendToStartingProfilingEnabled()) {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
log.info("[kanna/send->starting][server]", JSON.stringify({
|
|
53
|
+
traceId: profile.traceId,
|
|
54
|
+
stage,
|
|
55
|
+
elapsedMs: elapsedProfileMs(profile.startedAt),
|
|
56
|
+
...details,
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
subagentPendingKey,
|
|
4
|
+
rejectPendingResolvers,
|
|
5
|
+
rejectPendingResolversForChat,
|
|
6
|
+
rejectPendingResolversForRun,
|
|
7
|
+
respondSubagentTool,
|
|
8
|
+
cancelSubagentRun,
|
|
9
|
+
type SubagentToolResponseDeps,
|
|
10
|
+
} from "./claude-subagent-tool-response"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function makeResolver() {
|
|
17
|
+
let resolveFn: ((v: unknown) => void) | undefined
|
|
18
|
+
let rejectFn: ((e: Error) => void) | undefined
|
|
19
|
+
const promise = new Promise<unknown>((res, rej) => {
|
|
20
|
+
resolveFn = res
|
|
21
|
+
rejectFn = rej
|
|
22
|
+
})
|
|
23
|
+
// Suppress unhandled-rejection noise in tests
|
|
24
|
+
promise.catch(() => undefined)
|
|
25
|
+
return {
|
|
26
|
+
promise,
|
|
27
|
+
resolve: (v: unknown) => resolveFn!(v),
|
|
28
|
+
reject: (e: Error) => rejectFn!(e),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeDeps(
|
|
33
|
+
overrides?: Partial<SubagentToolResponseDeps>,
|
|
34
|
+
): SubagentToolResponseDeps {
|
|
35
|
+
return {
|
|
36
|
+
subagentPendingResolvers: new Map(),
|
|
37
|
+
store: {
|
|
38
|
+
appendSubagentEvent: mock(() => Promise.resolve()),
|
|
39
|
+
},
|
|
40
|
+
subagentOrchestrator: {
|
|
41
|
+
notifySubagentToolResolved: mock(() => undefined),
|
|
42
|
+
cancelRun: mock(() => undefined),
|
|
43
|
+
},
|
|
44
|
+
emitStateChange: mock(() => undefined),
|
|
45
|
+
...overrides,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// subagentPendingKey
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
describe("subagentPendingKey", () => {
|
|
54
|
+
it("concatenates chatId, runId, and toolUseId with ::", () => {
|
|
55
|
+
expect(subagentPendingKey("chat1", "run1", "tool1")).toBe("chat1::run1::tool1")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("handles empty segments", () => {
|
|
59
|
+
expect(subagentPendingKey("", "", "")).toBe("::::")
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// rejectPendingResolvers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
describe("rejectPendingResolvers", () => {
|
|
68
|
+
it("rejects entries matching predicate and removes them from the map", async () => {
|
|
69
|
+
const deps = makeDeps()
|
|
70
|
+
const r1 = makeResolver()
|
|
71
|
+
const r2 = makeResolver()
|
|
72
|
+
deps.subagentPendingResolvers.set("chat1::run1::t1", r1)
|
|
73
|
+
deps.subagentPendingResolvers.set("chat2::run2::t2", r2)
|
|
74
|
+
|
|
75
|
+
rejectPendingResolvers(
|
|
76
|
+
deps,
|
|
77
|
+
(k) => k.startsWith("chat1::"),
|
|
78
|
+
"test reason",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
await expect(r1.promise).rejects.toThrow("test reason")
|
|
82
|
+
expect(deps.subagentPendingResolvers.has("chat1::run1::t1")).toBe(false)
|
|
83
|
+
// Non-matching entry untouched
|
|
84
|
+
expect(deps.subagentPendingResolvers.has("chat2::run2::t2")).toBe(true)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it("does nothing when no entries match predicate", () => {
|
|
88
|
+
const deps = makeDeps()
|
|
89
|
+
const r = makeResolver()
|
|
90
|
+
deps.subagentPendingResolvers.set("chat1::run1::t1", r)
|
|
91
|
+
|
|
92
|
+
rejectPendingResolvers(deps, () => false, "ignored")
|
|
93
|
+
|
|
94
|
+
expect(deps.subagentPendingResolvers.size).toBe(1)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// rejectPendingResolversForChat
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
describe("rejectPendingResolversForChat", () => {
|
|
103
|
+
it("rejects all resolvers belonging to the given chatId", async () => {
|
|
104
|
+
const deps = makeDeps()
|
|
105
|
+
const r1 = makeResolver()
|
|
106
|
+
const r2 = makeResolver()
|
|
107
|
+
const rOther = makeResolver()
|
|
108
|
+
deps.subagentPendingResolvers.set("chatA::run1::t1", r1)
|
|
109
|
+
deps.subagentPendingResolvers.set("chatA::run2::t2", r2)
|
|
110
|
+
deps.subagentPendingResolvers.set("chatB::run1::t1", rOther)
|
|
111
|
+
|
|
112
|
+
rejectPendingResolversForChat(deps, "chatA")
|
|
113
|
+
|
|
114
|
+
await expect(r1.promise).rejects.toThrow("chat cancelled")
|
|
115
|
+
await expect(r2.promise).rejects.toThrow("chat cancelled")
|
|
116
|
+
expect(deps.subagentPendingResolvers.has("chatB::run1::t1")).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// rejectPendingResolversForRun
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("rejectPendingResolversForRun", () => {
|
|
125
|
+
it("rejects all resolvers belonging to the given run", async () => {
|
|
126
|
+
const deps = makeDeps()
|
|
127
|
+
const rTarget = makeResolver()
|
|
128
|
+
const rOtherRun = makeResolver()
|
|
129
|
+
deps.subagentPendingResolvers.set("chatA::run1::t1", rTarget)
|
|
130
|
+
deps.subagentPendingResolvers.set("chatA::run2::t1", rOtherRun)
|
|
131
|
+
|
|
132
|
+
rejectPendingResolversForRun(deps, "chatA", "run1")
|
|
133
|
+
|
|
134
|
+
await expect(rTarget.promise).rejects.toThrow("subagent run terminated")
|
|
135
|
+
expect(deps.subagentPendingResolvers.has("chatA::run2::t1")).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// respondSubagentTool
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
describe("respondSubagentTool", () => {
|
|
144
|
+
it("resolves the pending resolver, appends event, notifies orchestrator, emits state", async () => {
|
|
145
|
+
const deps = makeDeps()
|
|
146
|
+
const resolver = makeResolver()
|
|
147
|
+
deps.subagentPendingResolvers.set("chatX::runY::toolZ", resolver)
|
|
148
|
+
|
|
149
|
+
const command = {
|
|
150
|
+
type: "chat.respondSubagentTool" as const,
|
|
151
|
+
chatId: "chatX",
|
|
152
|
+
runId: "runY",
|
|
153
|
+
toolUseId: "toolZ",
|
|
154
|
+
result: { content: "ok" },
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await respondSubagentTool(deps, command)
|
|
158
|
+
|
|
159
|
+
// Resolver resolved with the result
|
|
160
|
+
await expect(resolver.promise).resolves.toEqual({ content: "ok" })
|
|
161
|
+
// Removed from the map
|
|
162
|
+
expect(deps.subagentPendingResolvers.has("chatX::runY::toolZ")).toBe(false)
|
|
163
|
+
// Event persisted
|
|
164
|
+
expect(deps.store.appendSubagentEvent).toHaveBeenCalledWith(
|
|
165
|
+
expect.objectContaining({
|
|
166
|
+
type: "subagent_tool_resolved",
|
|
167
|
+
chatId: "chatX",
|
|
168
|
+
runId: "runY",
|
|
169
|
+
toolUseId: "toolZ",
|
|
170
|
+
result: { content: "ok" },
|
|
171
|
+
resolution: "user",
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
// Orchestrator notified
|
|
175
|
+
expect(deps.subagentOrchestrator.notifySubagentToolResolved).toHaveBeenCalledWith("runY")
|
|
176
|
+
// State emitted
|
|
177
|
+
expect(deps.emitStateChange).toHaveBeenCalledWith("chatX")
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it("is idempotent: does nothing when resolver is not found", async () => {
|
|
181
|
+
const deps = makeDeps()
|
|
182
|
+
|
|
183
|
+
await respondSubagentTool(deps, {
|
|
184
|
+
type: "chat.respondSubagentTool",
|
|
185
|
+
chatId: "chatX",
|
|
186
|
+
runId: "runY",
|
|
187
|
+
toolUseId: "toolZ",
|
|
188
|
+
result: {},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(deps.store.appendSubagentEvent).not.toHaveBeenCalled()
|
|
192
|
+
expect(deps.subagentOrchestrator.notifySubagentToolResolved).not.toHaveBeenCalled()
|
|
193
|
+
expect(deps.emitStateChange).not.toHaveBeenCalled()
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// cancelSubagentRun
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
describe("cancelSubagentRun", () => {
|
|
202
|
+
it("delegates to orchestrator.cancelRun", () => {
|
|
203
|
+
const deps = makeDeps()
|
|
204
|
+
|
|
205
|
+
cancelSubagentRun(deps, {
|
|
206
|
+
type: "chat.cancelSubagentRun",
|
|
207
|
+
chatId: "chatA",
|
|
208
|
+
runId: "runB",
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
expect(deps.subagentOrchestrator.cancelRun).toHaveBeenCalledWith("chatA", "runB")
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone functions for managing subagent pending tool-response resolvers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from AgentCoordinator to keep agent.ts below the 600-LOC target.
|
|
5
|
+
* No direct IO — all side-effects are injected via SubagentToolResponseDeps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnyValue } from "../shared/errors"
|
|
9
|
+
import type { SubagentRunEvent } from "./events"
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Deps interface
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/** Minimal surface of SubagentOrchestrator required by this module. */
|
|
16
|
+
export interface SubagentToolResponseOrchestratorDeps {
|
|
17
|
+
notifySubagentToolResolved(runId: string): void
|
|
18
|
+
cancelRun(chatId: string, runId: string): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Minimal surface of EventStore required by this module. */
|
|
22
|
+
export interface SubagentToolResponseStoreDeps {
|
|
23
|
+
appendSubagentEvent(event: SubagentRunEvent): Promise<void>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Dependency bag injected into every function in this module. */
|
|
27
|
+
export interface SubagentToolResponseDeps {
|
|
28
|
+
/** The shared in-memory resolver map owned by AgentCoordinator. */
|
|
29
|
+
subagentPendingResolvers: Map<
|
|
30
|
+
string,
|
|
31
|
+
{ resolve: (v: AnyValue) => void; reject: (e: Error) => void }
|
|
32
|
+
>
|
|
33
|
+
store: SubagentToolResponseStoreDeps
|
|
34
|
+
subagentOrchestrator: SubagentToolResponseOrchestratorDeps
|
|
35
|
+
emitStateChange: (chatId: string) => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Command shape re-exports (inline to avoid protocol import cycle)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export type RespondSubagentToolCommand = {
|
|
43
|
+
type: "chat.respondSubagentTool"
|
|
44
|
+
chatId: string
|
|
45
|
+
runId: string
|
|
46
|
+
toolUseId: string
|
|
47
|
+
result: Record<string, unknown>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type CancelSubagentRunCommand = {
|
|
51
|
+
type: "chat.cancelSubagentRun"
|
|
52
|
+
chatId: string
|
|
53
|
+
runId: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Pure helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** Builds the map key used to look up a pending subagent tool resolver. */
|
|
61
|
+
export function subagentPendingKey(
|
|
62
|
+
chatId: string,
|
|
63
|
+
runId: string,
|
|
64
|
+
toolUseId: string,
|
|
65
|
+
): string {
|
|
66
|
+
return `${chatId}::${runId}::${toolUseId}`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Resolver management
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Rejects all pending resolvers whose key satisfies `predicate`.
|
|
75
|
+
* Mutates `deps.subagentPendingResolvers` in place.
|
|
76
|
+
*/
|
|
77
|
+
export function rejectPendingResolvers(
|
|
78
|
+
deps: Pick<SubagentToolResponseDeps, "subagentPendingResolvers">,
|
|
79
|
+
predicate: (key: string) => boolean,
|
|
80
|
+
reason: string,
|
|
81
|
+
): void {
|
|
82
|
+
for (const [key, resolver] of deps.subagentPendingResolvers) {
|
|
83
|
+
if (!predicate(key)) continue
|
|
84
|
+
deps.subagentPendingResolvers.delete(key)
|
|
85
|
+
resolver.reject(new Error(reason))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Rejects all pending resolvers that belong to `chatId`. */
|
|
90
|
+
export function rejectPendingResolversForChat(
|
|
91
|
+
deps: Pick<SubagentToolResponseDeps, "subagentPendingResolvers">,
|
|
92
|
+
chatId: string,
|
|
93
|
+
): void {
|
|
94
|
+
const prefix = `${chatId}::`
|
|
95
|
+
rejectPendingResolvers(deps, (k) => k.startsWith(prefix), "chat cancelled")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Rejects all pending resolvers that belong to a specific run within `chatId`. */
|
|
99
|
+
export function rejectPendingResolversForRun(
|
|
100
|
+
deps: Pick<SubagentToolResponseDeps, "subagentPendingResolvers">,
|
|
101
|
+
chatId: string,
|
|
102
|
+
runId: string,
|
|
103
|
+
): void {
|
|
104
|
+
const prefix = `${chatId}::${runId}::`
|
|
105
|
+
rejectPendingResolvers(
|
|
106
|
+
deps,
|
|
107
|
+
(k) => k.startsWith(prefix),
|
|
108
|
+
"subagent run terminated",
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Command handlers
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handles a `chat.respondSubagentTool` command from the client.
|
|
118
|
+
*
|
|
119
|
+
* Resolves the pending resolver for the given tool-use, persists a
|
|
120
|
+
* `subagent_tool_resolved` event, notifies the orchestrator, and emits a
|
|
121
|
+
* state-change.
|
|
122
|
+
*
|
|
123
|
+
* Idempotent: a double-submit (client retry, concurrent WS messages, or a
|
|
124
|
+
* response arriving after the run already terminated) is silently ignored.
|
|
125
|
+
*/
|
|
126
|
+
export async function respondSubagentTool(
|
|
127
|
+
deps: SubagentToolResponseDeps,
|
|
128
|
+
command: RespondSubagentToolCommand,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
const key = subagentPendingKey(command.chatId, command.runId, command.toolUseId)
|
|
131
|
+
const resolver = deps.subagentPendingResolvers.get(key)
|
|
132
|
+
if (!resolver) {
|
|
133
|
+
// Idempotent: a double-submit (client retry, concurrent WS messages, or
|
|
134
|
+
// a response arriving after the run already terminated) should not
|
|
135
|
+
// surface a confusing error to the UI. Resolver-absent = already
|
|
136
|
+
// resolved or run died; nothing to do.
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
deps.subagentPendingResolvers.delete(key)
|
|
140
|
+
await deps.store.appendSubagentEvent({
|
|
141
|
+
v: 3,
|
|
142
|
+
type: "subagent_tool_resolved",
|
|
143
|
+
timestamp: Date.now(),
|
|
144
|
+
chatId: command.chatId,
|
|
145
|
+
runId: command.runId,
|
|
146
|
+
toolUseId: command.toolUseId,
|
|
147
|
+
result: command.result,
|
|
148
|
+
resolution: "user",
|
|
149
|
+
})
|
|
150
|
+
deps.subagentOrchestrator.notifySubagentToolResolved(command.runId)
|
|
151
|
+
resolver.resolve(command.result)
|
|
152
|
+
deps.emitStateChange(command.chatId)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Handles a `chat.cancelSubagentRun` command from the client.
|
|
157
|
+
*
|
|
158
|
+
* Cancels the named run via the orchestrator.
|
|
159
|
+
*/
|
|
160
|
+
export function cancelSubagentRun(
|
|
161
|
+
deps: Pick<SubagentToolResponseDeps, "subagentOrchestrator">,
|
|
162
|
+
command: CancelSubagentRunCommand,
|
|
163
|
+
): void {
|
|
164
|
+
deps.subagentOrchestrator.cancelRun(command.chatId, command.runId)
|
|
165
|
+
}
|