@herdctl/chat 0.3.13 → 0.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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for the transport-agnostic SDKMessage → chat-event translator.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=sdk-message-translator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-message-translator.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/sdk-message-translator.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tests for the transport-agnostic SDKMessage → chat-event translator.
3
+ */
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { createSDKMessageHandler, SDKMessageTranslator, } from "../sdk-message-translator.js";
6
+ // =============================================================================
7
+ // Helpers — build SDK messages in the shape the extractors expect
8
+ // =============================================================================
9
+ function assistantText(text) {
10
+ return { type: "assistant", message: { content: [{ type: "text", text }] } };
11
+ }
12
+ function assistantToolUse(id, name, input) {
13
+ return {
14
+ type: "assistant",
15
+ message: { content: [{ type: "tool_use", id, name, input }] },
16
+ };
17
+ }
18
+ function toolResult(toolUseId, output, isError = false) {
19
+ return {
20
+ type: "user",
21
+ message: {
22
+ content: [
23
+ { type: "tool_result", tool_use_id: toolUseId, content: output, is_error: isError },
24
+ ],
25
+ },
26
+ };
27
+ }
28
+ describe("SDKMessageTranslator", () => {
29
+ describe("text deltas", () => {
30
+ it("emits assistant text via onText", async () => {
31
+ const onText = vi.fn();
32
+ const t = new SDKMessageTranslator({ onText });
33
+ await t.handle(assistantText("Hello "));
34
+ await t.handle(assistantText("world"));
35
+ expect(onText).toHaveBeenNthCalledWith(1, "Hello ");
36
+ expect(onText).toHaveBeenNthCalledWith(2, "world");
37
+ });
38
+ it("ignores non-assistant/non-user messages", async () => {
39
+ const onText = vi.fn();
40
+ const onToolCall = vi.fn();
41
+ const t = new SDKMessageTranslator({ onText, onToolCall });
42
+ await t.handle({ type: "system" });
43
+ await t.handle({ type: "result" });
44
+ await t.handle({ type: "stream_event" });
45
+ expect(onText).not.toHaveBeenCalled();
46
+ expect(onToolCall).not.toHaveBeenCalled();
47
+ });
48
+ it("does not emit onText for an assistant message with no text", async () => {
49
+ const onText = vi.fn();
50
+ const t = new SDKMessageTranslator({ onText });
51
+ await t.handle(assistantToolUse("t1", "Bash", { command: "ls" }));
52
+ expect(onText).not.toHaveBeenCalled();
53
+ });
54
+ });
55
+ describe("boundaries", () => {
56
+ it("emits a boundary between two consecutive assistant text turns", async () => {
57
+ const order = [];
58
+ const t = new SDKMessageTranslator({
59
+ onText: (text) => {
60
+ order.push(`text:${text}`);
61
+ },
62
+ onBoundary: () => {
63
+ order.push("boundary");
64
+ },
65
+ });
66
+ await t.handle(assistantText("first"));
67
+ await t.handle(assistantText("second"));
68
+ expect(order).toEqual(["text:first", "boundary", "text:second"]);
69
+ });
70
+ it("does not emit a boundary before the first text", async () => {
71
+ const onBoundary = vi.fn();
72
+ const t = new SDKMessageTranslator({ onText: vi.fn(), onBoundary });
73
+ await t.handle(assistantText("only"));
74
+ expect(onBoundary).not.toHaveBeenCalled();
75
+ });
76
+ it("does not emit a boundary for text that immediately follows a tool result", async () => {
77
+ const onBoundary = vi.fn();
78
+ const t = new SDKMessageTranslator({ onText: vi.fn(), onBoundary, onToolCall: vi.fn() });
79
+ await t.handle(assistantText("before tool"));
80
+ await t.handle(assistantToolUse("t1", "Read", { file_path: "/a" }));
81
+ await t.handle(toolResult("t1", "file contents"));
82
+ await t.handle(assistantText("after tool"));
83
+ // The tool result resets the text flag, so the post-tool text is NOT a boundary.
84
+ expect(onBoundary).not.toHaveBeenCalled();
85
+ });
86
+ });
87
+ describe("tool calls", () => {
88
+ it("pairs a tool_use with its result, including name, input summary and duration", async () => {
89
+ const calls = [];
90
+ let clock = 1000;
91
+ const t = new SDKMessageTranslator({ onToolCall: (c) => void calls.push(c) }, { now: () => clock });
92
+ await t.handle(assistantToolUse("t1", "Bash", { command: "echo hi" }));
93
+ clock = 1250; // 250ms later
94
+ await t.handle(toolResult("t1", "hi"));
95
+ expect(calls).toHaveLength(1);
96
+ expect(calls[0]).toEqual({
97
+ toolName: "Bash",
98
+ inputSummary: "echo hi",
99
+ output: "hi",
100
+ isError: false,
101
+ durationMs: 250,
102
+ toolUseId: "t1",
103
+ });
104
+ });
105
+ it("marks error results", async () => {
106
+ const calls = [];
107
+ const t = new SDKMessageTranslator({ onToolCall: (c) => void calls.push(c) });
108
+ await t.handle(assistantToolUse("t1", "Bash", { command: "false" }));
109
+ await t.handle(toolResult("t1", "boom", true));
110
+ expect(calls[0].isError).toBe(true);
111
+ expect(calls[0].output).toBe("boom");
112
+ });
113
+ it("falls back to 'Tool' when there is no matching tool_use", async () => {
114
+ const calls = [];
115
+ const t = new SDKMessageTranslator({ onToolCall: (c) => void calls.push(c) });
116
+ // Result arrives with no preceding tool_use of that id
117
+ await t.handle(toolResult("orphan", "result"));
118
+ expect(calls).toHaveLength(1);
119
+ expect(calls[0].toolName).toBe("Tool");
120
+ expect(calls[0].durationMs).toBeUndefined();
121
+ expect(calls[0].inputSummary).toBeUndefined();
122
+ });
123
+ it("does not emit tool calls when toolResults is false but still consumes pending uses", async () => {
124
+ const onToolCall = vi.fn();
125
+ const t = new SDKMessageTranslator({ onToolCall }, { toolResults: false });
126
+ await t.handle(assistantToolUse("t1", "Bash", { command: "ls" }));
127
+ await t.handle(toolResult("t1", "out"));
128
+ expect(onToolCall).not.toHaveBeenCalled();
129
+ });
130
+ });
131
+ describe("backpressure", () => {
132
+ it("awaits async handlers in order", async () => {
133
+ const order = [];
134
+ const t = new SDKMessageTranslator({
135
+ onText: async (text) => {
136
+ await new Promise((r) => setTimeout(r, 5));
137
+ order.push(`text:${text}`);
138
+ },
139
+ onBoundary: async () => {
140
+ order.push("boundary");
141
+ },
142
+ });
143
+ await t.handle(assistantText("a"));
144
+ await t.handle(assistantText("b"));
145
+ expect(order).toEqual(["text:a", "boundary", "text:b"]);
146
+ });
147
+ });
148
+ describe("reset()", () => {
149
+ it("clears pending tool uses and text state", async () => {
150
+ const onBoundary = vi.fn();
151
+ const calls = [];
152
+ const t = new SDKMessageTranslator({
153
+ onText: vi.fn(),
154
+ onBoundary,
155
+ onToolCall: (c) => void calls.push(c),
156
+ });
157
+ await t.handle(assistantText("turn 1"));
158
+ await t.handle(assistantToolUse("t1", "Bash", { command: "x" }));
159
+ t.reset();
160
+ // After reset: previous text flag cleared (no boundary on next text)
161
+ await t.handle(assistantText("turn 2"));
162
+ expect(onBoundary).not.toHaveBeenCalled();
163
+ // After reset: the orphaned tool_use no longer pairs
164
+ await t.handle(toolResult("t1", "late"));
165
+ expect(calls[0].toolName).toBe("Tool");
166
+ });
167
+ });
168
+ });
169
+ describe("createSDKMessageHandler", () => {
170
+ it("returns an onMessage handler driving a fresh translator", async () => {
171
+ const onText = vi.fn();
172
+ const calls = [];
173
+ const handler = createSDKMessageHandler({
174
+ onText,
175
+ onToolCall: (c) => void calls.push(c),
176
+ });
177
+ await handler(assistantText("hi"));
178
+ await handler(assistantToolUse("t1", "Read", { file_path: "/f" }));
179
+ await handler(toolResult("t1", "contents"));
180
+ expect(onText).toHaveBeenCalledWith("hi");
181
+ expect(calls[0]).toMatchObject({ toolName: "Read", inputSummary: "/f", output: "contents" });
182
+ });
183
+ });
184
+ //# sourceMappingURL=sdk-message-translator.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-message-translator.test.js","sourceRoot":"","sources":["../../src/__tests__/sdk-message-translator.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { DEFAULT_MESSAGE_DELAY_MS, DEFAULT_SPLIT_POINTS, findSplitPoint, formatC
16
16
  export { StreamingResponder, type StreamingResponderOptions, } from "./streaming-responder.js";
17
17
  export { type ContentBlock, extractMessageContent, hasTextContent, isTextContentBlock, type SDKMessage, type TextContentBlock, } from "./message-extraction.js";
18
18
  export { extractToolResultContent, extractToolResults, extractToolUseBlocks, getToolInputSummary, TOOL_EMOJIS, type ToolResult, type ToolUseBlock, } from "./tool-parsing.js";
19
+ export { createSDKMessageHandler, type SDKMessageHandlers, SDKMessageTranslator, type SDKMessageTranslatorOptions, type TranslatedToolCall, } from "./sdk-message-translator.js";
19
20
  export { checkDMUserFilter, type DMConfig, type DMFilterResult, getDMMode, isDMEnabled, shouldProcessInMode, } from "./dm-filter.js";
20
21
  export { AlreadyConnectedError, ChatConnectionError, ChatConnectorError, ChatErrorCode, InvalidTokenError, isAlreadyConnectedError, isChatConnectionError, isChatConnectorError, isInvalidTokenError, isMissingTokenError, MissingTokenError, } from "./errors.js";
21
22
  export { type ClassifiedError, ErrorCategory, isAuthError, isRateLimitError, isTransientError, type RetryOptions, type RetryResult, safeExecute, safeExecuteWithReply, USER_ERROR_MESSAGES, type UserErrorMessageKey, withRetry, } from "./error-handler.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAEL,KAAK,cAAc,EAEnB,oBAAoB,EAEpB,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,oBAAoB,EAEpB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,2BAA2B,EAE3B,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAMpC,YAAY,EAEV,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EAEnB,kBAAkB,EAClB,gBAAgB,EAEhB,mBAAmB,EAEnB,cAAc,EACd,mBAAmB,IAAI,4BAA4B,EAEnD,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAMpB,OAAO,EAEL,wBAAwB,EACxB,oBAAoB,EAEpB,cAAc,EACd,eAAe,EAEf,KAAK,mBAAmB,EACxB,cAAc,EACd,UAAU,EACV,KAAK,WAAW,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EACL,KAAK,YAAY,EACjB,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EAEnB,WAAW,EACX,KAAK,UAAU,EAEf,KAAK,YAAY,GAClB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,EACL,iBAAiB,EACjB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAMxB,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EAEnB,kBAAkB,EAElB,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EAErB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAMrB,OAAO,EAEL,KAAK,eAAe,EAEpB,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,WAAW,EACX,oBAAoB,EAEpB,mBAAmB,EACnB,KAAK,mBAAmB,EAExB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,OAAO,EAEL,KAAK,cAAc,EAEnB,oBAAoB,EAEpB,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,sBAAsB,EACtB,oBAAoB,EAEpB,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,qBAAqB,EACrB,2BAA2B,EAE3B,gBAAgB,EAChB,mBAAmB,EACnB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAMpC,YAAY,EAEV,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,yBAAyB,EACzB,mBAAmB,EAEnB,kBAAkB,EAClB,gBAAgB,EAEhB,mBAAmB,EAEnB,cAAc,EACd,mBAAmB,IAAI,4BAA4B,EAEnD,qBAAqB,GACtB,MAAM,YAAY,CAAC;AAMpB,OAAO,EAEL,wBAAwB,EACxB,oBAAoB,EAEpB,cAAc,EACd,eAAe,EAEf,KAAK,mBAAmB,EACxB,cAAc,EACd,UAAU,EACV,KAAK,WAAW,EAChB,YAAY,EACZ,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAMhC,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EACL,KAAK,YAAY,EACjB,qBAAqB,EACrB,cAAc,EACd,kBAAkB,EAClB,KAAK,UAAU,EACf,KAAK,gBAAgB,GACtB,MAAM,yBAAyB,CAAC;AAMjC,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAElB,oBAAoB,EACpB,mBAAmB,EAEnB,WAAW,EACX,KAAK,UAAU,EAEf,KAAK,YAAY,GAClB,MAAM,mBAAmB,CAAC;AAM3B,OAAO,EAEL,uBAAuB,EACvB,KAAK,kBAAkB,EAEvB,oBAAoB,EACpB,KAAK,2BAA2B,EAEhC,KAAK,kBAAkB,GACxB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,EACL,iBAAiB,EACjB,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAMxB,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EAEnB,kBAAkB,EAElB,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,EAErB,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAMrB,OAAO,EAEL,KAAK,eAAe,EAEpB,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,YAAY,EACjB,KAAK,WAAW,EAChB,WAAW,EACX,oBAAoB,EAEpB,mBAAmB,EACnB,KAAK,mBAAmB,EAExB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -47,6 +47,14 @@ extractToolUseBlocks, getToolInputSummary,
47
47
  // Constants
48
48
  TOOL_EMOJIS, } from "./tool-parsing.js";
49
49
  // =============================================================================
50
+ // SDK Message Translation
51
+ // =============================================================================
52
+ export {
53
+ // Factory
54
+ createSDKMessageHandler,
55
+ // Class
56
+ SDKMessageTranslator, } from "./sdk-message-translator.js";
57
+ // =============================================================================
50
58
  // DM Filtering
51
59
  // =============================================================================
52
60
  export { checkDMUserFilter, getDMMode, isDMEnabled, shouldProcessInMode, } from "./dm-filter.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF,OAAO;AAGL,UAAU;AACV,oBAAoB;AACpB,QAAQ;AACR,kBAAkB,EAGlB,sBAAsB,EACtB,oBAAoB;AACpB,oBAAoB;AACpB,yBAAyB,EAEzB,qBAAqB,EACrB,2BAA2B;AAC3B,SAAS;AACT,gBAAgB,EAChB,mBAAmB,EAGnB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAyBpC,gFAAgF;AAChF,oBAAoB;AACpB,gFAAgF;AAEhF,OAAO;AACL,YAAY;AACZ,wBAAwB,EACxB,oBAAoB;AACpB,YAAY;AACZ,cAAc,EACd,eAAe,EAGf,cAAc,EACd,UAAU,EAEV,YAAY,EACZ,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,OAAO,EACL,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAElC,gFAAgF;AAChF,qBAAqB;AACrB,gFAAgF;AAEhF,OAAO,EAEL,qBAAqB,EACrB,cAAc,EACd,kBAAkB,GAGnB,MAAM,yBAAyB,CAAC;AAEjC,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,OAAO,EACL,wBAAwB,EACxB,kBAAkB;AAClB,YAAY;AACZ,oBAAoB,EACpB,mBAAmB;AACnB,YAAY;AACZ,WAAW,GAIZ,MAAM,mBAAmB,CAAC;AAE3B,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,OAAO,EACL,iBAAiB,EAGjB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAExB,gFAAgF;AAChF,SAAS;AACT,gFAAgF;AAEhF,OAAO,EACL,qBAAqB,EACrB,mBAAmB;AACnB,gBAAgB;AAChB,kBAAkB;AAClB,cAAc;AACd,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB;AACrB,cAAc;AACd,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,OAAO;AAGL,aAAa;AACb,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAGhB,WAAW,EACX,oBAAoB;AACpB,YAAY;AACZ,mBAAmB;AAEnB,YAAY;AACZ,SAAS,GACV,MAAM,oBAAoB,CAAC;AAE5B,gFAAgF;AAChF,oBAAoB;AACpB,gFAAgF;AAEhF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF,OAAO;AAGL,UAAU;AACV,oBAAoB;AACpB,QAAQ;AACR,kBAAkB,EAGlB,sBAAsB,EACtB,oBAAoB;AACpB,oBAAoB;AACpB,yBAAyB,EAEzB,qBAAqB,EACrB,2BAA2B;AAC3B,SAAS;AACT,gBAAgB,EAChB,mBAAmB,EAGnB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAyBpC,gFAAgF;AAChF,oBAAoB;AACpB,gFAAgF;AAEhF,OAAO;AACL,YAAY;AACZ,wBAAwB,EACxB,oBAAoB;AACpB,YAAY;AACZ,cAAc,EACd,eAAe,EAGf,cAAc,EACd,UAAU,EAEV,YAAY,EACZ,eAAe,GAChB,MAAM,wBAAwB,CAAC;AAEhC,gFAAgF;AAChF,sBAAsB;AACtB,gFAAgF;AAEhF,OAAO,EACL,kBAAkB,GAEnB,MAAM,0BAA0B,CAAC;AAElC,gFAAgF;AAChF,qBAAqB;AACrB,gFAAgF;AAEhF,OAAO,EAEL,qBAAqB,EACrB,cAAc,EACd,kBAAkB,GAGnB,MAAM,yBAAyB,CAAC;AAEjC,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,OAAO,EACL,wBAAwB,EACxB,kBAAkB;AAClB,YAAY;AACZ,oBAAoB,EACpB,mBAAmB;AACnB,YAAY;AACZ,WAAW,GAIZ,MAAM,mBAAmB,CAAC;AAE3B,gFAAgF;AAChF,0BAA0B;AAC1B,gFAAgF;AAEhF,OAAO;AACL,UAAU;AACV,uBAAuB;AAEvB,QAAQ;AACR,oBAAoB,GAIrB,MAAM,6BAA6B,CAAC;AAErC,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,OAAO,EACL,iBAAiB,EAGjB,SAAS,EACT,WAAW,EACX,mBAAmB,GACpB,MAAM,gBAAgB,CAAC;AAExB,gFAAgF;AAChF,SAAS;AACT,gFAAgF;AAEhF,OAAO,EACL,qBAAqB,EACrB,mBAAmB;AACnB,gBAAgB;AAChB,kBAAkB;AAClB,cAAc;AACd,aAAa,EACb,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB;AACrB,cAAc;AACd,oBAAoB,EACpB,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,OAAO;AAGL,aAAa;AACb,aAAa,EACb,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAGhB,WAAW,EACX,oBAAoB;AACpB,YAAY;AACZ,mBAAmB;AAEnB,YAAY;AACZ,SAAS,GACV,MAAM,oBAAoB,CAAC;AAE5B,gFAAgF;AAChF,oBAAoB;AACpB,gFAAgF;AAEhF,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,eAAe,EACf,cAAc,GACf,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Transport-agnostic SDKMessage → chat-event translation
3
+ *
4
+ * Every chat surface built on herdctl (Discord, Slack, the web dashboard, and
5
+ * downstream apps like paddock) consumes the same stream of `SDKMessage`s from
6
+ * `FleetManager.trigger({ onMessage })` and turns it into the same handful of
7
+ * UI events:
8
+ * - assistant text deltas,
9
+ * - a boundary between distinct assistant turns,
10
+ * - paired tool calls (a `tool_use` matched to its later `tool_result`,
11
+ * enriched with an input summary and a wall-clock duration).
12
+ *
13
+ * That translation was previously reimplemented per connector (see
14
+ * `@herdctl/web`'s WebChatManager and paddock's `ws.ts`). This module extracts
15
+ * it into one stateful translator so transports only have to supply the
16
+ * destination handlers.
17
+ *
18
+ * @module sdk-message-translator
19
+ */
20
+ import { type SDKMessage } from "./message-extraction.js";
21
+ /**
22
+ * A paired tool call: a `tool_use` block matched with its `tool_result`.
23
+ */
24
+ export interface TranslatedToolCall {
25
+ /** Tool name (e.g. "Bash", "Read"); "Tool" if the pairing could not be resolved */
26
+ toolName: string;
27
+ /** Human-readable summary of the tool input (e.g. the bash command or file path) */
28
+ inputSummary?: string;
29
+ /** Tool output text (may be empty) */
30
+ output: string;
31
+ /** Whether the tool reported an error */
32
+ isError: boolean;
33
+ /** Wall-clock duration between the tool_use and its result, in milliseconds */
34
+ durationMs?: number;
35
+ /** The originating tool_use id, when present */
36
+ toolUseId?: string;
37
+ }
38
+ /**
39
+ * Handlers invoked as SDK messages are translated. All are optional and may be
40
+ * async; the translator awaits them in order so transports can apply
41
+ * backpressure (e.g. a slow WebSocket send).
42
+ */
43
+ export interface SDKMessageHandlers {
44
+ /** Called with each assistant text delta as it streams in. */
45
+ onText?: (text: string) => void | Promise<void>;
46
+ /**
47
+ * Called when a new assistant turn begins after a previous one produced text
48
+ * (or after a tool call interrupts the text), so transports can split bubbles.
49
+ */
50
+ onBoundary?: () => void | Promise<void>;
51
+ /** Called once per tool result, paired with its originating tool_use. */
52
+ onToolCall?: (toolCall: TranslatedToolCall) => void | Promise<void>;
53
+ }
54
+ /**
55
+ * Options for {@link SDKMessageTranslator}.
56
+ */
57
+ export interface SDKMessageTranslatorOptions {
58
+ /**
59
+ * Emit tool calls via `onToolCall`. When `false`, tool_use blocks are still
60
+ * tracked (so boundaries stay correct) but no `onToolCall` is fired.
61
+ * Defaults to `true`.
62
+ */
63
+ toolResults?: boolean;
64
+ /**
65
+ * Clock used for duration measurement. Injectable for deterministic tests.
66
+ * Defaults to `Date.now`.
67
+ */
68
+ now?: () => number;
69
+ }
70
+ /**
71
+ * Stateful translator from the Claude Agent SDK message stream to chat-UI events.
72
+ *
73
+ * Feed every `SDKMessage` from a trigger's `onMessage` callback into
74
+ * {@link SDKMessageTranslator.handle}; it extracts assistant text, tracks
75
+ * `tool_use` blocks so they can be paired with their later `tool_result`s, and
76
+ * emits boundaries between distinct assistant turns.
77
+ *
78
+ * One instance corresponds to one trigger/turn (it holds per-turn state such as
79
+ * pending tool uses). Create a fresh translator per `trigger()` call.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const translator = new SDKMessageTranslator({
84
+ * onText: (t) => ws.send({ type: "chat:response", text: t }),
85
+ * onToolCall: (c) => ws.send({ type: "chat:tool_call", ...c }),
86
+ * onBoundary: () => ws.send({ type: "chat:boundary" }),
87
+ * });
88
+ *
89
+ * await fleet.trigger("agent", undefined, {
90
+ * prompt,
91
+ * onMessage: (m) => translator.handle(m),
92
+ * });
93
+ * ```
94
+ */
95
+ export declare class SDKMessageTranslator {
96
+ private readonly handlers;
97
+ private readonly toolResults;
98
+ private readonly now;
99
+ /** tool_use id -> pending call awaiting its result */
100
+ private readonly pendingToolUses;
101
+ /** Whether the current assistant turn has already emitted text */
102
+ private hasAssistantText;
103
+ constructor(handlers: SDKMessageHandlers, options?: SDKMessageTranslatorOptions);
104
+ /**
105
+ * Translate a single SDK message, invoking the configured handlers.
106
+ *
107
+ * Safe to call with any SDK message type — non-assistant/non-user messages
108
+ * (system, result, stream events, etc.) are ignored.
109
+ *
110
+ * @param message - One message from the trigger's `onMessage` stream
111
+ */
112
+ handle(message: SDKMessage): Promise<void>;
113
+ /**
114
+ * Reset per-turn state. Call between reused turns if you keep one translator
115
+ * across multiple triggers (not required when creating one per trigger).
116
+ */
117
+ reset(): void;
118
+ private handleAssistant;
119
+ private handleUser;
120
+ }
121
+ /**
122
+ * Build an `onMessage` callback that drives a fresh {@link SDKMessageTranslator}.
123
+ *
124
+ * Convenience for the common case: pass the returned function straight to
125
+ * `FleetManager.trigger({ onMessage })`. Creates one translator bound to the
126
+ * given handlers, so this represents a single trigger/turn.
127
+ *
128
+ * @param handlers - Destination handlers for text/boundary/tool events
129
+ * @param options - Translator options (toolResults toggle, injectable clock)
130
+ * @returns An async `onMessage` handler suitable for `TriggerOptions.onMessage`
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * await fleet.trigger("agent", undefined, {
135
+ * prompt,
136
+ * onMessage: createSDKMessageHandler({
137
+ * onText: (t) => stream(t),
138
+ * onToolCall: (c) => renderTool(c),
139
+ * }),
140
+ * });
141
+ * ```
142
+ */
143
+ export declare function createSDKMessageHandler(handlers: SDKMessageHandlers, options?: SDKMessageTranslatorOptions): (message: SDKMessage) => Promise<void>;
144
+ //# sourceMappingURL=sdk-message-translator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-message-translator.d.ts","sourceRoot":"","sources":["../src/sdk-message-translator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAOjF;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,mFAAmF;IACnF,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,yEAAyE;IACzE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrE;AAED;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAYD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAU;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,sDAAsD;IACtD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqC;IACrE,kEAAkE;IAClE,OAAO,CAAC,gBAAgB,CAAS;gBAErB,QAAQ,EAAE,kBAAkB,EAAE,OAAO,CAAC,EAAE,2BAA2B;IAM/E;;;;;;;OAOG;IACG,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD;;;OAGG;IACH,KAAK,IAAI,IAAI;YAOC,eAAe;YAyBf,UAAU;CAqCzB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,CAAC,EAAE,2BAA2B,GACpC,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAGxC"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Transport-agnostic SDKMessage → chat-event translation
3
+ *
4
+ * Every chat surface built on herdctl (Discord, Slack, the web dashboard, and
5
+ * downstream apps like paddock) consumes the same stream of `SDKMessage`s from
6
+ * `FleetManager.trigger({ onMessage })` and turns it into the same handful of
7
+ * UI events:
8
+ * - assistant text deltas,
9
+ * - a boundary between distinct assistant turns,
10
+ * - paired tool calls (a `tool_use` matched to its later `tool_result`,
11
+ * enriched with an input summary and a wall-clock duration).
12
+ *
13
+ * That translation was previously reimplemented per connector (see
14
+ * `@herdctl/web`'s WebChatManager and paddock's `ws.ts`). This module extracts
15
+ * it into one stateful translator so transports only have to supply the
16
+ * destination handlers.
17
+ *
18
+ * @module sdk-message-translator
19
+ */
20
+ import { extractMessageContent } from "./message-extraction.js";
21
+ import { extractToolResults, extractToolUseBlocks, getToolInputSummary } from "./tool-parsing.js";
22
+ /**
23
+ * Stateful translator from the Claude Agent SDK message stream to chat-UI events.
24
+ *
25
+ * Feed every `SDKMessage` from a trigger's `onMessage` callback into
26
+ * {@link SDKMessageTranslator.handle}; it extracts assistant text, tracks
27
+ * `tool_use` blocks so they can be paired with their later `tool_result`s, and
28
+ * emits boundaries between distinct assistant turns.
29
+ *
30
+ * One instance corresponds to one trigger/turn (it holds per-turn state such as
31
+ * pending tool uses). Create a fresh translator per `trigger()` call.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const translator = new SDKMessageTranslator({
36
+ * onText: (t) => ws.send({ type: "chat:response", text: t }),
37
+ * onToolCall: (c) => ws.send({ type: "chat:tool_call", ...c }),
38
+ * onBoundary: () => ws.send({ type: "chat:boundary" }),
39
+ * });
40
+ *
41
+ * await fleet.trigger("agent", undefined, {
42
+ * prompt,
43
+ * onMessage: (m) => translator.handle(m),
44
+ * });
45
+ * ```
46
+ */
47
+ export class SDKMessageTranslator {
48
+ handlers;
49
+ toolResults;
50
+ now;
51
+ /** tool_use id -> pending call awaiting its result */
52
+ pendingToolUses = new Map();
53
+ /** Whether the current assistant turn has already emitted text */
54
+ hasAssistantText = false;
55
+ constructor(handlers, options) {
56
+ this.handlers = handlers;
57
+ this.toolResults = options?.toolResults ?? true;
58
+ this.now = options?.now ?? Date.now;
59
+ }
60
+ /**
61
+ * Translate a single SDK message, invoking the configured handlers.
62
+ *
63
+ * Safe to call with any SDK message type — non-assistant/non-user messages
64
+ * (system, result, stream events, etc.) are ignored.
65
+ *
66
+ * @param message - One message from the trigger's `onMessage` stream
67
+ */
68
+ async handle(message) {
69
+ if (message.type === "assistant") {
70
+ await this.handleAssistant(message);
71
+ }
72
+ else if (message.type === "user") {
73
+ await this.handleUser(message);
74
+ }
75
+ }
76
+ /**
77
+ * Reset per-turn state. Call between reused turns if you keep one translator
78
+ * across multiple triggers (not required when creating one per trigger).
79
+ */
80
+ reset() {
81
+ this.pendingToolUses.clear();
82
+ this.hasAssistantText = false;
83
+ }
84
+ // ---------------------------------------------------------------------------
85
+ async handleAssistant(message) {
86
+ const content = extractMessageContent(message);
87
+ if (content) {
88
+ // A new assistant turn after a previous one produced text → boundary.
89
+ if (this.hasAssistantText) {
90
+ this.hasAssistantText = false;
91
+ await this.handlers.onBoundary?.();
92
+ }
93
+ this.hasAssistantText = true;
94
+ await this.handlers.onText?.(content);
95
+ }
96
+ // Track tool_use blocks so we can pair them with results later. We track
97
+ // even when toolResults is disabled so boundary handling stays correct.
98
+ for (const block of extractToolUseBlocks(message)) {
99
+ if (block.id) {
100
+ this.pendingToolUses.set(block.id, {
101
+ name: block.name,
102
+ input: block.input,
103
+ startTime: this.now(),
104
+ });
105
+ }
106
+ }
107
+ }
108
+ async handleUser(message) {
109
+ const results = extractToolResults(message);
110
+ if (results.length === 0) {
111
+ return;
112
+ }
113
+ // A tool result ends the current text run; the next assistant text is a new
114
+ // bubble. Drop the text flag so we don't emit a spurious boundary, but the
115
+ // transport already received the text via onText.
116
+ this.hasAssistantText = false;
117
+ if (!this.toolResults || !this.handlers.onToolCall) {
118
+ // Still consume pending tool uses so the map doesn't leak.
119
+ for (const result of results) {
120
+ if (result.toolUseId)
121
+ this.pendingToolUses.delete(result.toolUseId);
122
+ }
123
+ return;
124
+ }
125
+ for (const result of results) {
126
+ const toolUse = result.toolUseId ? this.pendingToolUses.get(result.toolUseId) : undefined;
127
+ if (result.toolUseId) {
128
+ this.pendingToolUses.delete(result.toolUseId);
129
+ }
130
+ await this.handlers.onToolCall({
131
+ toolName: toolUse?.name ?? "Tool",
132
+ inputSummary: toolUse ? getToolInputSummary(toolUse.name, toolUse.input) : undefined,
133
+ output: result.output,
134
+ isError: result.isError,
135
+ durationMs: toolUse ? this.now() - toolUse.startTime : undefined,
136
+ toolUseId: result.toolUseId,
137
+ });
138
+ }
139
+ }
140
+ }
141
+ /**
142
+ * Build an `onMessage` callback that drives a fresh {@link SDKMessageTranslator}.
143
+ *
144
+ * Convenience for the common case: pass the returned function straight to
145
+ * `FleetManager.trigger({ onMessage })`. Creates one translator bound to the
146
+ * given handlers, so this represents a single trigger/turn.
147
+ *
148
+ * @param handlers - Destination handlers for text/boundary/tool events
149
+ * @param options - Translator options (toolResults toggle, injectable clock)
150
+ * @returns An async `onMessage` handler suitable for `TriggerOptions.onMessage`
151
+ *
152
+ * @example
153
+ * ```typescript
154
+ * await fleet.trigger("agent", undefined, {
155
+ * prompt,
156
+ * onMessage: createSDKMessageHandler({
157
+ * onText: (t) => stream(t),
158
+ * onToolCall: (c) => renderTool(c),
159
+ * }),
160
+ * });
161
+ * ```
162
+ */
163
+ export function createSDKMessageHandler(handlers, options) {
164
+ const translator = new SDKMessageTranslator(handlers, options);
165
+ return (message) => translator.handle(message);
166
+ }
167
+ //# sourceMappingURL=sdk-message-translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-message-translator.js","sourceRoot":"","sources":["../src/sdk-message-translator.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herdctl/chat",
3
- "version": "0.3.13",
3
+ "version": "0.4.0",
4
4
  "description": "Shared chat infrastructure for herdctl connectors",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "yaml": "^2.3.0",
14
14
  "zod": "^3.22.0",
15
- "@herdctl/core": "5.10.0"
15
+ "@herdctl/core": "5.11.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@vitest/coverage-v8": "^4.0.17",