@jini-ai/agent-runtime 0.2.0 → 0.3.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/README.md +2 -2
- package/dist/agent-protocol/acp/session.d.ts +206 -1
- package/dist/agent-protocol/acp/session.d.ts.map +1 -1
- package/dist/agent-protocol/acp/session.js +505 -350
- package/dist/agent-protocol/acp/session.js.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.d.ts +53 -0
- package/dist/agent-protocol/core/json-line-stream.d.ts.map +1 -1
- package/dist/agent-protocol/core/json-line-stream.js +307 -255
- package/dist/agent-protocol/core/json-line-stream.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts +22 -1
- package/dist/agent-protocol/pi-rpc/events.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/events.js +189 -153
- package/dist/agent-protocol/pi-rpc/events.js.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.d.ts +39 -0
- package/dist/agent-protocol/pi-rpc/session.d.ts.map +1 -1
- package/dist/agent-protocol/pi-rpc/session.js +143 -107
- package/dist/agent-protocol/pi-rpc/session.js.map +1 -1
- package/dist/claude-stream.d.ts +64 -1
- package/dist/claude-stream.d.ts.map +1 -1
- package/dist/claude-stream.js +568 -441
- package/dist/claude-stream.js.map +1 -1
- package/dist/defs/aider.d.ts.map +1 -1
- package/dist/defs/aider.js +13 -1
- package/dist/defs/aider.js.map +1 -1
- package/dist/defs/amp.d.ts.map +1 -1
- package/dist/defs/amp.js +16 -1
- package/dist/defs/amp.js.map +1 -1
- package/dist/defs/amr.d.ts +2 -1
- package/dist/defs/amr.d.ts.map +1 -1
- package/dist/defs/amr.js +12 -1
- package/dist/defs/amr.js.map +1 -1
- package/dist/defs/antigravity.d.ts +41 -2
- package/dist/defs/antigravity.d.ts.map +1 -1
- package/dist/defs/antigravity.js +201 -22
- package/dist/defs/antigravity.js.map +1 -1
- package/dist/defs/claude.d.ts +2 -0
- package/dist/defs/claude.d.ts.map +1 -1
- package/dist/defs/claude.js +58 -3
- package/dist/defs/claude.js.map +1 -1
- package/dist/defs/codebuddy.d.ts.map +1 -1
- package/dist/defs/codebuddy.js +9 -1
- package/dist/defs/codebuddy.js.map +1 -1
- package/dist/defs/codex.d.ts +1 -0
- package/dist/defs/codex.d.ts.map +1 -1
- package/dist/defs/codex.js +34 -0
- package/dist/defs/codex.js.map +1 -1
- package/dist/defs/copilot.d.ts.map +1 -1
- package/dist/defs/copilot.js +15 -1
- package/dist/defs/copilot.js.map +1 -1
- package/dist/defs/cursor-agent.d.ts.map +1 -1
- package/dist/defs/cursor-agent.js +24 -2
- package/dist/defs/cursor-agent.js.map +1 -1
- package/dist/defs/deepseek.d.ts.map +1 -1
- package/dist/defs/deepseek.js +12 -0
- package/dist/defs/deepseek.js.map +1 -1
- package/dist/defs/devin.d.ts +2 -1
- package/dist/defs/devin.d.ts.map +1 -1
- package/dist/defs/devin.js +12 -7
- package/dist/defs/devin.js.map +1 -1
- package/dist/defs/grok-build.d.ts.map +1 -1
- package/dist/defs/grok-build.js +10 -0
- package/dist/defs/grok-build.js.map +1 -1
- package/dist/defs/hermes.d.ts +1 -0
- package/dist/defs/hermes.d.ts.map +1 -1
- package/dist/defs/hermes.js +3 -0
- package/dist/defs/hermes.js.map +1 -1
- package/dist/defs/kilo.d.ts +1 -0
- package/dist/defs/kilo.d.ts.map +1 -1
- package/dist/defs/kilo.js +3 -0
- package/dist/defs/kilo.js.map +1 -1
- package/dist/defs/kimi.d.ts +1 -0
- package/dist/defs/kimi.d.ts.map +1 -1
- package/dist/defs/kimi.js +3 -0
- package/dist/defs/kimi.js.map +1 -1
- package/dist/defs/kiro.d.ts +1 -0
- package/dist/defs/kiro.d.ts.map +1 -1
- package/dist/defs/kiro.js +3 -0
- package/dist/defs/kiro.js.map +1 -1
- package/dist/defs/pi.d.ts +1 -1
- package/dist/defs/pi.d.ts.map +1 -1
- package/dist/defs/pi.js +6 -1
- package/dist/defs/pi.js.map +1 -1
- package/dist/defs/qoder.d.ts +1 -0
- package/dist/defs/qoder.d.ts.map +1 -1
- package/dist/defs/qoder.js +15 -0
- package/dist/defs/qoder.js.map +1 -1
- package/dist/defs/qwen.d.ts.map +1 -1
- package/dist/defs/qwen.js +9 -0
- package/dist/defs/qwen.js.map +1 -1
- package/dist/defs/reasonix.d.ts +1 -0
- package/dist/defs/reasonix.d.ts.map +1 -1
- package/dist/defs/reasonix.js +3 -0
- package/dist/defs/reasonix.js.map +1 -1
- package/dist/defs/shared.d.ts +31 -1
- package/dist/defs/shared.d.ts.map +1 -1
- package/dist/defs/shared.js +35 -0
- package/dist/defs/shared.js.map +1 -1
- package/dist/defs/trae-cli.d.ts +1 -0
- package/dist/defs/trae-cli.d.ts.map +1 -1
- package/dist/defs/trae-cli.js +3 -0
- package/dist/defs/trae-cli.js.map +1 -1
- package/dist/defs/vibe.d.ts +1 -0
- package/dist/defs/vibe.d.ts.map +1 -1
- package/dist/defs/vibe.js +3 -0
- package/dist/defs/vibe.js.map +1 -1
- package/dist/detection.d.ts +1 -1
- package/dist/detection.d.ts.map +1 -1
- package/dist/detection.js +7 -2
- package/dist/detection.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/log-file.d.ts +23 -0
- package/dist/log-file.d.ts.map +1 -0
- package/dist/log-file.js +56 -0
- package/dist/log-file.js.map +1 -0
- package/dist/model-registry.d.ts +7 -45
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js.map +1 -1
- package/dist/providers/anthropic-messages.d.ts +198 -6
- package/dist/providers/anthropic-messages.d.ts.map +1 -1
- package/dist/providers/anthropic-messages.js +317 -140
- package/dist/providers/anthropic-messages.js.map +1 -1
- package/dist/providers/azure-chat.d.ts +113 -3
- package/dist/providers/azure-chat.d.ts.map +1 -1
- package/dist/providers/azure-chat.js +114 -21
- package/dist/providers/azure-chat.js.map +1 -1
- package/dist/providers/connection-guard.d.ts +121 -1
- package/dist/providers/connection-guard.d.ts.map +1 -1
- package/dist/providers/connection-guard.js +205 -3
- package/dist/providers/connection-guard.js.map +1 -1
- package/dist/providers/connection-test.d.ts +62 -0
- package/dist/providers/connection-test.d.ts.map +1 -0
- package/dist/providers/connection-test.js +404 -0
- package/dist/providers/connection-test.js.map +1 -0
- package/dist/providers/elevenlabs.d.ts.map +1 -1
- package/dist/providers/elevenlabs.js +3 -2
- package/dist/providers/elevenlabs.js.map +1 -1
- package/dist/providers/google-messages.d.ts +208 -7
- package/dist/providers/google-messages.d.ts.map +1 -1
- package/dist/providers/google-messages.js +331 -105
- package/dist/providers/google-messages.js.map +1 -1
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/model-catalog.d.ts.map +1 -1
- package/dist/providers/model-catalog.js +21 -0
- package/dist/providers/model-catalog.js.map +1 -1
- package/dist/providers/ollama-chat.d.ts +190 -1
- package/dist/providers/ollama-chat.d.ts.map +1 -1
- package/dist/providers/ollama-chat.js +319 -110
- package/dist/providers/ollama-chat.js.map +1 -1
- package/dist/providers/openai-chat.d.ts +222 -14
- package/dist/providers/openai-chat.d.ts.map +1 -1
- package/dist/providers/openai-chat.js +377 -128
- package/dist/providers/openai-chat.js.map +1 -1
- package/dist/providers/sse-decode.d.ts +2 -2
- package/dist/providers/sse-decode.js +2 -2
- package/dist/types.d.ts +169 -87
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/claude-stream.js
CHANGED
|
@@ -25,11 +25,305 @@
|
|
|
25
25
|
* Callers give us `onEvent({ type, ...payload })`. We track per-content-block
|
|
26
26
|
* state to accumulate partial tool_use input JSON and emit a single
|
|
27
27
|
* `tool_use` event when that block stops.
|
|
28
|
+
*
|
|
29
|
+
* Module layout: functions above `createClaudeStreamHandler` are hoisted,
|
|
30
|
+
* exported, unit-testable pure (or near-pure, dependency-injected) decision
|
|
31
|
+
* logic — they take their inputs explicitly rather than reading them off a
|
|
32
|
+
* closure. `createClaudeStreamHandler` itself, and everything declared
|
|
33
|
+
* inside it, is the genuinely stateful part: per-message id/epoch
|
|
34
|
+
* bookkeeping, the block-accumulation map, and the artifact-echo
|
|
35
|
+
* dedup state machine all mutate several interdependent fields together
|
|
36
|
+
* (see the inline comments on `anonymousMessageEpoch` and
|
|
37
|
+
* `stripDuplicateArtifactText` for the specific invariants), so pulling
|
|
38
|
+
* those out into standalone functions would mean designing an explicit
|
|
39
|
+
* reducer over the whole stream state rather than extracting a helper —
|
|
40
|
+
* a larger, riskier change than this pass makes. They stay as closures,
|
|
41
|
+
* exercised only through `feed`/`flush` in the test suite.
|
|
28
42
|
*/
|
|
29
43
|
import { createRoleMarkerGuard } from './role-marker-guard.js';
|
|
30
44
|
function isRecord(value) {
|
|
31
45
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
32
46
|
}
|
|
47
|
+
const WRITE_TOOL_NAMES = new Set(['Write', 'Edit', 'write_file', 'replace']);
|
|
48
|
+
export function toolInputPath(input) {
|
|
49
|
+
if (typeof input.file_path === 'string')
|
|
50
|
+
return input.file_path;
|
|
51
|
+
if (typeof input.path === 'string')
|
|
52
|
+
return input.path;
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
// Runtime-task status aliases the Claude Code wire format has used across
|
|
56
|
+
// versions, normalized to the four statuses the UI understands.
|
|
57
|
+
const TASK_STATUS_ALIASES = {
|
|
58
|
+
completed: 'completed',
|
|
59
|
+
in_progress: 'in_progress',
|
|
60
|
+
stopped: 'stopped',
|
|
61
|
+
complete: 'completed',
|
|
62
|
+
done: 'completed',
|
|
63
|
+
doing: 'in_progress',
|
|
64
|
+
active: 'in_progress',
|
|
65
|
+
failed: 'stopped',
|
|
66
|
+
canceled: 'stopped',
|
|
67
|
+
cancelled: 'stopped',
|
|
68
|
+
};
|
|
69
|
+
export function normalizeTaskStatus(value) {
|
|
70
|
+
if (typeof value !== 'string')
|
|
71
|
+
return 'pending';
|
|
72
|
+
return TASK_STATUS_ALIASES[value] ?? 'pending';
|
|
73
|
+
}
|
|
74
|
+
function nextGeneratedRuntimeTaskId(counter) {
|
|
75
|
+
// Coverage-driven refactor (2026-07-18, no behavior change): the
|
|
76
|
+
// original OD source guarded the returned id with
|
|
77
|
+
// `while (runtimeTasks.has(String(nextRuntimeTaskId))) nextRuntimeTaskId += 1;`.
|
|
78
|
+
// Tracing every path that can insert a numeric id into `runtimeTasks`
|
|
79
|
+
// shows the guard can never trigger: the only two insertion paths are
|
|
80
|
+
// (a) this function itself, which always hands out the current
|
|
81
|
+
// `nextRuntimeTaskId` and then increments it, so that exact value is
|
|
82
|
+
// never in the map yet; and (b) `runtimeTaskIdFromCreate`'s
|
|
83
|
+
// explicit-id branch, which bumps `nextRuntimeTaskId` to
|
|
84
|
+
// `numericId + 1` (strictly past itself) BEFORE this function could
|
|
85
|
+
// ever be asked to reuse that id. Since `nextRuntimeTaskId` only ever
|
|
86
|
+
// increases, it can never equal an id already present in the map at
|
|
87
|
+
// this point. Removed per the coverage skill's Phase 6.5 "dead
|
|
88
|
+
// branch" classification rather than fake a test for an unreachable
|
|
89
|
+
// loop; see source-map.md.
|
|
90
|
+
const id = String(counter.next);
|
|
91
|
+
counter.next += 1;
|
|
92
|
+
return id;
|
|
93
|
+
}
|
|
94
|
+
export function runtimeTaskIdFromCreate(input, counter) {
|
|
95
|
+
if (typeof input.taskId === 'string' && input.taskId) {
|
|
96
|
+
const numericId = Number(input.taskId);
|
|
97
|
+
if (Number.isSafeInteger(numericId) && numericId >= counter.next) {
|
|
98
|
+
counter.next = numericId + 1;
|
|
99
|
+
}
|
|
100
|
+
return input.taskId;
|
|
101
|
+
}
|
|
102
|
+
return nextGeneratedRuntimeTaskId(counter);
|
|
103
|
+
}
|
|
104
|
+
export function applyTaskCreate(input, tasks, counter) {
|
|
105
|
+
const content = typeof input.subject === 'string'
|
|
106
|
+
? input.subject
|
|
107
|
+
: typeof input.description === 'string'
|
|
108
|
+
? input.description
|
|
109
|
+
: '';
|
|
110
|
+
if (!content)
|
|
111
|
+
return false;
|
|
112
|
+
const id = runtimeTaskIdFromCreate(input, counter);
|
|
113
|
+
const activeForm = typeof input.activeForm === 'string' ? input.activeForm : undefined;
|
|
114
|
+
tasks.set(id, {
|
|
115
|
+
id,
|
|
116
|
+
content,
|
|
117
|
+
status: normalizeTaskStatus(input.status),
|
|
118
|
+
...(activeForm ? { activeForm } : {}),
|
|
119
|
+
});
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
export function applyTaskUpdate(input, tasks) {
|
|
123
|
+
if (typeof input.taskId !== 'string')
|
|
124
|
+
return false;
|
|
125
|
+
const existing = tasks.get(input.taskId);
|
|
126
|
+
if (!existing)
|
|
127
|
+
return false;
|
|
128
|
+
const content = typeof input.subject === 'string'
|
|
129
|
+
? input.subject
|
|
130
|
+
: typeof input.description === 'string'
|
|
131
|
+
? input.description
|
|
132
|
+
: existing.content;
|
|
133
|
+
const activeForm = typeof input.activeForm === 'string' ? input.activeForm : existing.activeForm;
|
|
134
|
+
tasks.set(input.taskId, {
|
|
135
|
+
...existing,
|
|
136
|
+
content,
|
|
137
|
+
status: normalizeTaskStatus(input.status),
|
|
138
|
+
...(activeForm ? { activeForm } : {}),
|
|
139
|
+
});
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
export function emitCanonicalTaskSnapshot(toolUseId, name, input, registry, onEvent) {
|
|
143
|
+
if (typeof toolUseId !== 'string' || typeof name !== 'string' || !isRecord(input))
|
|
144
|
+
return false;
|
|
145
|
+
if (registry.seenToolUseIds.has(toolUseId))
|
|
146
|
+
return true;
|
|
147
|
+
const applied = name === 'TaskCreate'
|
|
148
|
+
? applyTaskCreate(input, registry.tasks, registry.counter)
|
|
149
|
+
: name === 'TaskUpdate'
|
|
150
|
+
? applyTaskUpdate(input, registry.tasks)
|
|
151
|
+
: false;
|
|
152
|
+
if (!applied)
|
|
153
|
+
return false;
|
|
154
|
+
// Coverage-driven refactor (2026-07-18, no behavior change): the
|
|
155
|
+
// original OD source guarded this emission with
|
|
156
|
+
// `if (!changed || runtimeTasks.size === 0) return false;`. Tracing
|
|
157
|
+
// every path into this point shows both conditions are unreachable —
|
|
158
|
+
// the `TaskCreate` and `TaskUpdate` branches above only fall through
|
|
159
|
+
// to here after an unconditional `runtimeTasks.set(...)` (so `size`
|
|
160
|
+
// can never be 0), and every early-return path above returns before
|
|
161
|
+
// reaching here (so the "changed" flag those returns guarded was
|
|
162
|
+
// always true here). Removed the dead double-guard per the coverage
|
|
163
|
+
// skill's Phase 6.5 "dead branch" classification rather than fake a
|
|
164
|
+
// test for an unreachable condition; see source-map.md.
|
|
165
|
+
registry.seenToolUseIds.add(toolUseId);
|
|
166
|
+
onEvent({
|
|
167
|
+
type: 'tool_use',
|
|
168
|
+
id: `${toolUseId}:todo-task`,
|
|
169
|
+
name: 'TodoWrite',
|
|
170
|
+
input: {
|
|
171
|
+
todos: Array.from(registry.tasks.values()).map(({ content, status, activeForm }) => ({
|
|
172
|
+
content,
|
|
173
|
+
status,
|
|
174
|
+
...(activeForm ? { activeForm } : {}),
|
|
175
|
+
})),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
export function isFileWriteToolUse(name, input) {
|
|
181
|
+
if (typeof name !== 'string' || !isRecord(input))
|
|
182
|
+
return false;
|
|
183
|
+
if (!WRITE_TOOL_NAMES.has(name))
|
|
184
|
+
return false;
|
|
185
|
+
if (/\.(html|htm|css|js|jsx|ts|tsx|md)$/iu.test(toolInputPath(input)))
|
|
186
|
+
return true;
|
|
187
|
+
return typeof input.content === 'string' || typeof input.new_string === 'string';
|
|
188
|
+
}
|
|
189
|
+
// Coverage-driven refactor (2026-07-18, no behavior change): both
|
|
190
|
+
// `fileWriteContent` and `isHtmlWriteToolInput` originally opened with
|
|
191
|
+
// their own `if (!isRecord(input)) return ...;` guard. Each function
|
|
192
|
+
// has exactly one call site (`emitToolUse`, for both), and both call
|
|
193
|
+
// sites already sit inside an `if (isFileWriteToolUse(name, input))`
|
|
194
|
+
// block, which itself already asserted `isRecord(input)` before
|
|
195
|
+
// returning true. So `input` is always a record by the time either
|
|
196
|
+
// function runs — the guards duplicated a check the shared caller
|
|
197
|
+
// already made. Removed the runtime early-return per the coverage
|
|
198
|
+
// skill's Phase 6.5 "dead branch" classification rather than fake a
|
|
199
|
+
// test with a non-record `input` neither function can ever actually
|
|
200
|
+
// receive (see source-map.md), replaced by a one-line type assertion
|
|
201
|
+
// (classification 4: TS-required, no real runtime path) so the
|
|
202
|
+
// parameter can stay `unknown` without disturbing the call sites.
|
|
203
|
+
export function fileWriteContent(input) {
|
|
204
|
+
const record = input;
|
|
205
|
+
if (typeof record.content === 'string')
|
|
206
|
+
return record.content;
|
|
207
|
+
if (typeof record.new_string === 'string')
|
|
208
|
+
return record.new_string;
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
export function isHtmlWriteToolInput(input) {
|
|
212
|
+
const record = input;
|
|
213
|
+
const rawPath = record.file_path ?? record.filePath;
|
|
214
|
+
if (typeof rawPath === 'string' && /\.(?:html?|xhtml)$/i.test(rawPath))
|
|
215
|
+
return true;
|
|
216
|
+
const content = fileWriteContent(input);
|
|
217
|
+
return typeof content === 'string' && /<!doctype\s+html\b|<html\b/i.test(content);
|
|
218
|
+
}
|
|
219
|
+
export function artifactOpenCandidateLength(text, openTag) {
|
|
220
|
+
const max = Math.min(openTag.length - 1, text.length);
|
|
221
|
+
for (let len = max; len > 0; len -= 1) {
|
|
222
|
+
if (openTag.startsWith(text.slice(-len)))
|
|
223
|
+
return len;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
function isHtmlArtifact(candidate) {
|
|
228
|
+
const openTag = candidate.slice(0, Math.max(0, candidate.indexOf('>') + 1));
|
|
229
|
+
return /\btype\s*=\s*["']text\/html["']/i.test(openTag);
|
|
230
|
+
}
|
|
231
|
+
function normalizeArtifactEchoContent(value) {
|
|
232
|
+
return value
|
|
233
|
+
.replace(/^\uFEFF/, '')
|
|
234
|
+
.replace(/\r\n?/g, '\n')
|
|
235
|
+
.replace(/^(?:\s|\\r|\\n)+|(?:\s|\\r|\\n)+$/g, '');
|
|
236
|
+
}
|
|
237
|
+
export function isRedundantWrittenArtifact(candidate, ctx) {
|
|
238
|
+
const gt = candidate.indexOf('>');
|
|
239
|
+
const close = candidate.lastIndexOf('</artifact>');
|
|
240
|
+
if (gt === -1 || close === -1 || close <= gt)
|
|
241
|
+
return false;
|
|
242
|
+
if (ctx.suppressHtmlArtifactsAfterFileWrite && isHtmlArtifact(candidate) && ctx.wroteHtmlFileThisTurn) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
const body = normalizeArtifactEchoContent(candidate.slice(gt + 1, close));
|
|
246
|
+
return ctx.recentWriteContents.some((content) => content === body);
|
|
247
|
+
}
|
|
248
|
+
export function assistantText(content) {
|
|
249
|
+
const parts = [];
|
|
250
|
+
for (const block of content) {
|
|
251
|
+
if (isRecord(block) && block.type === 'text' && typeof block.text === 'string') {
|
|
252
|
+
parts.push(block.text);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return parts.join('\n').trim();
|
|
256
|
+
}
|
|
257
|
+
export function isUnstreamedTextBlock(block, alreadyStreamed) {
|
|
258
|
+
return !alreadyStreamed && block.type === 'text' && typeof block.text === 'string' && block.text.length > 0;
|
|
259
|
+
}
|
|
260
|
+
export function isUnstreamedThinkingBlock(block, alreadyStreamed) {
|
|
261
|
+
return (!alreadyStreamed && block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.length > 0);
|
|
262
|
+
}
|
|
263
|
+
export function handleSystemMessage(obj, onEvent) {
|
|
264
|
+
if (obj.subtype === 'init') {
|
|
265
|
+
onEvent({
|
|
266
|
+
type: 'status',
|
|
267
|
+
label: 'initializing',
|
|
268
|
+
model: obj.model ?? null,
|
|
269
|
+
sessionId: obj.session_id ?? null,
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (obj.subtype === 'status') {
|
|
274
|
+
onEvent({ type: 'status', label: typeof obj.status === 'string' ? obj.status : 'working' });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
export function stringifyToolResult(content) {
|
|
278
|
+
if (typeof content === 'string')
|
|
279
|
+
return content;
|
|
280
|
+
if (Array.isArray(content)) {
|
|
281
|
+
return content
|
|
282
|
+
.map((c) => (isRecord(c) && c.type === 'text' ? String(c.text) : JSON.stringify(c)))
|
|
283
|
+
.join('\n');
|
|
284
|
+
}
|
|
285
|
+
return JSON.stringify(content);
|
|
286
|
+
}
|
|
287
|
+
// `user` messages in a stream-json transcript are usually tool_result
|
|
288
|
+
// wrappers from prior turns.
|
|
289
|
+
export function handleUserMessage(obj, onEvent) {
|
|
290
|
+
if (!isRecord(obj.message) || !Array.isArray(obj.message.content))
|
|
291
|
+
return;
|
|
292
|
+
for (const block of obj.message.content) {
|
|
293
|
+
if (!isRecord(block))
|
|
294
|
+
continue;
|
|
295
|
+
if (block.type === 'tool_result') {
|
|
296
|
+
onEvent({
|
|
297
|
+
type: 'tool_result',
|
|
298
|
+
toolUseId: block.tool_use_id,
|
|
299
|
+
content: stringifyToolResult(block.content),
|
|
300
|
+
isError: Boolean(block.is_error),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function applyInputJsonDelta(state, partialJson, onEvent) {
|
|
306
|
+
if (!state || state.type !== 'tool_use')
|
|
307
|
+
return;
|
|
308
|
+
state.input += partialJson;
|
|
309
|
+
if (typeof state.id === 'string' && typeof state.name === 'string') {
|
|
310
|
+
onEvent({
|
|
311
|
+
type: 'tool_input_delta',
|
|
312
|
+
id: state.id,
|
|
313
|
+
name: state.name,
|
|
314
|
+
delta: partialJson,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function emitAssistantErrorIfPresent(obj, content, onEvent) {
|
|
319
|
+
if (typeof obj.error !== 'string' || !obj.error.trim())
|
|
320
|
+
return;
|
|
321
|
+
onEvent({
|
|
322
|
+
type: 'error',
|
|
323
|
+
message: assistantText(content) || obj.error,
|
|
324
|
+
code: obj.error,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
33
327
|
export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
34
328
|
let buffer = '';
|
|
35
329
|
// Per-content-block scratch, keyed by `${messageId}:${blockIndex}`.
|
|
@@ -58,11 +352,24 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
58
352
|
// Remember the message id that already emitted `turn_end` so accepting
|
|
59
353
|
// both wire shapes cannot double-trigger stdin continuation/closure.
|
|
60
354
|
let turnEndSignature = null;
|
|
355
|
+
// Stands in for `currentMessageId` in the dedup key when a frame carried no
|
|
356
|
+
// message id at all. Without it the key is null and the guard below cannot
|
|
357
|
+
// fire, so an id-less assistant wrapper plus the same-reason `result` frame
|
|
358
|
+
// that follows it emit two identical `turn_end` events. It is bumped only at
|
|
359
|
+
// `message_start` and at an id-less `assistant` wrapper — the two frames that
|
|
360
|
+
// actually begin a new assistant message — and deliberately NOT at
|
|
361
|
+
// `message_delta`/`result`, which report on the message already in flight.
|
|
362
|
+
// Bumping it on a real message boundary is what keeps this from over-deduping:
|
|
363
|
+
// two consecutive id-less `tool_use` turns still emit twice, and a suppressed
|
|
364
|
+
// `tool_use` turn_end would strand the daemon's stdin-close handler.
|
|
365
|
+
let anonymousMessageEpoch = 0;
|
|
61
366
|
// Per-message role-marker guards for cross-chunk detection (#3247).
|
|
62
367
|
const roleGuards = new Map();
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
368
|
+
const taskRegistry = {
|
|
369
|
+
tasks: new Map(),
|
|
370
|
+
counter: { next: 1 },
|
|
371
|
+
seenToolUseIds: new Set(),
|
|
372
|
+
};
|
|
66
373
|
let suppressNextArtifactText = false;
|
|
67
374
|
let suppressDuplicateArtifactText = false;
|
|
68
375
|
let artifactOpenCandidate = '';
|
|
@@ -71,8 +378,11 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
71
378
|
const recentWriteContents = [];
|
|
72
379
|
let wroteHtmlFileThisTurn = false;
|
|
73
380
|
function emitTurnEnd(stopReason) {
|
|
74
|
-
|
|
75
|
-
|
|
381
|
+
// `\u0001` prefixes the synthetic key so it can never collide with a real
|
|
382
|
+
// message id, which is why this is safe to compare in the same slot.
|
|
383
|
+
const messageKey = currentMessageId ?? `\u0001anon${anonymousMessageEpoch}`;
|
|
384
|
+
const signature = `${messageKey}\u0000${stopReason}`;
|
|
385
|
+
if (turnEndSignature === signature)
|
|
76
386
|
return;
|
|
77
387
|
onEvent({ type: 'turn_end', stopReason });
|
|
78
388
|
turnEndSignature = signature;
|
|
@@ -81,120 +391,8 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
81
391
|
wroteHtmlFileThisTurn = false;
|
|
82
392
|
}
|
|
83
393
|
}
|
|
84
|
-
function normalizeTaskStatus(value) {
|
|
85
|
-
if (value === 'completed' || value === 'in_progress' || value === 'stopped') {
|
|
86
|
-
return value;
|
|
87
|
-
}
|
|
88
|
-
if (value === 'complete' || value === 'done')
|
|
89
|
-
return 'completed';
|
|
90
|
-
if (value === 'doing' || value === 'active')
|
|
91
|
-
return 'in_progress';
|
|
92
|
-
if (value === 'failed' || value === 'canceled' || value === 'cancelled')
|
|
93
|
-
return 'stopped';
|
|
94
|
-
return 'pending';
|
|
95
|
-
}
|
|
96
|
-
function nextGeneratedRuntimeTaskId() {
|
|
97
|
-
// Coverage-driven refactor (2026-07-18, no behavior change): the
|
|
98
|
-
// original OD source guarded the returned id with
|
|
99
|
-
// `while (runtimeTasks.has(String(nextRuntimeTaskId))) nextRuntimeTaskId += 1;`.
|
|
100
|
-
// Tracing every path that can insert a numeric id into `runtimeTasks`
|
|
101
|
-
// shows the guard can never trigger: the only two insertion paths are
|
|
102
|
-
// (a) this function itself, which always hands out the current
|
|
103
|
-
// `nextRuntimeTaskId` and then increments it, so that exact value is
|
|
104
|
-
// never in the map yet; and (b) `runtimeTaskIdFromCreate`'s
|
|
105
|
-
// explicit-id branch, which bumps `nextRuntimeTaskId` to
|
|
106
|
-
// `numericId + 1` (strictly past itself) BEFORE this function could
|
|
107
|
-
// ever be asked to reuse that id. Since `nextRuntimeTaskId` only ever
|
|
108
|
-
// increases, it can never equal an id already present in the map at
|
|
109
|
-
// this point. Removed per the coverage skill's Phase 6.5 "dead
|
|
110
|
-
// branch" classification rather than fake a test for an unreachable
|
|
111
|
-
// loop; see source-map.md.
|
|
112
|
-
const id = String(nextRuntimeTaskId);
|
|
113
|
-
nextRuntimeTaskId += 1;
|
|
114
|
-
return id;
|
|
115
|
-
}
|
|
116
|
-
function runtimeTaskIdFromCreate(input) {
|
|
117
|
-
if (typeof input.taskId === 'string' && input.taskId) {
|
|
118
|
-
const numericId = Number(input.taskId);
|
|
119
|
-
if (Number.isSafeInteger(numericId) && numericId >= nextRuntimeTaskId) {
|
|
120
|
-
nextRuntimeTaskId = numericId + 1;
|
|
121
|
-
}
|
|
122
|
-
return input.taskId;
|
|
123
|
-
}
|
|
124
|
-
return nextGeneratedRuntimeTaskId();
|
|
125
|
-
}
|
|
126
|
-
function emitCanonicalTaskSnapshot(toolUseId, name, input) {
|
|
127
|
-
if (typeof toolUseId !== 'string' || typeof name !== 'string' || !isRecord(input))
|
|
128
|
-
return false;
|
|
129
|
-
if (canonicalTaskToolUseIds.has(toolUseId))
|
|
130
|
-
return true;
|
|
131
|
-
if (name === 'TaskCreate') {
|
|
132
|
-
const content = typeof input.subject === 'string'
|
|
133
|
-
? input.subject
|
|
134
|
-
: typeof input.description === 'string'
|
|
135
|
-
? input.description
|
|
136
|
-
: '';
|
|
137
|
-
if (!content)
|
|
138
|
-
return false;
|
|
139
|
-
const id = runtimeTaskIdFromCreate(input);
|
|
140
|
-
const activeForm = typeof input.activeForm === 'string' ? input.activeForm : undefined;
|
|
141
|
-
runtimeTasks.set(id, {
|
|
142
|
-
id,
|
|
143
|
-
content,
|
|
144
|
-
status: normalizeTaskStatus(input.status),
|
|
145
|
-
...(activeForm ? { activeForm } : {}),
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
else if (name === 'TaskUpdate') {
|
|
149
|
-
if (typeof input.taskId !== 'string')
|
|
150
|
-
return false;
|
|
151
|
-
const existing = runtimeTasks.get(input.taskId);
|
|
152
|
-
if (!existing)
|
|
153
|
-
return false;
|
|
154
|
-
const content = typeof input.subject === 'string'
|
|
155
|
-
? input.subject
|
|
156
|
-
: typeof input.description === 'string'
|
|
157
|
-
? input.description
|
|
158
|
-
: existing.content;
|
|
159
|
-
const activeForm = typeof input.activeForm === 'string' ? input.activeForm : existing.activeForm;
|
|
160
|
-
runtimeTasks.set(input.taskId, {
|
|
161
|
-
...existing,
|
|
162
|
-
content,
|
|
163
|
-
status: normalizeTaskStatus(input.status),
|
|
164
|
-
...(activeForm ? { activeForm } : {}),
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
// Coverage-driven refactor (2026-07-18, no behavior change): the
|
|
171
|
-
// original OD source guarded this emission with
|
|
172
|
-
// `if (!changed || runtimeTasks.size === 0) return false;`. Tracing
|
|
173
|
-
// every path into this point shows both conditions are unreachable —
|
|
174
|
-
// the `TaskCreate` and `TaskUpdate` branches above only fall through
|
|
175
|
-
// to here after an unconditional `runtimeTasks.set(...)` (so `size`
|
|
176
|
-
// can never be 0), and every early-return path above returns before
|
|
177
|
-
// reaching here (so the "changed" flag those returns guarded was
|
|
178
|
-
// always true here). Removed the dead double-guard per the coverage
|
|
179
|
-
// skill's Phase 6.5 "dead branch" classification rather than fake a
|
|
180
|
-
// test for an unreachable condition; see source-map.md.
|
|
181
|
-
canonicalTaskToolUseIds.add(toolUseId);
|
|
182
|
-
onEvent({
|
|
183
|
-
type: 'tool_use',
|
|
184
|
-
id: `${toolUseId}:todo-task`,
|
|
185
|
-
name: 'TodoWrite',
|
|
186
|
-
input: {
|
|
187
|
-
todos: Array.from(runtimeTasks.values()).map(({ content, status, activeForm }) => ({
|
|
188
|
-
content,
|
|
189
|
-
status,
|
|
190
|
-
...(activeForm ? { activeForm } : {}),
|
|
191
|
-
})),
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
394
|
function emitToolUse(id, name, input) {
|
|
197
|
-
if (emitCanonicalTaskSnapshot(id, name, input))
|
|
395
|
+
if (emitCanonicalTaskSnapshot(id, name, input, taskRegistry, onEvent))
|
|
198
396
|
return;
|
|
199
397
|
if (isFileWriteToolUse(name, input)) {
|
|
200
398
|
suppressNextArtifactText = true;
|
|
@@ -230,6 +428,14 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
230
428
|
// any compensating security benefit. See PR #3303 review
|
|
231
429
|
// r3324xxxxxx. Thinking is passed through unguarded; only the
|
|
232
430
|
// user-visible text channel is policed.
|
|
431
|
+
function getOrCreateRoleGuard(msgId) {
|
|
432
|
+
let guard = roleGuards.get(msgId);
|
|
433
|
+
if (!guard) {
|
|
434
|
+
guard = createRoleMarkerGuard(msgId);
|
|
435
|
+
roleGuards.set(msgId, guard);
|
|
436
|
+
}
|
|
437
|
+
return guard;
|
|
438
|
+
}
|
|
233
439
|
function emitSafeText(msgId, text, eventType = 'text_delta') {
|
|
234
440
|
if (eventType === 'text_delta') {
|
|
235
441
|
text = stripDuplicateArtifactText(text);
|
|
@@ -240,11 +446,7 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
240
446
|
onEvent({ type: eventType, delta: text });
|
|
241
447
|
return;
|
|
242
448
|
}
|
|
243
|
-
|
|
244
|
-
if (!guard) {
|
|
245
|
-
guard = createRoleMarkerGuard(msgId);
|
|
246
|
-
roleGuards.set(msgId, guard);
|
|
247
|
-
}
|
|
449
|
+
const guard = getOrCreateRoleGuard(msgId);
|
|
248
450
|
if (guard.contaminated)
|
|
249
451
|
return;
|
|
250
452
|
const safe = guard.feedText(text);
|
|
@@ -257,6 +459,51 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
257
459
|
onEvent(warn);
|
|
258
460
|
}
|
|
259
461
|
}
|
|
462
|
+
// Resolves an in-progress `<artifact>...</artifact>` echo once its close
|
|
463
|
+
// tag has arrived: drops it if it duplicates a recent file write, then
|
|
464
|
+
// resumes stripping on whatever text followed the close tag.
|
|
465
|
+
function resolveDuplicateArtifactCandidate(current) {
|
|
466
|
+
duplicateArtifactCandidate += current;
|
|
467
|
+
const closeIndex = duplicateArtifactCandidate.indexOf('</artifact>');
|
|
468
|
+
if (closeIndex === -1)
|
|
469
|
+
return '';
|
|
470
|
+
const closeEnd = closeIndex + '</artifact>'.length;
|
|
471
|
+
const candidate = duplicateArtifactCandidate.slice(0, closeEnd);
|
|
472
|
+
const rest = duplicateArtifactCandidate.slice(closeEnd);
|
|
473
|
+
duplicateArtifactCandidate = '';
|
|
474
|
+
suppressDuplicateArtifactText = false;
|
|
475
|
+
suppressNextArtifactText = false;
|
|
476
|
+
const duplicate = isRedundantWrittenArtifact(candidate, {
|
|
477
|
+
suppressHtmlArtifactsAfterFileWrite: options.suppressHtmlArtifactsAfterFileWrite === true,
|
|
478
|
+
wroteHtmlFileThisTurn,
|
|
479
|
+
recentWriteContents,
|
|
480
|
+
});
|
|
481
|
+
if (options.suppressHtmlArtifactsAfterFileWrite !== true) {
|
|
482
|
+
recentWriteContents.length = 0;
|
|
483
|
+
}
|
|
484
|
+
return `${duplicate ? '' : candidate}${stripDuplicateArtifactText(rest)}`;
|
|
485
|
+
}
|
|
486
|
+
// No open `<artifact` tag has been seen yet: either buffer a partial tag
|
|
487
|
+
// prefix at the end of `current` so it can match across chunk boundaries,
|
|
488
|
+
// or pass the text through untouched.
|
|
489
|
+
function bufferPartialArtifactOpenTag(current, openTag) {
|
|
490
|
+
const candidateLength = artifactOpenCandidateLength(current, openTag);
|
|
491
|
+
if ((suppressNextArtifactText || recentWriteContents.length > 0) && candidateLength > 0) {
|
|
492
|
+
artifactOpenCandidate = current.slice(-candidateLength);
|
|
493
|
+
return current.slice(0, -candidateLength);
|
|
494
|
+
}
|
|
495
|
+
return current;
|
|
496
|
+
}
|
|
497
|
+
// An `<artifact` open tag just arrived: start accumulating it as a
|
|
498
|
+
// duplicate-write candidate and flush whatever text preceded it.
|
|
499
|
+
function startDuplicateArtifactCandidate(current, openIndex) {
|
|
500
|
+
suppressDuplicateArtifactText = true;
|
|
501
|
+
suppressNextArtifactText = false;
|
|
502
|
+
duplicateArtifactCandidate = current.slice(openIndex);
|
|
503
|
+
const prefix = `${pendingArtifactText}${current.slice(0, openIndex)}`;
|
|
504
|
+
pendingArtifactText = '';
|
|
505
|
+
return `${prefix}${stripDuplicateArtifactText('')}`;
|
|
506
|
+
}
|
|
260
507
|
function stripDuplicateArtifactText(text) {
|
|
261
508
|
if (!suppressNextArtifactText &&
|
|
262
509
|
!suppressDuplicateArtifactText &&
|
|
@@ -268,115 +515,13 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
268
515
|
const current = `${artifactOpenCandidate}${text}`;
|
|
269
516
|
artifactOpenCandidate = '';
|
|
270
517
|
if (suppressDuplicateArtifactText) {
|
|
271
|
-
|
|
272
|
-
const closeIndex = duplicateArtifactCandidate.indexOf('</artifact>');
|
|
273
|
-
if (closeIndex === -1)
|
|
274
|
-
return '';
|
|
275
|
-
const closeEnd = closeIndex + '</artifact>'.length;
|
|
276
|
-
const candidate = duplicateArtifactCandidate.slice(0, closeEnd);
|
|
277
|
-
const rest = duplicateArtifactCandidate.slice(closeEnd);
|
|
278
|
-
duplicateArtifactCandidate = '';
|
|
279
|
-
suppressDuplicateArtifactText = false;
|
|
280
|
-
suppressNextArtifactText = false;
|
|
281
|
-
const duplicate = isRedundantWrittenArtifact(candidate);
|
|
282
|
-
if (options.suppressHtmlArtifactsAfterFileWrite !== true) {
|
|
283
|
-
recentWriteContents.length = 0;
|
|
284
|
-
}
|
|
285
|
-
return `${duplicate ? '' : candidate}${stripDuplicateArtifactText(rest)}`;
|
|
518
|
+
return resolveDuplicateArtifactCandidate(current);
|
|
286
519
|
}
|
|
287
520
|
const openIndex = current.indexOf(openTag);
|
|
288
521
|
if (openIndex === -1) {
|
|
289
|
-
|
|
290
|
-
if ((suppressNextArtifactText || recentWriteContents.length > 0) && candidateLength > 0) {
|
|
291
|
-
artifactOpenCandidate = current.slice(-candidateLength);
|
|
292
|
-
return current.slice(0, -candidateLength);
|
|
293
|
-
}
|
|
294
|
-
return current;
|
|
522
|
+
return bufferPartialArtifactOpenTag(current, openTag);
|
|
295
523
|
}
|
|
296
|
-
|
|
297
|
-
suppressNextArtifactText = false;
|
|
298
|
-
duplicateArtifactCandidate = current.slice(openIndex);
|
|
299
|
-
const prefix = `${pendingArtifactText}${current.slice(0, openIndex)}`;
|
|
300
|
-
pendingArtifactText = '';
|
|
301
|
-
return `${prefix}${stripDuplicateArtifactText('')}`;
|
|
302
|
-
}
|
|
303
|
-
function isRedundantWrittenArtifact(candidate) {
|
|
304
|
-
const gt = candidate.indexOf('>');
|
|
305
|
-
const close = candidate.lastIndexOf('</artifact>');
|
|
306
|
-
if (gt === -1 || close === -1 || close <= gt)
|
|
307
|
-
return false;
|
|
308
|
-
if (options.suppressHtmlArtifactsAfterFileWrite === true &&
|
|
309
|
-
isHtmlArtifact(candidate) &&
|
|
310
|
-
wroteHtmlFileThisTurn)
|
|
311
|
-
return true;
|
|
312
|
-
const body = normalizeArtifactEchoContent(candidate.slice(gt + 1, close));
|
|
313
|
-
return recentWriteContents.some((content) => content === body);
|
|
314
|
-
}
|
|
315
|
-
function isHtmlArtifact(candidate) {
|
|
316
|
-
const openTag = candidate.slice(0, Math.max(0, candidate.indexOf('>') + 1));
|
|
317
|
-
return /\btype\s*=\s*["']text\/html["']/i.test(openTag);
|
|
318
|
-
}
|
|
319
|
-
function normalizeArtifactEchoContent(value) {
|
|
320
|
-
return value
|
|
321
|
-
.replace(/^\uFEFF/, '')
|
|
322
|
-
.replace(/\r\n?/g, '\n')
|
|
323
|
-
.replace(/^(?:\s|\\r|\\n)+|(?:\s|\\r|\\n)+$/g, '');
|
|
324
|
-
}
|
|
325
|
-
function artifactOpenCandidateLength(text, openTag) {
|
|
326
|
-
const max = Math.min(openTag.length - 1, text.length);
|
|
327
|
-
for (let len = max; len > 0; len -= 1) {
|
|
328
|
-
if (openTag.startsWith(text.slice(-len)))
|
|
329
|
-
return len;
|
|
330
|
-
}
|
|
331
|
-
return 0;
|
|
332
|
-
}
|
|
333
|
-
function isFileWriteToolUse(name, input) {
|
|
334
|
-
if (typeof name !== 'string' || !isRecord(input))
|
|
335
|
-
return false;
|
|
336
|
-
const path = typeof input.file_path === 'string'
|
|
337
|
-
? input.file_path
|
|
338
|
-
: typeof input.path === 'string'
|
|
339
|
-
? input.path
|
|
340
|
-
: '';
|
|
341
|
-
const writesFile = name === 'Write' ||
|
|
342
|
-
name === 'Edit' ||
|
|
343
|
-
name === 'write_file' ||
|
|
344
|
-
name === 'replace';
|
|
345
|
-
if (!writesFile)
|
|
346
|
-
return false;
|
|
347
|
-
if (/\.(html|htm|css|js|jsx|ts|tsx|md)$/iu.test(path))
|
|
348
|
-
return true;
|
|
349
|
-
return typeof input.content === 'string' || typeof input.new_string === 'string';
|
|
350
|
-
}
|
|
351
|
-
// Coverage-driven refactor (2026-07-18, no behavior change): both
|
|
352
|
-
// `fileWriteContent` and `isHtmlWriteToolInput` originally opened with
|
|
353
|
-
// their own `if (!isRecord(input)) return ...;` guard. Each function
|
|
354
|
-
// has exactly one call site (`emitToolUse`, for both), and both call
|
|
355
|
-
// sites already sit inside an `if (isFileWriteToolUse(name, input))`
|
|
356
|
-
// block, which itself already asserted `isRecord(input)` before
|
|
357
|
-
// returning true. So `input` is always a record by the time either
|
|
358
|
-
// function runs — the guards duplicated a check the shared caller
|
|
359
|
-
// already made. Removed the runtime early-return per the coverage
|
|
360
|
-
// skill's Phase 6.5 "dead branch" classification rather than fake a
|
|
361
|
-
// test with a non-record `input` neither function can ever actually
|
|
362
|
-
// receive (see source-map.md), replaced by a one-line type assertion
|
|
363
|
-
// (classification 4: TS-required, no real runtime path) so the
|
|
364
|
-
// parameter can stay `unknown` without disturbing the call sites.
|
|
365
|
-
function fileWriteContent(input) {
|
|
366
|
-
const record = input;
|
|
367
|
-
if (typeof record.content === 'string')
|
|
368
|
-
return record.content;
|
|
369
|
-
if (typeof record.new_string === 'string')
|
|
370
|
-
return record.new_string;
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
function isHtmlWriteToolInput(input) {
|
|
374
|
-
const record = input;
|
|
375
|
-
const rawPath = record.file_path ?? record.filePath;
|
|
376
|
-
if (typeof rawPath === 'string' && /\.(?:html?|xhtml)$/i.test(rawPath))
|
|
377
|
-
return true;
|
|
378
|
-
const content = fileWriteContent(input);
|
|
379
|
-
return typeof content === 'string' && /<!doctype\s+html\b|<html\b/i.test(content);
|
|
524
|
+
return startDuplicateArtifactCandidate(current, openIndex);
|
|
380
525
|
}
|
|
381
526
|
function feed(chunk) {
|
|
382
527
|
buffer += chunk;
|
|
@@ -410,232 +555,224 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
410
555
|
}
|
|
411
556
|
flushPendingArtifactText();
|
|
412
557
|
}
|
|
413
|
-
function
|
|
414
|
-
if (
|
|
558
|
+
function handleToolUseBlock(block) {
|
|
559
|
+
if (typeof block.id === 'string' && streamedToolUseIds.has(block.id))
|
|
415
560
|
return;
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
model: obj.model ?? null,
|
|
421
|
-
sessionId: obj.session_id ?? null,
|
|
422
|
-
});
|
|
561
|
+
emitToolUse(block.id, block.name, block.input ?? null);
|
|
562
|
+
}
|
|
563
|
+
function handleAssistantContentBlock(block, ctx) {
|
|
564
|
+
if (!isRecord(block))
|
|
423
565
|
return;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
onEvent({ type: 'status', label: typeof obj.status === 'string' ? obj.status : 'working' });
|
|
566
|
+
if (block.type === 'tool_use') {
|
|
567
|
+
handleToolUseBlock(block);
|
|
427
568
|
return;
|
|
428
569
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
570
|
+
if (isUnstreamedTextBlock(block, ctx.textAlreadyStreamed)) {
|
|
571
|
+
emitSafeText(ctx.textMsgId, block.text);
|
|
431
572
|
return;
|
|
432
573
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
// emit tool_use now with the final parsed input. For text blocks, emit
|
|
436
|
-
// the text as a single delta — but only if no streaming deltas already
|
|
437
|
-
// covered it (older Claude Code without --include-partial-messages
|
|
438
|
-
// delivers text only here; newer builds stream it and would duplicate).
|
|
439
|
-
if (obj.type === 'assistant' && isRecord(obj.message) && Array.isArray(obj.message.content)) {
|
|
440
|
-
const explicitMsgId = typeof obj.message.id === 'string' ? obj.message.id : null;
|
|
441
|
-
const textMsgId = explicitMsgId ?? (currentMessageStreamedText ? currentMessageId : null);
|
|
442
|
-
const thinkingMsgId = explicitMsgId ?? (currentMessageStreamedThinking ? currentMessageId : null);
|
|
443
|
-
if (explicitMsgId)
|
|
444
|
-
currentMessageId = explicitMsgId;
|
|
445
|
-
const textAlreadyStreamed = textMsgId ? textStreamed.has(textMsgId) : false;
|
|
446
|
-
const thinkingAlreadyStreamed = thinkingMsgId ? thinkingStreamed.has(thinkingMsgId) : false;
|
|
447
|
-
// Per-turn `stop_reason` is emitted as `turn_end` AFTER the content
|
|
448
|
-
// blocks have been processed (see below). When `--include-partial-
|
|
449
|
-
// messages` is unsupported, tool_use events surface only from the
|
|
450
|
-
// assistant wrapper here — emitting `turn_end` before that loop would
|
|
451
|
-
// let the daemon's stdin-close handler act on the turn before its
|
|
452
|
-
// tool_use blocks were seen, closing stdin mid-tool. Read the
|
|
453
|
-
// stop_reason now, emit after.
|
|
454
|
-
const stopReason = typeof obj.message.stop_reason === 'string'
|
|
455
|
-
? obj.message.stop_reason
|
|
456
|
-
: null;
|
|
457
|
-
for (const block of obj.message.content) {
|
|
458
|
-
if (!isRecord(block))
|
|
459
|
-
continue;
|
|
460
|
-
if (block.type === 'tool_use') {
|
|
461
|
-
if (typeof block.id === 'string' && streamedToolUseIds.has(block.id)) {
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
emitToolUse(block.id, block.name, block.input ?? null);
|
|
465
|
-
}
|
|
466
|
-
else if (!textAlreadyStreamed &&
|
|
467
|
-
block.type === 'text' &&
|
|
468
|
-
typeof block.text === 'string' &&
|
|
469
|
-
block.text.length > 0) {
|
|
470
|
-
emitSafeText(textMsgId, block.text);
|
|
471
|
-
}
|
|
472
|
-
else if (!thinkingAlreadyStreamed &&
|
|
473
|
-
block.type === 'thinking' &&
|
|
474
|
-
typeof block.thinking === 'string' &&
|
|
475
|
-
block.thinking.length > 0) {
|
|
476
|
-
emitSafeText(thinkingMsgId, block.thinking, 'thinking_delta');
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// Surface the turn_end signal now that every tool_use in this
|
|
480
|
-
// assistant message has been emitted, so the daemon's stdin-close
|
|
481
|
-
// handler sees the final `stop_reason` before deciding whether to
|
|
482
|
-
// close stream-json input stdin.
|
|
483
|
-
if (stopReason) {
|
|
484
|
-
emitTurnEnd(stopReason);
|
|
485
|
-
}
|
|
486
|
-
if (typeof obj.error === 'string' && obj.error.trim()) {
|
|
487
|
-
onEvent({
|
|
488
|
-
type: 'error',
|
|
489
|
-
message: assistantText(obj.message.content) || obj.error,
|
|
490
|
-
code: obj.error,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
currentMessageStreamedText = false;
|
|
494
|
-
currentMessageStreamedThinking = false;
|
|
495
|
-
return;
|
|
574
|
+
if (isUnstreamedThinkingBlock(block, ctx.thinkingAlreadyStreamed)) {
|
|
575
|
+
emitSafeText(ctx.thinkingMsgId, block.thinking, 'thinking_delta');
|
|
496
576
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
577
|
+
}
|
|
578
|
+
// Resolves the message id assistant text/thinking blocks should be
|
|
579
|
+
// attributed to (an explicit id on the wrapper, or the currently-tracked
|
|
580
|
+
// streaming id), and whether that channel already streamed via deltas.
|
|
581
|
+
// Also owns the message-id/epoch bookkeeping side effects that must
|
|
582
|
+
// happen exactly once per assistant wrapper.
|
|
583
|
+
function resolveAssistantMessageIds(message) {
|
|
584
|
+
const explicitMsgId = typeof message.id === 'string' ? message.id : null;
|
|
585
|
+
const textMsgId = explicitMsgId ?? (currentMessageStreamedText ? currentMessageId : null);
|
|
586
|
+
const thinkingMsgId = explicitMsgId ?? (currentMessageStreamedThinking ? currentMessageId : null);
|
|
587
|
+
if (explicitMsgId)
|
|
588
|
+
currentMessageId = explicitMsgId;
|
|
589
|
+
// An id-less assistant wrapper is itself a new message boundary: no
|
|
590
|
+
// `message_start` announced it, so this is the only place the epoch can
|
|
591
|
+
// advance before its own `turn_end` is emitted below. See
|
|
592
|
+
// `anonymousMessageEpoch`'s declaration for why message_delta/result must
|
|
593
|
+
// NOT advance it.
|
|
594
|
+
if (currentMessageId === null)
|
|
595
|
+
anonymousMessageEpoch += 1;
|
|
596
|
+
return {
|
|
597
|
+
textMsgId,
|
|
598
|
+
thinkingMsgId,
|
|
599
|
+
textAlreadyStreamed: textMsgId ? textStreamed.has(textMsgId) : false,
|
|
600
|
+
thinkingAlreadyStreamed: thinkingMsgId ? thinkingStreamed.has(thinkingMsgId) : false,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
// `assistant` messages are the "block finished" signal for the current
|
|
604
|
+
// content block. For tool_use blocks whose input finished assembling,
|
|
605
|
+
// emit tool_use now with the final parsed input. For text blocks, emit
|
|
606
|
+
// the text as a single delta — but only if no streaming deltas already
|
|
607
|
+
// covered it (older Claude Code without --include-partial-messages
|
|
608
|
+
// delivers text only here; newer builds stream it and would duplicate).
|
|
609
|
+
function handleAssistantMessage(obj) {
|
|
610
|
+
const message = obj.message;
|
|
611
|
+
if (!isRecord(message) || !Array.isArray(message.content))
|
|
512
612
|
return;
|
|
613
|
+
const { textMsgId, thinkingMsgId, textAlreadyStreamed, thinkingAlreadyStreamed } = resolveAssistantMessageIds(message);
|
|
614
|
+
// Per-turn `stop_reason` is emitted as `turn_end` AFTER the content
|
|
615
|
+
// blocks have been processed (see below). When `--include-partial-
|
|
616
|
+
// messages` is unsupported, tool_use events surface only from the
|
|
617
|
+
// assistant wrapper here — emitting `turn_end` before that loop would
|
|
618
|
+
// let the daemon's stdin-close handler act on the turn before its
|
|
619
|
+
// tool_use blocks were seen, closing stdin mid-tool. Read the
|
|
620
|
+
// stop_reason now, emit after.
|
|
621
|
+
const stopReason = typeof message.stop_reason === 'string' ? message.stop_reason : null;
|
|
622
|
+
for (const block of message.content) {
|
|
623
|
+
handleAssistantContentBlock(block, { textMsgId, thinkingMsgId, textAlreadyStreamed, thinkingAlreadyStreamed });
|
|
513
624
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
625
|
+
// Surface the turn_end signal now that every tool_use in this
|
|
626
|
+
// assistant message has been emitted, so the daemon's stdin-close
|
|
627
|
+
// handler sees the final `stop_reason` before deciding whether to
|
|
628
|
+
// close stream-json input stdin.
|
|
629
|
+
if (stopReason) {
|
|
630
|
+
emitTurnEnd(stopReason);
|
|
631
|
+
}
|
|
632
|
+
emitAssistantErrorIfPresent(obj, message.content, onEvent);
|
|
633
|
+
currentMessageStreamedText = false;
|
|
634
|
+
currentMessageStreamedThinking = false;
|
|
635
|
+
}
|
|
636
|
+
function handleResultMessage(obj) {
|
|
637
|
+
// Explicit annotation: the `&&`/`||` chain's inferred type includes a phantom `false` (TS
|
|
638
|
+
// widens each unresolved `&&` branch to its check's boolean), even though no runtime path
|
|
639
|
+
// through this chain can ever produce the literal `false` — the last `|| null` guarantees a
|
|
640
|
+
// string or null. Pinning the declared type to what's actually true, not casting past it.
|
|
641
|
+
const stopReason = (typeof obj.stop_reason === 'string' && obj.stop_reason) ||
|
|
642
|
+
(typeof obj.terminal_reason === 'string' && obj.terminal_reason) ||
|
|
643
|
+
null;
|
|
644
|
+
// Claude Code 2.1.201 does not always emit a terminal
|
|
645
|
+
// stream_event/message_delta. In that wire shape, the top-level result
|
|
646
|
+
// is the only frame carrying `end_turn`. Surface it as a turn boundary
|
|
647
|
+
// so AgentExecutor can close stream-json stdin and reap the process.
|
|
648
|
+
if (stopReason) {
|
|
649
|
+
emitTurnEnd(stopReason);
|
|
650
|
+
}
|
|
651
|
+
onEvent({
|
|
652
|
+
type: 'usage',
|
|
653
|
+
usage: obj.usage ?? null,
|
|
654
|
+
costUsd: obj.total_cost_usd ?? null,
|
|
655
|
+
durationMs: obj.duration_ms ?? null,
|
|
656
|
+
stopReason,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function handleObject(obj) {
|
|
660
|
+
if (!isRecord(obj))
|
|
536
661
|
return;
|
|
662
|
+
switch (obj.type) {
|
|
663
|
+
case 'system':
|
|
664
|
+
handleSystemMessage(obj, onEvent);
|
|
665
|
+
return;
|
|
666
|
+
case 'stream_event':
|
|
667
|
+
if (isRecord(obj.event))
|
|
668
|
+
handleStreamEvent(obj.event);
|
|
669
|
+
return;
|
|
670
|
+
case 'assistant':
|
|
671
|
+
handleAssistantMessage(obj);
|
|
672
|
+
return;
|
|
673
|
+
case 'user':
|
|
674
|
+
handleUserMessage(obj, onEvent);
|
|
675
|
+
return;
|
|
676
|
+
case 'result':
|
|
677
|
+
handleResultMessage(obj);
|
|
678
|
+
return;
|
|
537
679
|
}
|
|
538
680
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
681
|
+
function handleMessageStart(ev) {
|
|
682
|
+
flushPendingArtifactText();
|
|
683
|
+
// Clean up per-message role-marker guard from the previous message.
|
|
684
|
+
if (currentMessageId)
|
|
685
|
+
roleGuards.delete(currentMessageId);
|
|
686
|
+
currentMessageId = isRecord(ev.message) && typeof ev.message.id === 'string' ? ev.message.id : null;
|
|
687
|
+
// New message boundary — see `anonymousMessageEpoch`'s declaration.
|
|
688
|
+
if (currentMessageId === null)
|
|
689
|
+
anonymousMessageEpoch += 1;
|
|
690
|
+
currentMessageStreamedText = false;
|
|
691
|
+
currentMessageStreamedThinking = false;
|
|
692
|
+
if (typeof ev.ttft_ms === 'number') {
|
|
693
|
+
onEvent({ type: 'status', label: 'streaming', ttftMs: ev.ttft_ms });
|
|
545
694
|
}
|
|
546
|
-
return parts.join('\n').trim();
|
|
547
695
|
}
|
|
548
|
-
function
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
696
|
+
function handleContentBlockStart(index, block) {
|
|
697
|
+
const key = blockKey(index);
|
|
698
|
+
blocks.set(key, {
|
|
699
|
+
type: block.type,
|
|
700
|
+
name: block.name,
|
|
701
|
+
id: block.id,
|
|
702
|
+
input: '',
|
|
703
|
+
inputValue: 'input' in block ? block.input : undefined,
|
|
704
|
+
});
|
|
705
|
+
if (block.type === 'thinking') {
|
|
706
|
+
onEvent({ type: 'thinking_start' });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function handleContentBlockDelta(index, delta) {
|
|
710
|
+
const state = blocks.get(blockKey(index));
|
|
711
|
+
if (delta.type === 'text_delta' && typeof delta.text === 'string') {
|
|
552
712
|
if (currentMessageId)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
currentMessageStreamedThinking = false;
|
|
557
|
-
if (typeof ev.ttft_ms === 'number') {
|
|
558
|
-
onEvent({ type: 'status', label: 'streaming', ttftMs: ev.ttft_ms });
|
|
559
|
-
}
|
|
713
|
+
textStreamed.add(currentMessageId);
|
|
714
|
+
currentMessageStreamedText = true;
|
|
715
|
+
emitSafeText(currentMessageId, delta.text);
|
|
560
716
|
return;
|
|
561
717
|
}
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
name: block.name,
|
|
568
|
-
id: block.id,
|
|
569
|
-
input: '',
|
|
570
|
-
inputValue: 'input' in block ? block.input : undefined,
|
|
571
|
-
});
|
|
572
|
-
if (block.type === 'thinking') {
|
|
573
|
-
onEvent({ type: 'thinking_start' });
|
|
574
|
-
}
|
|
718
|
+
if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') {
|
|
719
|
+
if (currentMessageId)
|
|
720
|
+
thinkingStreamed.add(currentMessageId);
|
|
721
|
+
currentMessageStreamedThinking = true;
|
|
722
|
+
emitSafeText(currentMessageId, delta.thinking, 'thinking_delta');
|
|
575
723
|
return;
|
|
576
724
|
}
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
const delta = ev.delta;
|
|
580
|
-
if (delta.type === 'text_delta' && typeof delta.text === 'string') {
|
|
581
|
-
if (currentMessageId)
|
|
582
|
-
textStreamed.add(currentMessageId);
|
|
583
|
-
currentMessageStreamedText = true;
|
|
584
|
-
emitSafeText(currentMessageId, delta.text);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') {
|
|
588
|
-
if (currentMessageId)
|
|
589
|
-
thinkingStreamed.add(currentMessageId);
|
|
590
|
-
currentMessageStreamedThinking = true;
|
|
591
|
-
emitSafeText(currentMessageId, delta.thinking, 'thinking_delta');
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
595
|
-
if (state && state.type === 'tool_use') {
|
|
596
|
-
state.input += delta.partial_json;
|
|
597
|
-
if (typeof state.id === 'string' && typeof state.name === 'string') {
|
|
598
|
-
onEvent({
|
|
599
|
-
type: 'tool_input_delta',
|
|
600
|
-
id: state.id,
|
|
601
|
-
name: state.name,
|
|
602
|
-
delta: delta.partial_json,
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
725
|
+
if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') {
|
|
726
|
+
applyInputJsonDelta(state, delta.partial_json, onEvent);
|
|
608
727
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
// Fall through to the final assistant wrapper's input if the
|
|
619
|
-
// streamed JSON is malformed or incomplete.
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
else if (state &&
|
|
623
|
-
state.type === 'tool_use' &&
|
|
624
|
-
typeof state.id === 'string' &&
|
|
625
|
-
state.inputValue !== undefined) {
|
|
626
|
-
emitToolUse(state.id, state.name, state.inputValue);
|
|
728
|
+
}
|
|
729
|
+
function handleContentBlockStop(ev) {
|
|
730
|
+
const key = blockKey(ev.index);
|
|
731
|
+
const state = blocks.get(key);
|
|
732
|
+
if (state && state.type === 'tool_use' && typeof state.id === 'string' && state.input.trim()) {
|
|
733
|
+
try {
|
|
734
|
+
emitToolUse(state.id, state.name, JSON.parse(state.input));
|
|
627
735
|
streamedToolUseIds.add(state.id);
|
|
628
736
|
}
|
|
629
|
-
|
|
630
|
-
|
|
737
|
+
catch {
|
|
738
|
+
// Fall through to the final assistant wrapper's input if the
|
|
739
|
+
// streamed JSON is malformed or incomplete.
|
|
740
|
+
}
|
|
631
741
|
}
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
742
|
+
else if (state &&
|
|
743
|
+
state.type === 'tool_use' &&
|
|
744
|
+
typeof state.id === 'string' &&
|
|
745
|
+
state.inputValue !== undefined) {
|
|
746
|
+
emitToolUse(state.id, state.name, state.inputValue);
|
|
747
|
+
streamedToolUseIds.add(state.id);
|
|
748
|
+
}
|
|
749
|
+
blocks.delete(key);
|
|
750
|
+
}
|
|
751
|
+
function handleMessageDeltaEvent(delta) {
|
|
752
|
+
const stopReason = typeof delta.stop_reason === 'string' ? delta.stop_reason : null;
|
|
753
|
+
if (stopReason)
|
|
754
|
+
emitTurnEnd(stopReason);
|
|
755
|
+
}
|
|
756
|
+
function handleStreamEvent(ev) {
|
|
757
|
+
switch (ev.type) {
|
|
758
|
+
case 'message_start':
|
|
759
|
+
handleMessageStart(ev);
|
|
760
|
+
return;
|
|
761
|
+
case 'content_block_start':
|
|
762
|
+
if (isRecord(ev.content_block))
|
|
763
|
+
handleContentBlockStart(ev.index, ev.content_block);
|
|
764
|
+
return;
|
|
765
|
+
case 'content_block_delta':
|
|
766
|
+
if (isRecord(ev.delta))
|
|
767
|
+
handleContentBlockDelta(ev.index, ev.delta);
|
|
768
|
+
return;
|
|
769
|
+
case 'content_block_stop':
|
|
770
|
+
handleContentBlockStop(ev);
|
|
771
|
+
return;
|
|
772
|
+
case 'message_delta':
|
|
773
|
+
if (isRecord(ev.delta))
|
|
774
|
+
handleMessageDeltaEvent(ev.delta);
|
|
775
|
+
return;
|
|
639
776
|
}
|
|
640
777
|
}
|
|
641
778
|
function flushPendingArtifactText() {
|
|
@@ -653,14 +790,4 @@ export function createClaudeStreamHandler(onEvent, options = {}) {
|
|
|
653
790
|
}
|
|
654
791
|
return { feed, flush };
|
|
655
792
|
}
|
|
656
|
-
function stringifyToolResult(content) {
|
|
657
|
-
if (typeof content === 'string')
|
|
658
|
-
return content;
|
|
659
|
-
if (Array.isArray(content)) {
|
|
660
|
-
return content
|
|
661
|
-
.map((c) => (isRecord(c) && c.type === 'text' ? String(c.text) : JSON.stringify(c)))
|
|
662
|
-
.join('\n');
|
|
663
|
-
}
|
|
664
|
-
return JSON.stringify(content);
|
|
665
|
-
}
|
|
666
793
|
//# sourceMappingURL=claude-stream.js.map
|