@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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure prompt-manipulation helpers — no IO, no side effects.
|
|
3
|
+
* Extracted from agent.ts to keep it lean.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ChatAttachment } from "../shared/types"
|
|
7
|
+
import { isRecord } from "../shared/errors"
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// XML helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function escapeXmlAttribute(value: string) {
|
|
14
|
+
return value
|
|
15
|
+
.replaceAll("&", "&")
|
|
16
|
+
.replaceAll("\"", """)
|
|
17
|
+
.replaceAll("<", "<")
|
|
18
|
+
.replaceAll(">", ">")
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Attachment hint
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export function buildAttachmentHintText(attachments: ChatAttachment[]) {
|
|
26
|
+
if (attachments.length === 0) return ""
|
|
27
|
+
|
|
28
|
+
const lines = attachments.map((attachment) => (
|
|
29
|
+
`<attachment kind="${escapeXmlAttribute(attachment.kind)}" mime_type="${escapeXmlAttribute(attachment.mimeType)}" path="${escapeXmlAttribute(attachment.absolutePath)}" project_path="${escapeXmlAttribute(attachment.relativePath)}" size_bytes="${attachment.size}" display_name="${escapeXmlAttribute(attachment.displayName)}" />`
|
|
30
|
+
))
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
"<kanna-attachments>",
|
|
34
|
+
...lines,
|
|
35
|
+
"</kanna-attachments>",
|
|
36
|
+
].join("\n")
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildPromptText(content: string, attachments: ChatAttachment[]) {
|
|
40
|
+
const attachmentHint = buildAttachmentHintText(attachments)
|
|
41
|
+
if (!attachmentHint) {
|
|
42
|
+
return content.trim()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const trimmed = content.trim()
|
|
46
|
+
return [
|
|
47
|
+
trimmed || "Please inspect the attached files.",
|
|
48
|
+
attachmentHint,
|
|
49
|
+
].join("\n\n").trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Steered message
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const STEERED_MESSAGE_PREFIX = `<system-message>
|
|
57
|
+
The user would like to inform you of something while you continue to work. Acknowledge receipt immediately with a text response, then continue with the task at hand, incorporating the user's feedback if needed.
|
|
58
|
+
</system-message>`
|
|
59
|
+
|
|
60
|
+
export function buildSteeredMessageContent(content: string) {
|
|
61
|
+
return content.trim().length > 0
|
|
62
|
+
? `${STEERED_MESSAGE_PREFIX}\n\n${content}`
|
|
63
|
+
: STEERED_MESSAGE_PREFIX
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Error-message classifiers
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export function isPromptTooLongMessage(message: string): boolean {
|
|
71
|
+
return /\bprompt\b.*\btoo\s+long\b/i.test(message)
|
|
72
|
+
|| /\bprompt\b.*\btoo\s+large\b/i.test(message)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The stored session token points at a conversation the Claude CLI never
|
|
76
|
+
// persisted (e.g. a spawn interrupted before its first write). Every resume
|
|
77
|
+
// then fails instantly — and the doomed spawn mints yet another unpersisted
|
|
78
|
+
// session id, so without clearing the token the chat is wedged forever. The
|
|
79
|
+
// message rides in result.errors (debugRaw); result text is empty.
|
|
80
|
+
export function isNoConversationFoundMessage(message: string): boolean {
|
|
81
|
+
return /No conversation found with session ID/i.test(message)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// SDK effort normaliser
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
/** Narrows a free-form effort string to the SDK-accepted union without a cast. */
|
|
89
|
+
export function toSdkEffort(effort: string | undefined): "low" | "medium" | "high" | "xhigh" | "max" | undefined {
|
|
90
|
+
if (effort === "low" || effort === "medium" || effort === "high" || effort === "xhigh" || effort === "max") {
|
|
91
|
+
return effort
|
|
92
|
+
}
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Background-task ID extraction
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
// Claude Code's BashTool emits this exact line in the tool_result when a command
|
|
101
|
+
// is launched with `run_in_background: true`. It is the only observable launch
|
|
102
|
+
// signal in Kanna's entry stream (the later `<task-notification>` line produces
|
|
103
|
+
// no transcript entry). The id is alphanumeric. Global flag: one result may
|
|
104
|
+
// report multiple launches in theory; capture every id.
|
|
105
|
+
const BACKGROUND_TASK_LAUNCH_RE = /Command running in background with ID:\s*(\w+)/g
|
|
106
|
+
|
|
107
|
+
/** Extract background-task ids from a tool_result entry's content (string or content blocks). */
|
|
108
|
+
export function backgroundTaskIdsFromToolResult<T>(content: T): string[] {
|
|
109
|
+
let text = ""
|
|
110
|
+
if (typeof content === "string") {
|
|
111
|
+
text = content
|
|
112
|
+
} else if (Array.isArray(content)) {
|
|
113
|
+
for (const block of content) {
|
|
114
|
+
if (isRecord(block)) {
|
|
115
|
+
const blockText = block.text
|
|
116
|
+
if (typeof blockText === "string") {
|
|
117
|
+
text += `${blockText}\n`
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
return []
|
|
123
|
+
}
|
|
124
|
+
const ids: string[] = []
|
|
125
|
+
for (const match of text.matchAll(BACKGROUND_TASK_LAUNCH_RE)) {
|
|
126
|
+
if (match[1]) ids.push(match[1])
|
|
127
|
+
}
|
|
128
|
+
return ids
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Env-var helpers
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
export function positiveIntegerFromEnv(value: string | undefined, fallback: number): number {
|
|
136
|
+
if (value === undefined || value.trim() === "") return fallback
|
|
137
|
+
const parsed = Number(value)
|
|
138
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
139
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { expect, test, describe } from "bun:test"
|
|
2
|
+
import { AsyncMessageQueue, discardedToolResult, toClaudeMessageStream } from "./claude-sdk-queue"
|
|
3
|
+
import type { ClaudeRawSdkMessage } from "./claude-message-normalizer"
|
|
4
|
+
import type { NormalizedToolCall } from "../shared/types"
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Helpers
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Build a fake Query (AsyncGenerator) from an array of objects */
|
|
11
|
+
async function* fakeQuery(
|
|
12
|
+
items: Record<string, unknown>[]
|
|
13
|
+
): AsyncGenerator<Record<string, unknown>, void> {
|
|
14
|
+
for (const item of items) {
|
|
15
|
+
yield item
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Collect all values from an async iterable into an array */
|
|
20
|
+
async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
|
|
21
|
+
const out: T[] = []
|
|
22
|
+
for await (const v of iter) {
|
|
23
|
+
out.push(v)
|
|
24
|
+
}
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// AsyncMessageQueue
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
describe("AsyncMessageQueue", () => {
|
|
33
|
+
test("push + async iteration yields values in order", async () => {
|
|
34
|
+
const q = new AsyncMessageQueue<number>()
|
|
35
|
+
q.push(1)
|
|
36
|
+
q.push(2)
|
|
37
|
+
q.push(3)
|
|
38
|
+
q.close()
|
|
39
|
+
const result = await collect(q)
|
|
40
|
+
expect(result).toEqual([1, 2, 3])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test("close() ends iteration", async () => {
|
|
44
|
+
const q = new AsyncMessageQueue<string>()
|
|
45
|
+
q.push("a")
|
|
46
|
+
q.close()
|
|
47
|
+
const result = await collect(q)
|
|
48
|
+
expect(result).toEqual(["a"])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("push after close throws", () => {
|
|
52
|
+
const q = new AsyncMessageQueue<number>()
|
|
53
|
+
q.close()
|
|
54
|
+
expect(() => q.push(42)).toThrow("Cannot push to a closed queue")
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("waiter is resolved immediately when queue had a pending value", async () => {
|
|
58
|
+
const q = new AsyncMessageQueue<string>()
|
|
59
|
+
// Start consuming before any push
|
|
60
|
+
const iter = q[Symbol.asyncIterator]()
|
|
61
|
+
const nextPromise = iter.next()
|
|
62
|
+
// Push while a waiter is registered
|
|
63
|
+
q.push("hello")
|
|
64
|
+
const result = await nextPromise
|
|
65
|
+
expect(result).toEqual({ done: false, value: "hello" })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("multiple pushes before iteration are delivered in order", async () => {
|
|
69
|
+
const q = new AsyncMessageQueue<number>()
|
|
70
|
+
for (let i = 0; i < 5; i++) q.push(i)
|
|
71
|
+
q.close()
|
|
72
|
+
const result = await collect(q)
|
|
73
|
+
expect(result).toEqual([0, 1, 2, 3, 4])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("close with pending waiters resolves them as done", async () => {
|
|
77
|
+
const q = new AsyncMessageQueue<number>()
|
|
78
|
+
const iter = q[Symbol.asyncIterator]()
|
|
79
|
+
const nextPromise = iter.next()
|
|
80
|
+
// Close while a waiter is pending
|
|
81
|
+
q.close()
|
|
82
|
+
const result = await nextPromise
|
|
83
|
+
expect(result).toEqual({ done: true, value: undefined })
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("close() is idempotent — calling twice does not throw", () => {
|
|
87
|
+
const q = new AsyncMessageQueue<number>()
|
|
88
|
+
q.push(1)
|
|
89
|
+
q.close()
|
|
90
|
+
expect(() => q.close()).not.toThrow()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// discardedToolResult
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/** Build a minimal mock NormalizedToolCall with the given toolKind. */
|
|
99
|
+
function makeTool<TKind extends "ask_user_question" | "exit_plan_mode">(
|
|
100
|
+
toolKind: TKind
|
|
101
|
+
): NormalizedToolCall & { toolKind: TKind } {
|
|
102
|
+
return {
|
|
103
|
+
kind: "tool",
|
|
104
|
+
toolKind,
|
|
105
|
+
toolName: toolKind,
|
|
106
|
+
toolId: "t1",
|
|
107
|
+
input: {} as never,
|
|
108
|
+
} as unknown as NormalizedToolCall & { toolKind: TKind }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe("discardedToolResult", () => {
|
|
112
|
+
test("ask_user_question variant returns answers: {}", () => {
|
|
113
|
+
const result = discardedToolResult(makeTool("ask_user_question"))
|
|
114
|
+
expect(result).toHaveProperty("answers")
|
|
115
|
+
expect((result as { answers: Record<string, string> }).answers).toEqual({})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test("exit_plan_mode variant does not have an answers field", () => {
|
|
119
|
+
const result = discardedToolResult(makeTool("exit_plan_mode"))
|
|
120
|
+
expect(result).not.toHaveProperty("answers")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("ask_user_question result carries discarded: true", () => {
|
|
124
|
+
expect(discardedToolResult(makeTool("ask_user_question")).discarded).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test("exit_plan_mode result carries discarded: true", () => {
|
|
128
|
+
expect(discardedToolResult(makeTool("exit_plan_mode")).discarded).toBe(true)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// toClaudeMessageStream
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
describe("toClaudeMessageStream", () => {
|
|
137
|
+
test("passes through all SDK messages unchanged", async () => {
|
|
138
|
+
const messages: ClaudeRawSdkMessage[] = [
|
|
139
|
+
{ type: "assistant", uuid: "a" },
|
|
140
|
+
{ type: "system", uuid: "b" },
|
|
141
|
+
]
|
|
142
|
+
// fakeQuery produces objects; cast to satisfy Query's AsyncGenerator shape
|
|
143
|
+
const q = fakeQuery(messages as Record<string, unknown>[])
|
|
144
|
+
const result = await collect(toClaudeMessageStream(q as never))
|
|
145
|
+
expect(result).toHaveLength(2)
|
|
146
|
+
expect(result[0]).toEqual(messages[0])
|
|
147
|
+
expect(result[1]).toEqual(messages[1])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("yields empty sequence for an empty query", async () => {
|
|
151
|
+
const q = fakeQuery([])
|
|
152
|
+
const result = await collect(toClaudeMessageStream(q as never))
|
|
153
|
+
expect(result).toHaveLength(0)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test("yielded messages preserve their properties", async () => {
|
|
157
|
+
const msg: ClaudeRawSdkMessage = {
|
|
158
|
+
type: "result",
|
|
159
|
+
result: "DONE",
|
|
160
|
+
total_cost_usd: 0.001,
|
|
161
|
+
session_id: "sess-1",
|
|
162
|
+
}
|
|
163
|
+
const q = fakeQuery([msg as Record<string, unknown>])
|
|
164
|
+
const [yielded] = await collect(toClaudeMessageStream(q as never))
|
|
165
|
+
expect(yielded?.result).toBe("DONE")
|
|
166
|
+
expect(yielded?.total_cost_usd).toBe(0.001)
|
|
167
|
+
expect(yielded?.session_id).toBe("sess-1")
|
|
168
|
+
})
|
|
169
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type Query } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import { type ClaudeRawSdkMessage, isSdkToClaudeMessage } from "./claude-message-normalizer"
|
|
3
|
+
import type { NormalizedToolCall } from "../shared/types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic async iterable queue used to buffer SDK user-message turns.
|
|
7
|
+
* Pure data structure — no IO.
|
|
8
|
+
*/
|
|
9
|
+
export class AsyncMessageQueue<T> implements AsyncIterable<T> {
|
|
10
|
+
private readonly values: T[] = []
|
|
11
|
+
private readonly waiters: Array<(result: IteratorResult<T, undefined>) => void> = []
|
|
12
|
+
private closed = false
|
|
13
|
+
|
|
14
|
+
push(value: T) {
|
|
15
|
+
if (this.closed) {
|
|
16
|
+
throw new Error("Cannot push to a closed queue")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const waiter = this.waiters.shift()
|
|
20
|
+
if (waiter) {
|
|
21
|
+
waiter({ done: false, value })
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
this.values.push(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
close() {
|
|
29
|
+
if (this.closed) return
|
|
30
|
+
this.closed = true
|
|
31
|
+
while (this.waiters.length > 0) {
|
|
32
|
+
const waiter = this.waiters.shift()
|
|
33
|
+
waiter?.({ done: true, value: undefined })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[Symbol.asyncIterator](): AsyncIterator<T, undefined> {
|
|
38
|
+
return {
|
|
39
|
+
next: async (): Promise<IteratorResult<T, undefined>> => {
|
|
40
|
+
if (this.values.length > 0) {
|
|
41
|
+
return { done: false, value: this.values.shift()! }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this.closed) {
|
|
45
|
+
return { done: true, value: undefined }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return await new Promise<IteratorResult<T, undefined>>((resolve) => {
|
|
49
|
+
this.waiters.push(resolve)
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Builds the discard payload for ask_user_question / exit_plan_mode tools
|
|
58
|
+
* when a pending tool call is discarded (e.g. session closed mid-flight).
|
|
59
|
+
*/
|
|
60
|
+
export function discardedToolResult(
|
|
61
|
+
tool: NormalizedToolCall & { toolKind: "ask_user_question" | "exit_plan_mode" }
|
|
62
|
+
) {
|
|
63
|
+
if (tool.toolKind === "ask_user_question") {
|
|
64
|
+
return {
|
|
65
|
+
discarded: true,
|
|
66
|
+
answers: {},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
discarded: true,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Filters the raw SDK stream to Claude-only messages.
|
|
77
|
+
*/
|
|
78
|
+
export async function* toClaudeMessageStream(q: Query): AsyncGenerator<ClaudeRawSdkMessage> {
|
|
79
|
+
for await (const m of q) {
|
|
80
|
+
if (isSdkToClaudeMessage(m)) yield m
|
|
81
|
+
}
|
|
82
|
+
}
|