@cuongtran001/kanna 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{arc-CAysfmX5.js → arc-BHj370W7.js} +1 -1
- package/dist/client/assets/{architectureDiagram-3BPJPVTR-BKqRYsm4.js → architectureDiagram-3BPJPVTR-1G88q1PY.js} +1 -1
- package/dist/client/assets/{blockDiagram-GPEHLZMM-Ce7Eorec.js → blockDiagram-GPEHLZMM-D0L3Co5v.js} +1 -1
- package/dist/client/assets/{c4Diagram-AAUBKEIU-CJ77zauH.js → c4Diagram-AAUBKEIU-CQtERl3H.js} +1 -1
- package/dist/client/assets/channel-BP9IMEvd.js +1 -0
- package/dist/client/assets/{chunk-2J33WTMH-Rk_3zLxJ.js → chunk-2J33WTMH-DxOwt09f.js} +1 -1
- package/dist/client/assets/{chunk-4BX2VUAB-DPIz6Nvf.js → chunk-4BX2VUAB-Wry66y9R.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-D-TkbGh2.js → chunk-55IACEB6-D6p-Hi4x.js} +1 -1
- package/dist/client/assets/{chunk-727SXJPM-B4NOSIZV.js → chunk-727SXJPM-CRQZt23N.js} +1 -1
- package/dist/client/assets/{chunk-AQP2D5EJ-Cm_TGLYm.js → chunk-AQP2D5EJ-BzgTKOM9.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-Bu45BUVh.js → chunk-FMBD7UC4-CKfD-91C.js} +1 -1
- package/dist/client/assets/{chunk-ND2GUHAM-C1Bw8_gg.js → chunk-ND2GUHAM-a71B27wc.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-DueG15o0.js → chunk-QZHKN3VN-vv9gJwx0.js} +1 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK--W6Wt7FF.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2--W6Wt7FF.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-YgpMurXf.js → cose-bilkent-S5V4N54A-Cu-dCCaI.js} +1 -1
- package/dist/client/assets/{dagre-BM42HDAG-Dop7Oq67.js → dagre-BM42HDAG-D8coOWLK.js} +1 -1
- package/dist/client/assets/{diagram-2AECGRRQ-unMfBEJH.js → diagram-2AECGRRQ-NIwZ65jE.js} +1 -1
- package/dist/client/assets/{diagram-5GNKFQAL-D9EJ9qMI.js → diagram-5GNKFQAL-d46aE_le.js} +1 -1
- package/dist/client/assets/{diagram-KO2AKTUF-C5IykOIR.js → diagram-KO2AKTUF-xdm2UxWc.js} +1 -1
- package/dist/client/assets/{diagram-LMA3HP47-DJAuPngz.js → diagram-LMA3HP47-hDbLwC5K.js} +1 -1
- package/dist/client/assets/{diagram-OG6HWLK6-B3wHFtS-.js → diagram-OG6HWLK6-MHUGG7ki.js} +1 -1
- package/dist/client/assets/{erDiagram-TEJ5UH35-Dm7kt9o5.js → erDiagram-TEJ5UH35-Ro6n9X70.js} +1 -1
- package/dist/client/assets/{flowDiagram-I6XJVG4X-s9Lds3ny.js → flowDiagram-I6XJVG4X-DOZqvRMu.js} +1 -1
- package/dist/client/assets/{ganttDiagram-6RSMTGT7-CRQNYdAf.js → ganttDiagram-6RSMTGT7-Bn2NXYIy.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-PVQCEYII-DhYjK4Jb.js → gitGraphDiagram-PVQCEYII-DK44bfbC.js} +1 -1
- package/dist/client/assets/{index-C0U2kwuB.js → index-CWJpHyQy.js} +115 -115
- package/dist/client/assets/{index-CLsOWXff.css → index-Cyzd4zZx.css} +1 -1
- package/dist/client/assets/{index-CbSY9HfS.js → index-DU56d4oC.js} +1 -1
- package/dist/client/assets/{infoDiagram-5YYISTIA-CRKJ0PSH.js → infoDiagram-5YYISTIA-C2tW_Dwj.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-YF4QCWOH-CEAkB0IY.js → ishikawaDiagram-YF4QCWOH-C__4Z1wz.js} +1 -1
- package/dist/client/assets/{journeyDiagram-JHISSGLW-CyOK5_DG.js → journeyDiagram-JHISSGLW-Bbn5EawR.js} +1 -1
- package/dist/client/assets/{kanban-definition-UN3LZRKU-BfKjiJSo.js → kanban-definition-UN3LZRKU-RgS5IRWl.js} +1 -1
- package/dist/client/assets/{linear-DLR6miy3.js → linear-DfI0qBO_.js} +1 -1
- package/dist/client/assets/{mermaid.core-D7AdC_lE.js → mermaid.core-BiN10VHc.js} +4 -4
- package/dist/client/assets/{mindmap-definition-RKZ34NQL-zYyJtDSa.js → mindmap-definition-RKZ34NQL-BQhibhh3.js} +1 -1
- package/dist/client/assets/{pieDiagram-4H26LBE5-BgP8PFlo.js → pieDiagram-4H26LBE5-BEfLRn7a.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-W4KKPZXB-Di8h7J96.js → quadrantDiagram-W4KKPZXB-ba641Y2x.js} +1 -1
- package/dist/client/assets/{requirementDiagram-4Y6WPE33-DuTzFl3R.js → requirementDiagram-4Y6WPE33-BGtl-tVO.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-5OEKKPKP-DBRoMm_T.js → sankeyDiagram-5OEKKPKP-DyziaSFr.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-3UESZ5HK-Bmb-1HUN.js → sequenceDiagram-3UESZ5HK-BfYBAYga.js} +1 -1
- package/dist/client/assets/{stateDiagram-AJRCARHV-fnv5ayn-.js → stateDiagram-AJRCARHV-BfUfmLYA.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-CdGcgYEq.js +1 -0
- package/dist/client/assets/{timeline-definition-PNZ67QCA-ByWRdpQX.js → timeline-definition-PNZ67QCA-T6z7dEFl.js} +1 -1
- package/dist/client/assets/{vennDiagram-CIIHVFJN--2WGpVXW.js → vennDiagram-CIIHVFJN-BAwQbFFd.js} +1 -1
- package/dist/client/assets/{wardley-L42UT6IY-24zlnzxK.js → wardley-L42UT6IY-DF1cRKsP.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-YWT4CUSO-Cllvqb3K.js → wardleyDiagram-YWT4CUSO-BRtAZ8za.js} +1 -1
- package/dist/client/assets/{xychartDiagram-2RQKCTM6-ZcdC6nPE.js → xychartDiagram-2RQKCTM6-Brb-q76h.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.test.ts +22 -6
- package/src/server/agent.ts +35 -4987
- package/src/server/chat-op-log.test.ts +63 -0
- package/src/server/chat-op-log.ts +71 -0
- package/src/server/chat-ops-diff.test.ts +120 -0
- package/src/server/chat-ops-diff.ts +97 -0
- package/src/server/chat-ops-parity.test.ts +140 -0
- 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 +532 -0
- package/src/server/event-store-messages.adapter.ts +455 -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 +291 -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 +75 -0
- package/src/server/event-store.ts +257 -2285
- package/src/server/events.ts +1 -1
- package/src/server/harness-types.ts +15 -1
- package/src/server/oauth-errors.ts +19 -0
- package/src/server/storage/backend.ts +7 -0
- package/src/server/storage/fs-storage.adapter.ts +21 -1
- package/src/server/storage/in-memory-storage.ts +12 -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 +560 -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 +360 -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 +238 -0
- package/src/server/ws-router.test.ts +90 -0
- package/src/server/ws-router.ts +340 -2254
- package/src/shared/app-settings-types.ts +365 -0
- package/src/shared/chat-ops.test.ts +138 -0
- package/src/shared/chat-ops.ts +71 -0
- package/src/shared/core-types.ts +27 -0
- package/src/shared/git-diff-types.ts +165 -0
- package/src/shared/mcp-types.ts +105 -0
- package/src/shared/protocol.ts +3 -2
- 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 +31 -1711
- package/dist/client/assets/channel-KQhRwDzJ.js +0 -1
- package/dist/client/assets/classDiagram-4FO5ZUOK-ChxT1GNq.js +0 -1
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-ChxT1GNq.js +0 -1
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-D4X-ZnLV.js +0 -1
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for claude-tool-respond.ts — the extracted respondTool handler.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, mock } from "bun:test"
|
|
5
|
+
import {
|
|
6
|
+
respondTool,
|
|
7
|
+
type RespondToolCommand,
|
|
8
|
+
type ToolRespondDeps,
|
|
9
|
+
} from "./claude-tool-respond"
|
|
10
|
+
import type { PendingToolRequest, ActiveTurn } from "./claude-session-state"
|
|
11
|
+
import type { AnyValue } from "../shared/errors"
|
|
12
|
+
import type { AskUserQuestionToolCall, ExitPlanModeToolCall } from "../shared/types"
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Minimal tool call stubs that satisfy the PendingToolRequest.tool type
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function askUserQuestionTool(toolId: string): AskUserQuestionToolCall {
|
|
19
|
+
return {
|
|
20
|
+
kind: "tool",
|
|
21
|
+
toolKind: "ask_user_question",
|
|
22
|
+
toolName: "ask_user_question",
|
|
23
|
+
toolId,
|
|
24
|
+
input: { questions: [] },
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function exitPlanModeTool(toolId: string): ExitPlanModeToolCall {
|
|
29
|
+
return {
|
|
30
|
+
kind: "tool",
|
|
31
|
+
toolKind: "exit_plan_mode",
|
|
32
|
+
toolName: "exit_plan_mode",
|
|
33
|
+
toolId,
|
|
34
|
+
input: {},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function makeActiveTurn(overrides: Partial<ActiveTurn> = {}): ActiveTurn {
|
|
43
|
+
return {
|
|
44
|
+
chatId: "chat-1",
|
|
45
|
+
provider: "claude",
|
|
46
|
+
turn: {} as ActiveTurn["turn"],
|
|
47
|
+
model: "claude-opus-4-5",
|
|
48
|
+
planMode: true,
|
|
49
|
+
status: "waiting_for_user",
|
|
50
|
+
pendingTool: null,
|
|
51
|
+
postToolFollowUp: null,
|
|
52
|
+
hasFinalResult: false,
|
|
53
|
+
cancelRequested: false,
|
|
54
|
+
cancelRecorded: false,
|
|
55
|
+
waitStartedAt: Date.now(),
|
|
56
|
+
userMessageId: null,
|
|
57
|
+
...overrides,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type AppendMessageFn = (chatId: string, entry: { kind: string }) => Promise<void>
|
|
62
|
+
type SetSessionTokenFn = (
|
|
63
|
+
chatId: string,
|
|
64
|
+
provider: string,
|
|
65
|
+
token: string | null,
|
|
66
|
+
) => Promise<void>
|
|
67
|
+
|
|
68
|
+
function makeDeps(
|
|
69
|
+
activeTurns: Map<string, ActiveTurn>,
|
|
70
|
+
appendMessage = mock(async (_chatId: string, _entry: unknown) => {}),
|
|
71
|
+
setSessionTokenForProvider = mock(
|
|
72
|
+
async (_chatId: string, _provider: string, _token: string | null) => {},
|
|
73
|
+
),
|
|
74
|
+
emitStateChange = mock((_chatId: string) => {}),
|
|
75
|
+
): ToolRespondDeps {
|
|
76
|
+
return {
|
|
77
|
+
activeTurns,
|
|
78
|
+
store: {
|
|
79
|
+
appendMessage: appendMessage as unknown as AppendMessageFn,
|
|
80
|
+
setSessionTokenForProvider: setSessionTokenForProvider as unknown as SetSessionTokenFn,
|
|
81
|
+
},
|
|
82
|
+
emitStateChange,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeCommand(
|
|
87
|
+
overrides: Partial<RespondToolCommand> = {},
|
|
88
|
+
): RespondToolCommand {
|
|
89
|
+
return {
|
|
90
|
+
type: "chat.respondTool",
|
|
91
|
+
chatId: "chat-1",
|
|
92
|
+
toolUseId: "tool-abc",
|
|
93
|
+
result: { confirmed: true } as AnyValue,
|
|
94
|
+
...overrides,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function makePendingTool(
|
|
99
|
+
toolUseId: string,
|
|
100
|
+
tool: PendingToolRequest["tool"],
|
|
101
|
+
resolve: (v: AnyValue) => void,
|
|
102
|
+
): PendingToolRequest {
|
|
103
|
+
return { toolUseId, tool, resolve }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Tests
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe("respondTool", () => {
|
|
111
|
+
it("throws when there is no active turn for the chat", async () => {
|
|
112
|
+
const deps = makeDeps(new Map())
|
|
113
|
+
await expect(respondTool(deps, makeCommand())).rejects.toThrow(
|
|
114
|
+
"No pending tool request",
|
|
115
|
+
)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("throws when the active turn has no pending tool", async () => {
|
|
119
|
+
const active = makeActiveTurn({ pendingTool: null })
|
|
120
|
+
const turns = new Map([["chat-1", active]])
|
|
121
|
+
const deps = makeDeps(turns)
|
|
122
|
+
await expect(respondTool(deps, makeCommand())).rejects.toThrow(
|
|
123
|
+
"No pending tool request",
|
|
124
|
+
)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it("throws when toolUseId does not match the pending request", async () => {
|
|
128
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
129
|
+
const pending = makePendingTool(
|
|
130
|
+
"tool-xyz",
|
|
131
|
+
askUserQuestionTool("tool-xyz"),
|
|
132
|
+
resolve,
|
|
133
|
+
)
|
|
134
|
+
const active = makeActiveTurn({ pendingTool: pending })
|
|
135
|
+
const turns = new Map([["chat-1", active]])
|
|
136
|
+
const deps = makeDeps(turns)
|
|
137
|
+
|
|
138
|
+
await expect(
|
|
139
|
+
respondTool(deps, makeCommand({ toolUseId: "tool-DIFFERENT" })),
|
|
140
|
+
).rejects.toThrow("Tool response does not match active request")
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("resolves an ask_user_question tool and updates active turn state", async () => {
|
|
144
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
145
|
+
const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
|
|
146
|
+
const emitStateChange = mock((_chatId: string) => {})
|
|
147
|
+
const result: AnyValue = { answer: "yes" }
|
|
148
|
+
|
|
149
|
+
const pending = makePendingTool(
|
|
150
|
+
"tool-abc",
|
|
151
|
+
askUserQuestionTool("tool-abc"),
|
|
152
|
+
resolve,
|
|
153
|
+
)
|
|
154
|
+
const active = makeActiveTurn({
|
|
155
|
+
status: "waiting_for_user",
|
|
156
|
+
waitStartedAt: 12345,
|
|
157
|
+
pendingTool: pending,
|
|
158
|
+
})
|
|
159
|
+
const turns = new Map([["chat-1", active]])
|
|
160
|
+
const deps = makeDeps(
|
|
161
|
+
turns,
|
|
162
|
+
appendMessage,
|
|
163
|
+
mock(async (_chatId: string, _provider: string, _token: string | null) => {}),
|
|
164
|
+
emitStateChange,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
await respondTool(deps, makeCommand({ result }))
|
|
168
|
+
|
|
169
|
+
// tool_result appended
|
|
170
|
+
expect(appendMessage).toHaveBeenCalledTimes(1)
|
|
171
|
+
const callArgs = appendMessage.mock.calls[0] as unknown as [
|
|
172
|
+
string,
|
|
173
|
+
{ kind: string },
|
|
174
|
+
]
|
|
175
|
+
expect(callArgs[1].kind).toBe("tool_result")
|
|
176
|
+
|
|
177
|
+
// active turn state updated
|
|
178
|
+
expect(active.pendingTool).toBeNull()
|
|
179
|
+
expect(active.status).toBe("running")
|
|
180
|
+
expect(active.waitStartedAt).toBeNull()
|
|
181
|
+
|
|
182
|
+
// promise resolved
|
|
183
|
+
expect(resolve).toHaveBeenCalledWith(result)
|
|
184
|
+
|
|
185
|
+
// state change emitted
|
|
186
|
+
expect(emitStateChange).toHaveBeenCalledWith("chat-1")
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("clears session token and appends context_cleared when exit_plan_mode confirmed+clearContext", async () => {
|
|
190
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
191
|
+
const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
|
|
192
|
+
const setSessionTokenForProvider = mock(
|
|
193
|
+
async (_chatId: string, _provider: string, _token: string | null) => {},
|
|
194
|
+
)
|
|
195
|
+
const emitStateChange = mock((_chatId: string) => {})
|
|
196
|
+
|
|
197
|
+
const pending = makePendingTool(
|
|
198
|
+
"tool-abc",
|
|
199
|
+
exitPlanModeTool("tool-abc"),
|
|
200
|
+
resolve,
|
|
201
|
+
)
|
|
202
|
+
const active = makeActiveTurn({ provider: "claude", pendingTool: pending })
|
|
203
|
+
const turns = new Map([["chat-1", active]])
|
|
204
|
+
const deps = makeDeps(turns, appendMessage, setSessionTokenForProvider, emitStateChange)
|
|
205
|
+
|
|
206
|
+
await respondTool(
|
|
207
|
+
deps,
|
|
208
|
+
makeCommand({
|
|
209
|
+
result: { confirmed: true, clearContext: true, message: "" } as AnyValue,
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
expect(setSessionTokenForProvider).toHaveBeenCalledWith("chat-1", "claude", null)
|
|
214
|
+
|
|
215
|
+
// appendMessage: tool_result + context_cleared
|
|
216
|
+
expect(appendMessage).toHaveBeenCalledTimes(2)
|
|
217
|
+
const call2 = appendMessage.mock.calls[1] as unknown as [string, { kind: string }]
|
|
218
|
+
expect(call2[1].kind).toBe("context_cleared")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("does NOT clear context when confirmed=false even if clearContext=true", async () => {
|
|
222
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
223
|
+
const appendMessage = mock(async (_chatId: string, _entry: unknown) => {})
|
|
224
|
+
const setSessionTokenForProvider = mock(
|
|
225
|
+
async (_chatId: string, _provider: string, _token: string | null) => {},
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
const pending = makePendingTool(
|
|
229
|
+
"tool-abc",
|
|
230
|
+
exitPlanModeTool("tool-abc"),
|
|
231
|
+
resolve,
|
|
232
|
+
)
|
|
233
|
+
const active = makeActiveTurn({ provider: "claude", pendingTool: pending })
|
|
234
|
+
const turns = new Map([["chat-1", active]])
|
|
235
|
+
const deps = makeDeps(turns, appendMessage, setSessionTokenForProvider)
|
|
236
|
+
|
|
237
|
+
await respondTool(
|
|
238
|
+
deps,
|
|
239
|
+
makeCommand({ result: { confirmed: false, clearContext: true } as AnyValue }),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
expect(setSessionTokenForProvider).not.toHaveBeenCalled()
|
|
243
|
+
// only tool_result appended (no context_cleared)
|
|
244
|
+
expect(appendMessage).toHaveBeenCalledTimes(1)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it("sets postToolFollowUp on codex provider when exit_plan_mode confirmed", async () => {
|
|
248
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
249
|
+
|
|
250
|
+
const pending = makePendingTool(
|
|
251
|
+
"tool-abc",
|
|
252
|
+
exitPlanModeTool("tool-abc"),
|
|
253
|
+
resolve,
|
|
254
|
+
)
|
|
255
|
+
const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
|
|
256
|
+
const turns = new Map([["chat-1", active]])
|
|
257
|
+
const deps = makeDeps(turns)
|
|
258
|
+
|
|
259
|
+
await respondTool(
|
|
260
|
+
deps,
|
|
261
|
+
makeCommand({
|
|
262
|
+
result: { confirmed: true, clearContext: false, message: "great plan" } as AnyValue,
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
expect(active.postToolFollowUp).toEqual({
|
|
267
|
+
content: "Proceed with the approved plan. Additional guidance: great plan",
|
|
268
|
+
planMode: false,
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("sets postToolFollowUp on codex provider when exit_plan_mode rejected", async () => {
|
|
273
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
274
|
+
|
|
275
|
+
const pending = makePendingTool(
|
|
276
|
+
"tool-abc",
|
|
277
|
+
exitPlanModeTool("tool-abc"),
|
|
278
|
+
resolve,
|
|
279
|
+
)
|
|
280
|
+
const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
|
|
281
|
+
const turns = new Map([["chat-1", active]])
|
|
282
|
+
const deps = makeDeps(turns)
|
|
283
|
+
|
|
284
|
+
await respondTool(
|
|
285
|
+
deps,
|
|
286
|
+
makeCommand({
|
|
287
|
+
result: { confirmed: false, clearContext: false, message: "needs work" } as AnyValue,
|
|
288
|
+
}),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
expect(active.postToolFollowUp).toEqual({
|
|
292
|
+
content: "Revise the plan using this feedback: needs work",
|
|
293
|
+
planMode: true,
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it("uses default messages when message field is empty", async () => {
|
|
298
|
+
const resolve = mock((_v: AnyValue) => {})
|
|
299
|
+
|
|
300
|
+
const pending = makePendingTool(
|
|
301
|
+
"tool-abc",
|
|
302
|
+
exitPlanModeTool("tool-abc"),
|
|
303
|
+
resolve,
|
|
304
|
+
)
|
|
305
|
+
const active = makeActiveTurn({ provider: "codex", pendingTool: pending })
|
|
306
|
+
const turns = new Map([["chat-1", active]])
|
|
307
|
+
const deps = makeDeps(turns)
|
|
308
|
+
|
|
309
|
+
await respondTool(
|
|
310
|
+
deps,
|
|
311
|
+
makeCommand({
|
|
312
|
+
result: { confirmed: true, clearContext: false, message: "" } as AnyValue,
|
|
313
|
+
}),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
expect(active.postToolFollowUp).toEqual({
|
|
317
|
+
content: "Proceed with the approved plan.",
|
|
318
|
+
planMode: false,
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone respondTool handler for AgentCoordinator.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from agent.ts so the pending-tool resolution logic lives in its
|
|
5
|
+
* own testable module. The coordinator delegates to `respondTool` by passing
|
|
6
|
+
* an object literal that satisfies `ToolRespondDeps`.
|
|
7
|
+
*
|
|
8
|
+
* Side-effect seal: this module contains NO direct IO (no node:fs, no HTTP
|
|
9
|
+
* calls, no Bun primitives). Every effectful operation is injected through
|
|
10
|
+
* the deps interface.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AgentProvider, TranscriptEntry } from "../shared/types"
|
|
14
|
+
import type { AnyValue } from "../shared/errors"
|
|
15
|
+
import { isRecord } from "../shared/errors"
|
|
16
|
+
import type { ActiveTurn } from "./claude-session-state"
|
|
17
|
+
import { timestamped, normalizeToolContent } from "./claude-message-normalizer"
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Structural sub-interfaces — only the slices this module calls.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Subset of EventStore required by the tool-respond handler. */
|
|
24
|
+
interface ToolRespondStore {
|
|
25
|
+
appendMessage(chatId: string, entry: TranscriptEntry): Promise<void>
|
|
26
|
+
setSessionTokenForProvider(
|
|
27
|
+
chatId: string,
|
|
28
|
+
provider: AgentProvider,
|
|
29
|
+
sessionToken: string | null,
|
|
30
|
+
): Promise<void>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Subset of the activeTurns map used by the tool-respond handler. */
|
|
34
|
+
interface ToolRespondActiveTurnsMap {
|
|
35
|
+
get(chatId: string): ActiveTurn | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Command shape (inlined to avoid protocol import cycle)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/** The slice of ClientCommand that this handler processes. */
|
|
43
|
+
export interface RespondToolCommand {
|
|
44
|
+
type: "chat.respondTool"
|
|
45
|
+
chatId: string
|
|
46
|
+
toolUseId: string
|
|
47
|
+
result: AnyValue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Dependency bundle injected by AgentCoordinator
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export interface ToolRespondDeps {
|
|
55
|
+
/** The shared in-memory active-turns map owned by AgentCoordinator. */
|
|
56
|
+
activeTurns: ToolRespondActiveTurnsMap
|
|
57
|
+
|
|
58
|
+
/** Persists transcript entries and session tokens. */
|
|
59
|
+
store: ToolRespondStore
|
|
60
|
+
|
|
61
|
+
/** Notify subscribers that a chat's observable state has changed. */
|
|
62
|
+
emitStateChange(chatId: string): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Exported handler
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a pending tool request (AskUserQuestion / ExitPlanMode) coming back
|
|
71
|
+
* from the UI.
|
|
72
|
+
*
|
|
73
|
+
* @throws if there is no pending tool for the chat or the toolUseId does not
|
|
74
|
+
* match.
|
|
75
|
+
*/
|
|
76
|
+
export async function respondTool(
|
|
77
|
+
deps: ToolRespondDeps,
|
|
78
|
+
command: RespondToolCommand,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const { activeTurns, store, emitStateChange } = deps
|
|
81
|
+
|
|
82
|
+
const active = activeTurns.get(command.chatId)
|
|
83
|
+
if (!active || !active.pendingTool) {
|
|
84
|
+
throw new Error("No pending tool request")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const pending = active.pendingTool
|
|
88
|
+
if (pending.toolUseId !== command.toolUseId) {
|
|
89
|
+
throw new Error("Tool response does not match active request")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await store.appendMessage(
|
|
93
|
+
command.chatId,
|
|
94
|
+
timestamped({
|
|
95
|
+
kind: "tool_result",
|
|
96
|
+
toolId: command.toolUseId,
|
|
97
|
+
content: normalizeToolContent(command.result),
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
active.pendingTool = null
|
|
102
|
+
active.status = "running"
|
|
103
|
+
active.waitStartedAt = null
|
|
104
|
+
|
|
105
|
+
if (pending.tool.toolKind === "exit_plan_mode") {
|
|
106
|
+
const resultRec: Record<string, unknown> = isRecord(command.result)
|
|
107
|
+
? command.result
|
|
108
|
+
: {}
|
|
109
|
+
const confirmed = Boolean(resultRec.confirmed)
|
|
110
|
+
const clearContext = Boolean(resultRec.clearContext)
|
|
111
|
+
const message =
|
|
112
|
+
typeof resultRec.message === "string" ? resultRec.message : ""
|
|
113
|
+
|
|
114
|
+
if (confirmed && clearContext) {
|
|
115
|
+
await store.setSessionTokenForProvider(command.chatId, active.provider, null)
|
|
116
|
+
await store.appendMessage(
|
|
117
|
+
command.chatId,
|
|
118
|
+
timestamped({ kind: "context_cleared" }),
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (active.provider === "codex") {
|
|
123
|
+
active.postToolFollowUp = confirmed
|
|
124
|
+
? {
|
|
125
|
+
content: message
|
|
126
|
+
? `Proceed with the approved plan. Additional guidance: ${message}`
|
|
127
|
+
: "Proceed with the approved plan.",
|
|
128
|
+
planMode: false,
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
content: message
|
|
132
|
+
? `Revise the plan using this feedback: ${message}`
|
|
133
|
+
: "Revise the plan using this feedback.",
|
|
134
|
+
planMode: true,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pending.resolve(command.result)
|
|
140
|
+
|
|
141
|
+
emitStateChange(command.chatId)
|
|
142
|
+
}
|