@saccolabs/pi-claude-cli 0.4.0 → 0.4.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/package.json +1 -1
- package/src/control-handler.ts +14 -7
- package/src/event-bridge.ts +188 -24
- package/src/provider.ts +19 -1
- package/src/types.ts +48 -2
package/package.json
CHANGED
package/src/control-handler.ts
CHANGED
|
@@ -17,15 +17,22 @@ export const TOOL_EXECUTION_DENIED_MESSAGE =
|
|
|
17
17
|
/** Prefix for MCP (Model Context Protocol) tool names. */
|
|
18
18
|
export const MCP_PREFIX = "mcp__";
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Claude Code 2.x control_response wire shape: `request_id` lives INSIDE
|
|
22
|
+
* `response`, and allow decisions carry `updatedInput` (the tool input,
|
|
23
|
+
* passed through unmodified). The 1.x shape (`request_id` at the top level)
|
|
24
|
+
* is silently ignored by 2.1.x — the CLI keeps waiting for an answer and
|
|
25
|
+
* the episode stalls until the inactivity timer kills it, which surfaced
|
|
26
|
+
* as truncated multi-cycle turns (#3). Verified against claude 2.1.237.
|
|
27
|
+
*/
|
|
20
28
|
interface ControlResponse {
|
|
21
29
|
type: "control_response";
|
|
22
|
-
request_id: string;
|
|
23
30
|
response: {
|
|
24
31
|
subtype: "success";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
request_id: string;
|
|
33
|
+
response:
|
|
34
|
+
| { behavior: "allow"; updatedInput: Record<string, unknown> }
|
|
35
|
+
| { behavior: "deny"; message: string };
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
@@ -54,12 +61,12 @@ export function handleControlRequest(
|
|
|
54
61
|
|
|
55
62
|
const response: ControlResponse = {
|
|
56
63
|
type: "control_response",
|
|
57
|
-
request_id: msg.request_id,
|
|
58
64
|
response: {
|
|
59
65
|
subtype: "success",
|
|
66
|
+
request_id: msg.request_id,
|
|
60
67
|
response: isCustomTool
|
|
61
68
|
? { behavior: "deny", message: TOOL_EXECUTION_DENIED_MESSAGE }
|
|
62
|
-
: { behavior: "allow" },
|
|
69
|
+
: { behavior: "allow", updatedInput: msg.request?.input ?? {} },
|
|
63
70
|
},
|
|
64
71
|
};
|
|
65
72
|
|
package/src/event-bridge.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ClaudeApiEvent,
|
|
3
|
+
ClaudeAssistantEnvelope,
|
|
4
|
+
ClaudeResultMessage,
|
|
5
|
+
ClaudeUsage,
|
|
6
|
+
TrackedContentBlock,
|
|
7
|
+
} from "./types";
|
|
2
8
|
import { calculateCost } from "@earendil-works/pi-ai";
|
|
3
9
|
import type {
|
|
4
10
|
AssistantMessage,
|
|
@@ -21,6 +27,7 @@ import {
|
|
|
21
27
|
interface TrackedToolBlock {
|
|
22
28
|
type: "tool_use";
|
|
23
29
|
index: number;
|
|
30
|
+
cycle: number;
|
|
24
31
|
id: string;
|
|
25
32
|
name: string; // Already mapped to pi name
|
|
26
33
|
claudeName: string; // Original Claude name for arg translation
|
|
@@ -39,6 +46,19 @@ type TrackedBlock = TrackedContentBlock | TrackedToolBlock;
|
|
|
39
46
|
*/
|
|
40
47
|
export interface EventBridge {
|
|
41
48
|
handleEvent(event: ClaudeApiEvent): void;
|
|
49
|
+
/**
|
|
50
|
+
* Complete-message envelopes (one per finished content block). Used for
|
|
51
|
+
* CLI-side tool visibility: tools pi cannot execute (WebSearch, user MCP
|
|
52
|
+
* servers, ToolSearch, …) run inside the CLI between cycles and would
|
|
53
|
+
* otherwise be invisible — each becomes a one-line marker text block.
|
|
54
|
+
*/
|
|
55
|
+
handleAssistantEnvelope(envelope: ClaudeAssistantEnvelope): void;
|
|
56
|
+
/**
|
|
57
|
+
* The final `result` envelope: authoritative cumulative usage for the
|
|
58
|
+
* whole episode, plus a safety net that appends the final answer text if
|
|
59
|
+
* any stream pathology kept it out of the bridged content.
|
|
60
|
+
*/
|
|
61
|
+
applyResult(result: ClaudeResultMessage): void;
|
|
42
62
|
getOutput(): AssistantMessage;
|
|
43
63
|
}
|
|
44
64
|
|
|
@@ -97,6 +117,71 @@ export function createEventBridge(
|
|
|
97
117
|
|
|
98
118
|
let started = false;
|
|
99
119
|
|
|
120
|
+
// One subprocess run is a full agentic EPISODE: N API calls ("cycles")
|
|
121
|
+
// with CLI-side tool executions between them. SSE content_block indexes
|
|
122
|
+
// reset every cycle, so blocks are matched per-cycle; usage is summed
|
|
123
|
+
// across cycles (each message_delta carries that cycle's final numbers).
|
|
124
|
+
let cycle = -1;
|
|
125
|
+
const cumulativeUsage: Required<ClaudeUsage> = {
|
|
126
|
+
input_tokens: 0,
|
|
127
|
+
output_tokens: 0,
|
|
128
|
+
cache_read_input_tokens: 0,
|
|
129
|
+
cache_creation_input_tokens: 0,
|
|
130
|
+
};
|
|
131
|
+
let cycleUsage: ClaudeUsage = {};
|
|
132
|
+
/** Tool ids already surfaced as markers (envelope arrives once per block). */
|
|
133
|
+
const markedToolIds = new Set<string>();
|
|
134
|
+
|
|
135
|
+
function recomputeUsage(): void {
|
|
136
|
+
output.usage.input =
|
|
137
|
+
cumulativeUsage.input_tokens + (cycleUsage.input_tokens ?? 0);
|
|
138
|
+
output.usage.output =
|
|
139
|
+
cumulativeUsage.output_tokens + (cycleUsage.output_tokens ?? 0);
|
|
140
|
+
output.usage.cacheRead =
|
|
141
|
+
cumulativeUsage.cache_read_input_tokens +
|
|
142
|
+
(cycleUsage.cache_read_input_tokens ?? 0);
|
|
143
|
+
output.usage.cacheWrite =
|
|
144
|
+
cumulativeUsage.cache_creation_input_tokens +
|
|
145
|
+
(cycleUsage.cache_creation_input_tokens ?? 0);
|
|
146
|
+
output.usage.totalTokens =
|
|
147
|
+
output.usage.input +
|
|
148
|
+
output.usage.output +
|
|
149
|
+
output.usage.cacheRead +
|
|
150
|
+
output.usage.cacheWrite;
|
|
151
|
+
calculateCost(model, output.usage);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Append a complete text block through proper pi stream events.
|
|
156
|
+
*
|
|
157
|
+
* `blocks` and `output.content` are parallel arrays (SSE handlers index
|
|
158
|
+
* into both with the same idx), so the marker must occupy a slot in both;
|
|
159
|
+
* cycle -1 / index -1 can never match a real SSE event.
|
|
160
|
+
*/
|
|
161
|
+
function appendTextBlock(text: string): void {
|
|
162
|
+
if (!started) {
|
|
163
|
+
stream.push({ type: "start", partial: output });
|
|
164
|
+
started = true;
|
|
165
|
+
}
|
|
166
|
+
blocks.push({ type: "text", text, index: -1, cycle: -1 });
|
|
167
|
+
output.content.push({ type: "text" as const, text: "" });
|
|
168
|
+
const contentIndex = output.content.length - 1;
|
|
169
|
+
stream.push({ type: "text_start", contentIndex, partial: output });
|
|
170
|
+
(output.content[contentIndex] as TextContent).text = text;
|
|
171
|
+
stream.push({
|
|
172
|
+
type: "text_delta",
|
|
173
|
+
contentIndex,
|
|
174
|
+
delta: text,
|
|
175
|
+
partial: output,
|
|
176
|
+
});
|
|
177
|
+
stream.push({
|
|
178
|
+
type: "text_end",
|
|
179
|
+
contentIndex,
|
|
180
|
+
content: text,
|
|
181
|
+
partial: output,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
100
185
|
function handleEvent(event: ClaudeApiEvent): void {
|
|
101
186
|
// Emit start event on first message — tells pi to begin incremental rendering
|
|
102
187
|
if (!started) {
|
|
@@ -106,6 +191,15 @@ export function createEventBridge(
|
|
|
106
191
|
|
|
107
192
|
switch (event.type) {
|
|
108
193
|
case "message_start":
|
|
194
|
+
// New cycle: bank the finished cycle's usage before resetting.
|
|
195
|
+
cumulativeUsage.input_tokens += cycleUsage.input_tokens ?? 0;
|
|
196
|
+
cumulativeUsage.output_tokens += cycleUsage.output_tokens ?? 0;
|
|
197
|
+
cumulativeUsage.cache_read_input_tokens +=
|
|
198
|
+
cycleUsage.cache_read_input_tokens ?? 0;
|
|
199
|
+
cumulativeUsage.cache_creation_input_tokens +=
|
|
200
|
+
cycleUsage.cache_creation_input_tokens ?? 0;
|
|
201
|
+
cycleUsage = {};
|
|
202
|
+
cycle++;
|
|
109
203
|
handleMessageStart(event);
|
|
110
204
|
break;
|
|
111
205
|
case "content_block_start":
|
|
@@ -130,16 +224,8 @@ export function createEventBridge(
|
|
|
130
224
|
function handleMessageStart(event: ClaudeApiEvent): void {
|
|
131
225
|
const usage = event.message?.usage;
|
|
132
226
|
if (usage) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
output.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
136
|
-
output.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
137
|
-
output.usage.totalTokens =
|
|
138
|
-
output.usage.input +
|
|
139
|
-
output.usage.output +
|
|
140
|
-
output.usage.cacheRead +
|
|
141
|
-
output.usage.cacheWrite;
|
|
142
|
-
calculateCost(model, output.usage);
|
|
227
|
+
cycleUsage = { ...usage };
|
|
228
|
+
recomputeUsage();
|
|
143
229
|
}
|
|
144
230
|
}
|
|
145
231
|
|
|
@@ -151,6 +237,7 @@ export function createEventBridge(
|
|
|
151
237
|
type: "text",
|
|
152
238
|
text: "",
|
|
153
239
|
index: event.index ?? 0,
|
|
240
|
+
cycle,
|
|
154
241
|
};
|
|
155
242
|
blocks.push(block);
|
|
156
243
|
output.content.push({ type: "text" as const, text: "" });
|
|
@@ -165,6 +252,7 @@ export function createEventBridge(
|
|
|
165
252
|
type: "thinking",
|
|
166
253
|
text: "",
|
|
167
254
|
index: event.index ?? 0,
|
|
255
|
+
cycle,
|
|
168
256
|
};
|
|
169
257
|
blocks.push(block);
|
|
170
258
|
output.content.push({
|
|
@@ -193,6 +281,7 @@ export function createEventBridge(
|
|
|
193
281
|
const block: TrackedToolBlock = {
|
|
194
282
|
type: "tool_use",
|
|
195
283
|
index: event.index ?? 0,
|
|
284
|
+
cycle,
|
|
196
285
|
id,
|
|
197
286
|
name: piName,
|
|
198
287
|
claudeName,
|
|
@@ -220,7 +309,9 @@ export function createEventBridge(
|
|
|
220
309
|
const deltaType = event.delta?.type;
|
|
221
310
|
|
|
222
311
|
if (deltaType === "text_delta" && event.delta!.text != null) {
|
|
223
|
-
const idx = blocks.findIndex(
|
|
312
|
+
const idx = blocks.findIndex(
|
|
313
|
+
(b) => b.cycle === cycle && b.index === event.index,
|
|
314
|
+
);
|
|
224
315
|
if (idx === -1) return;
|
|
225
316
|
|
|
226
317
|
const block = blocks[idx];
|
|
@@ -240,7 +331,9 @@ export function createEventBridge(
|
|
|
240
331
|
deltaType === "thinking_delta" &&
|
|
241
332
|
event.delta!.thinking != null
|
|
242
333
|
) {
|
|
243
|
-
const idx = blocks.findIndex(
|
|
334
|
+
const idx = blocks.findIndex(
|
|
335
|
+
(b) => b.cycle === cycle && b.index === event.index,
|
|
336
|
+
);
|
|
244
337
|
if (idx === -1) return;
|
|
245
338
|
|
|
246
339
|
const block = blocks[idx];
|
|
@@ -260,7 +353,9 @@ export function createEventBridge(
|
|
|
260
353
|
deltaType === "input_json_delta" &&
|
|
261
354
|
event.delta!.partial_json != null
|
|
262
355
|
) {
|
|
263
|
-
const idx = blocks.findIndex(
|
|
356
|
+
const idx = blocks.findIndex(
|
|
357
|
+
(b) => b.cycle === cycle && b.index === event.index,
|
|
358
|
+
);
|
|
264
359
|
if (idx === -1) return;
|
|
265
360
|
|
|
266
361
|
const block = blocks[idx];
|
|
@@ -287,7 +382,9 @@ export function createEventBridge(
|
|
|
287
382
|
event.delta!.signature != null
|
|
288
383
|
) {
|
|
289
384
|
// Accumulate signature on the thinking block
|
|
290
|
-
const idx = blocks.findIndex(
|
|
385
|
+
const idx = blocks.findIndex(
|
|
386
|
+
(b) => b.cycle === cycle && b.index === event.index,
|
|
387
|
+
);
|
|
291
388
|
if (idx === -1) return;
|
|
292
389
|
|
|
293
390
|
const block = blocks[idx];
|
|
@@ -300,7 +397,9 @@ export function createEventBridge(
|
|
|
300
397
|
}
|
|
301
398
|
|
|
302
399
|
function handleContentBlockStop(event: ClaudeApiEvent): void {
|
|
303
|
-
const idx = blocks.findIndex(
|
|
400
|
+
const idx = blocks.findIndex(
|
|
401
|
+
(b) => b.cycle === cycle && b.index === event.index,
|
|
402
|
+
);
|
|
304
403
|
if (idx === -1) return;
|
|
305
404
|
|
|
306
405
|
const block = blocks[idx];
|
|
@@ -356,20 +455,24 @@ export function createEventBridge(
|
|
|
356
455
|
|
|
357
456
|
function handleMessageDelta(event: ClaudeApiEvent): void {
|
|
358
457
|
if (event.delta?.stop_reason) {
|
|
458
|
+
// The LAST cycle's stop reason is the episode's stop reason.
|
|
359
459
|
output.stopReason = mapStopReason(event.delta.stop_reason);
|
|
360
460
|
}
|
|
361
461
|
|
|
362
462
|
const usage = event.usage;
|
|
363
463
|
if (usage) {
|
|
364
|
-
|
|
464
|
+
// message_delta carries the cycle's final numbers — overwrite within
|
|
465
|
+
// the cycle, never across cycles (those are banked at message_start).
|
|
466
|
+
if (usage.input_tokens != null)
|
|
467
|
+
cycleUsage.input_tokens = usage.input_tokens;
|
|
365
468
|
if (usage.output_tokens != null)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
469
|
+
cycleUsage.output_tokens = usage.output_tokens;
|
|
470
|
+
if (usage.cache_read_input_tokens != null)
|
|
471
|
+
cycleUsage.cache_read_input_tokens = usage.cache_read_input_tokens;
|
|
472
|
+
if (usage.cache_creation_input_tokens != null)
|
|
473
|
+
cycleUsage.cache_creation_input_tokens =
|
|
474
|
+
usage.cache_creation_input_tokens;
|
|
475
|
+
recomputeUsage();
|
|
373
476
|
}
|
|
374
477
|
}
|
|
375
478
|
|
|
@@ -378,8 +481,69 @@ export function createEventBridge(
|
|
|
378
481
|
// Pushing done here (synchronously) prevents pi from executing tools.
|
|
379
482
|
}
|
|
380
483
|
|
|
484
|
+
function handleAssistantEnvelope(envelope: ClaudeAssistantEnvelope): void {
|
|
485
|
+
// Sub-agent envelopes are the CLI's internal business.
|
|
486
|
+
if (envelope.parent_tool_use_id) return;
|
|
487
|
+
for (const block of envelope.message?.content ?? []) {
|
|
488
|
+
if (block.type !== "tool_use" || !block.name || !block.id) continue;
|
|
489
|
+
// Pi-known tools already streamed through the SSE path as real pi
|
|
490
|
+
// tool calls — markers are only for tools the CLI executes itself.
|
|
491
|
+
if (isPiKnownClaudeTool(block.name)) continue;
|
|
492
|
+
if (markedToolIds.has(block.id)) continue;
|
|
493
|
+
markedToolIds.add(block.id);
|
|
494
|
+
|
|
495
|
+
let argsPreview = "";
|
|
496
|
+
try {
|
|
497
|
+
const json = JSON.stringify(block.input ?? {});
|
|
498
|
+
argsPreview =
|
|
499
|
+
json === "{}"
|
|
500
|
+
? ""
|
|
501
|
+
: ` ${json.slice(0, 120)}${json.length > 120 ? "…" : ""}`;
|
|
502
|
+
} catch {
|
|
503
|
+
/* unserializable input — marker still names the tool */
|
|
504
|
+
}
|
|
505
|
+
appendTextBlock(`[Claude Code · ${block.name}${argsPreview}]`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function applyResult(result: ClaudeResultMessage): void {
|
|
510
|
+
// Authoritative cumulative usage for the whole episode (verified to
|
|
511
|
+
// equal the per-cycle sums on captured streams; trusted over them).
|
|
512
|
+
const usage = result.usage;
|
|
513
|
+
if (usage) {
|
|
514
|
+
cumulativeUsage.input_tokens =
|
|
515
|
+
usage.input_tokens ?? cumulativeUsage.input_tokens;
|
|
516
|
+
cumulativeUsage.output_tokens =
|
|
517
|
+
usage.output_tokens ?? cumulativeUsage.output_tokens;
|
|
518
|
+
cumulativeUsage.cache_read_input_tokens =
|
|
519
|
+
usage.cache_read_input_tokens ??
|
|
520
|
+
cumulativeUsage.cache_read_input_tokens;
|
|
521
|
+
cumulativeUsage.cache_creation_input_tokens =
|
|
522
|
+
usage.cache_creation_input_tokens ??
|
|
523
|
+
cumulativeUsage.cache_creation_input_tokens;
|
|
524
|
+
cycleUsage = {};
|
|
525
|
+
recomputeUsage();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Safety net: whatever ended the SSE stream early, the result envelope
|
|
529
|
+
// carries the episode's final answer — never let it be lost.
|
|
530
|
+
const finalText = (result.result ?? "").trim();
|
|
531
|
+
if (finalText) {
|
|
532
|
+
const streamedText = output.content
|
|
533
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
534
|
+
.map((c) => c.text)
|
|
535
|
+
.join("\n");
|
|
536
|
+
const tail = finalText.slice(-Math.min(finalText.length, 60));
|
|
537
|
+
if (!streamedText.includes(tail)) {
|
|
538
|
+
appendTextBlock(finalText);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
381
543
|
return {
|
|
382
544
|
handleEvent,
|
|
545
|
+
handleAssistantEnvelope,
|
|
546
|
+
applyResult,
|
|
383
547
|
getOutput: () => output,
|
|
384
548
|
};
|
|
385
549
|
}
|
package/src/provider.ts
CHANGED
|
@@ -41,7 +41,15 @@ import { handleControlRequest } from "./control-handler.js";
|
|
|
41
41
|
import { mapThinkingEffort } from "./thinking-config.js";
|
|
42
42
|
import { isPiKnownClaudeTool } from "./tool-mapping.js";
|
|
43
43
|
/** Inactivity timeout: kill subprocess if no stdout for 180 seconds (3 minutes). */
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Inactivity timeout. CLI-side tool executions (web search, user MCP
|
|
46
|
+
* servers, sub-agents) can be silent on stdout for minutes, so the default
|
|
47
|
+
* is generous and overridable via PI_CLAUDE_CLI_TIMEOUT_MS.
|
|
48
|
+
*/
|
|
49
|
+
const INACTIVITY_TIMEOUT_MS =
|
|
50
|
+
Number(process.env.PI_CLAUDE_CLI_TIMEOUT_MS) > 0
|
|
51
|
+
? Number(process.env.PI_CLAUDE_CLI_TIMEOUT_MS)
|
|
52
|
+
: 300_000;
|
|
45
53
|
|
|
46
54
|
/** Extended stream options: pi's SimpleStreamOptions plus optional cwd and mcpConfigPath */
|
|
47
55
|
type StreamViaCLiOptions = SimpleStreamOptions & {
|
|
@@ -275,6 +283,12 @@ export function streamViaCli(
|
|
|
275
283
|
rl.close();
|
|
276
284
|
return; // Don't process further -- done event already pushed by event bridge
|
|
277
285
|
}
|
|
286
|
+
} else if (msg.type === "assistant") {
|
|
287
|
+
// Complete-block envelopes: marker text for CLI-side tools that
|
|
288
|
+
// would otherwise be invisible between cycles.
|
|
289
|
+
bridge.handleAssistantEnvelope(msg as any);
|
|
290
|
+
} else if (msg.type === "user") {
|
|
291
|
+
// Tool results the CLI feeds back between cycles — internal.
|
|
278
292
|
} else if (msg.type === "control_request") {
|
|
279
293
|
handleControlRequest(msg, proc!.stdin!);
|
|
280
294
|
} else if (msg.type === "result") {
|
|
@@ -296,6 +310,10 @@ export function streamViaCli(
|
|
|
296
310
|
: `Claude CLI returned ${r.subtype ?? "non-success result"}`);
|
|
297
311
|
endStreamWithError(errMsg);
|
|
298
312
|
}
|
|
313
|
+
if (!isError) {
|
|
314
|
+
// Authoritative episode usage + final-answer safety net.
|
|
315
|
+
bridge.applyResult(r);
|
|
316
|
+
}
|
|
299
317
|
// For both success and error: clean up the subprocess
|
|
300
318
|
clearTimeout(inactivityTimer);
|
|
301
319
|
cleanupProcess(proc!);
|
package/src/types.ts
CHANGED
|
@@ -13,6 +13,49 @@ export interface ClaudeResultMessage {
|
|
|
13
13
|
result?: string;
|
|
14
14
|
error?: string;
|
|
15
15
|
session_id?: string;
|
|
16
|
+
/** Cumulative usage across every cycle of the episode (authoritative). */
|
|
17
|
+
usage?: ClaudeUsage;
|
|
18
|
+
num_turns?: number;
|
|
19
|
+
total_cost_usd?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Complete-message envelope the CLI emits once per finished content block
|
|
24
|
+
* (in addition to the SSE stream_events). `parent_tool_use_id` is null for
|
|
25
|
+
* top-level content and set for sub-agent activity.
|
|
26
|
+
*/
|
|
27
|
+
export interface ClaudeAssistantEnvelope {
|
|
28
|
+
type: "assistant";
|
|
29
|
+
parent_tool_use_id?: string | null;
|
|
30
|
+
message: {
|
|
31
|
+
id?: string;
|
|
32
|
+
role?: string;
|
|
33
|
+
stop_reason?: string | null;
|
|
34
|
+
content?: Array<{
|
|
35
|
+
type: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
thinking?: string;
|
|
38
|
+
id?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
input?: Record<string, unknown>;
|
|
41
|
+
}>;
|
|
42
|
+
usage?: ClaudeUsage;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Tool results the CLI feeds back between cycles (top-level only). */
|
|
47
|
+
export interface ClaudeUserEnvelope {
|
|
48
|
+
type: "user";
|
|
49
|
+
parent_tool_use_id?: string | null;
|
|
50
|
+
message: {
|
|
51
|
+
role?: string;
|
|
52
|
+
content?: Array<{
|
|
53
|
+
type: string;
|
|
54
|
+
tool_use_id?: string;
|
|
55
|
+
content?: unknown;
|
|
56
|
+
is_error?: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
16
59
|
}
|
|
17
60
|
|
|
18
61
|
export interface ClaudeSystemMessage {
|
|
@@ -36,7 +79,9 @@ export type NdjsonMessage =
|
|
|
36
79
|
| ClaudeStreamEventMessage
|
|
37
80
|
| ClaudeResultMessage
|
|
38
81
|
| ClaudeSystemMessage
|
|
39
|
-
| ClaudeControlRequest
|
|
82
|
+
| ClaudeControlRequest
|
|
83
|
+
| ClaudeAssistantEnvelope
|
|
84
|
+
| ClaudeUserEnvelope;
|
|
40
85
|
|
|
41
86
|
// Claude API event types (inside stream_event wrapper)
|
|
42
87
|
|
|
@@ -81,5 +126,6 @@ export interface ClaudeUsage {
|
|
|
81
126
|
export interface TrackedContentBlock {
|
|
82
127
|
type: "text" | "thinking";
|
|
83
128
|
text: string;
|
|
84
|
-
index: number; // Claude's content_block index
|
|
129
|
+
index: number; // Claude's content_block index (resets each cycle)
|
|
130
|
+
cycle: number; // Which API call of the episode this block belongs to
|
|
85
131
|
}
|