@ryan_nookpi/pi-extension-subagent 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/above-widget.ts +20 -0
- package/agents.ts +233 -0
- package/claude-args.ts +103 -0
- package/claude-sdk-runner.ts +223 -0
- package/claude-sdk-stream-adapter.ts +49 -0
- package/claude-sidecar-writer.ts +107 -0
- package/claude-stream-parser.ts +400 -0
- package/cli.ts +549 -0
- package/commands.ts +2295 -0
- package/config.ts +119 -0
- package/constants.ts +115 -0
- package/context-limits.ts +132 -0
- package/display-task.ts +98 -0
- package/escalation.ts +159 -0
- package/format.ts +188 -0
- package/group-pending.ts +90 -0
- package/index.ts +200 -0
- package/invocation-queue.ts +29 -0
- package/live-preview.ts +39 -0
- package/package.json +95 -0
- package/persisted-session.ts +185 -0
- package/replay.ts +461 -0
- package/retry.ts +164 -0
- package/run-utils.ts +203 -0
- package/runner.ts +979 -0
- package/session.ts +319 -0
- package/store.ts +143 -0
- package/tool-execute.ts +1932 -0
- package/tool-render.ts +145 -0
- package/types.ts +193 -0
- package/utils/agent-utils.ts +315 -0
- package/utils/format-utils.ts +255 -0
- package/utils/short-label.ts +78 -0
- package/utils/string-utils.ts +229 -0
- package/utils/time-utils.ts +105 -0
- package/widget.ts +294 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: Claude stream events are dynamic runtime data. */
|
|
2
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
3
|
+
import { extractActivityPreviewFromTextDelta, extractThoughtText } from "./live-preview.js";
|
|
4
|
+
import type { SingleResult } from "./types.js";
|
|
5
|
+
|
|
6
|
+
type ClaudeUsageTotals = {
|
|
7
|
+
input: number;
|
|
8
|
+
output: number;
|
|
9
|
+
cacheRead: number;
|
|
10
|
+
cacheWrite: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface ClaudeStreamState {
|
|
14
|
+
sessionId: string | undefined;
|
|
15
|
+
model: string | undefined;
|
|
16
|
+
messages: Message[];
|
|
17
|
+
liveText: string | undefined;
|
|
18
|
+
liveThinking: string | undefined;
|
|
19
|
+
liveToolCalls: number;
|
|
20
|
+
thoughtText: string | undefined;
|
|
21
|
+
stopReason: string | undefined;
|
|
22
|
+
errorMessage: string | undefined;
|
|
23
|
+
usage: {
|
|
24
|
+
input: number;
|
|
25
|
+
output: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
cost: number;
|
|
29
|
+
contextTokens: number;
|
|
30
|
+
turns: number;
|
|
31
|
+
};
|
|
32
|
+
completedUsage?: ClaudeUsageTotals;
|
|
33
|
+
currentMessageUsage?: ClaudeUsageTotals;
|
|
34
|
+
lastTurnContextTokens?: number;
|
|
35
|
+
resultReceived: boolean;
|
|
36
|
+
resultEvent: any | undefined;
|
|
37
|
+
isError: boolean;
|
|
38
|
+
permissionDenials: any[];
|
|
39
|
+
liveActivityPreview: string | undefined;
|
|
40
|
+
currentToolName: string | undefined;
|
|
41
|
+
currentToolInput: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createStreamState(): ClaudeStreamState {
|
|
45
|
+
return {
|
|
46
|
+
sessionId: undefined,
|
|
47
|
+
model: undefined,
|
|
48
|
+
messages: [],
|
|
49
|
+
liveText: undefined,
|
|
50
|
+
liveThinking: undefined,
|
|
51
|
+
liveToolCalls: 0,
|
|
52
|
+
thoughtText: undefined,
|
|
53
|
+
stopReason: undefined,
|
|
54
|
+
errorMessage: undefined,
|
|
55
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
56
|
+
completedUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
57
|
+
currentMessageUsage: undefined,
|
|
58
|
+
lastTurnContextTokens: 0,
|
|
59
|
+
resultReceived: false,
|
|
60
|
+
resultEvent: undefined,
|
|
61
|
+
isError: false,
|
|
62
|
+
permissionDenials: [],
|
|
63
|
+
liveActivityPreview: undefined,
|
|
64
|
+
currentToolName: undefined,
|
|
65
|
+
currentToolInput: "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function toClaudeUsageTotals(usage: any): ClaudeUsageTotals {
|
|
70
|
+
return {
|
|
71
|
+
input: usage?.input_tokens || 0,
|
|
72
|
+
output: usage?.output_tokens || 0,
|
|
73
|
+
cacheRead: usage?.cache_read_input_tokens || 0,
|
|
74
|
+
cacheWrite: usage?.cache_creation_input_tokens || 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getContextTokensFromUsage(usage: ClaudeUsageTotals | undefined): number {
|
|
79
|
+
if (!usage) return 0;
|
|
80
|
+
return usage.input + usage.cacheRead + usage.cacheWrite;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ensureUsageTrackingState(state: ClaudeStreamState): void {
|
|
84
|
+
state.completedUsage ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
85
|
+
state.lastTurnContextTokens ??= state.usage.contextTokens || 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getCompletedUsage(state: ClaudeStreamState): ClaudeUsageTotals {
|
|
89
|
+
state.completedUsage ??= { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
90
|
+
return state.completedUsage;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function syncUsageTotals(state: ClaudeStreamState): void {
|
|
94
|
+
ensureUsageTrackingState(state);
|
|
95
|
+
const current = state.currentMessageUsage;
|
|
96
|
+
const completed = getCompletedUsage(state);
|
|
97
|
+
state.usage.input = completed.input + (current?.input || 0);
|
|
98
|
+
state.usage.output = completed.output + (current?.output || 0);
|
|
99
|
+
state.usage.cacheRead = completed.cacheRead + (current?.cacheRead || 0);
|
|
100
|
+
state.usage.cacheWrite = completed.cacheWrite + (current?.cacheWrite || 0);
|
|
101
|
+
state.usage.contextTokens = current ? getContextTokensFromUsage(current) : (state.lastTurnContextTokens ?? 0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function finalizeCurrentMessageUsage(state: ClaudeStreamState): void {
|
|
105
|
+
ensureUsageTrackingState(state);
|
|
106
|
+
if (!state.currentMessageUsage) return;
|
|
107
|
+
const completed = getCompletedUsage(state);
|
|
108
|
+
completed.input += state.currentMessageUsage.input;
|
|
109
|
+
completed.output += state.currentMessageUsage.output;
|
|
110
|
+
completed.cacheRead += state.currentMessageUsage.cacheRead;
|
|
111
|
+
completed.cacheWrite += state.currentMessageUsage.cacheWrite;
|
|
112
|
+
state.lastTurnContextTokens = getContextTokensFromUsage(state.currentMessageUsage);
|
|
113
|
+
state.currentMessageUsage = undefined;
|
|
114
|
+
syncUsageTotals(state);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function processClaudeEvent(state: ClaudeStreamState, event: any): boolean {
|
|
118
|
+
ensureUsageTrackingState(state);
|
|
119
|
+
|
|
120
|
+
if (event.session_id && !state.sessionId) {
|
|
121
|
+
state.sessionId = event.session_id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.type === "system") {
|
|
125
|
+
if (event.subtype === "init") {
|
|
126
|
+
if (event.model && !state.model) state.model = event.model;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (event.type === "stream_event") {
|
|
132
|
+
return processStreamEvent(state, event.event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (event.type === "assistant") {
|
|
136
|
+
return processAssistantSnapshot(state, event);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (event.type === "user") {
|
|
140
|
+
processUserEvent(state, event);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (event.type === "result") {
|
|
145
|
+
processResultEvent(state, event);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function processStreamEvent(state: ClaudeStreamState, ev: any): boolean {
|
|
153
|
+
if (!ev?.type) return false;
|
|
154
|
+
|
|
155
|
+
if (ev.type === "message_start") {
|
|
156
|
+
finalizeCurrentMessageUsage(state);
|
|
157
|
+
const msg = ev.message;
|
|
158
|
+
if (msg?.model && !state.model) state.model = msg.model;
|
|
159
|
+
const usage = msg?.usage;
|
|
160
|
+
if (usage) {
|
|
161
|
+
state.currentMessageUsage = toClaudeUsageTotals(usage);
|
|
162
|
+
syncUsageTotals(state);
|
|
163
|
+
}
|
|
164
|
+
state.liveThinking = undefined;
|
|
165
|
+
state.thoughtText = undefined;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (ev.type === "content_block_start") {
|
|
170
|
+
const block = ev.content_block;
|
|
171
|
+
if (block?.type === "text") {
|
|
172
|
+
state.liveText = "";
|
|
173
|
+
} else if (block?.type === "tool_use") {
|
|
174
|
+
state.liveToolCalls++;
|
|
175
|
+
state.currentToolName = block.name;
|
|
176
|
+
state.currentToolInput = "";
|
|
177
|
+
state.liveActivityPreview = `\u2192 ${block.name}`;
|
|
178
|
+
} else if (block?.type === "thinking") {
|
|
179
|
+
state.liveThinking = undefined;
|
|
180
|
+
}
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (ev.type === "content_block_delta") {
|
|
185
|
+
const delta = ev.delta;
|
|
186
|
+
if (delta?.type === "text_delta") {
|
|
187
|
+
const chunk = delta.text ?? "";
|
|
188
|
+
state.liveText = `${state.liveText ?? ""}${chunk}`;
|
|
189
|
+
const preview = extractActivityPreviewFromTextDelta(state.liveText);
|
|
190
|
+
if (preview) state.liveActivityPreview = preview;
|
|
191
|
+
} else if (delta?.type === "input_json_delta") {
|
|
192
|
+
state.currentToolInput += delta.partial_json ?? "";
|
|
193
|
+
if (state.currentToolName) {
|
|
194
|
+
const argSnippet = state.currentToolInput.slice(0, 80);
|
|
195
|
+
state.liveActivityPreview = `\u2192 ${state.currentToolName}(${argSnippet})`;
|
|
196
|
+
}
|
|
197
|
+
} else if (delta?.type === "thinking_delta") {
|
|
198
|
+
const raw = delta.thinking ?? "";
|
|
199
|
+
if (raw) {
|
|
200
|
+
state.liveThinking = `${state.liveThinking ?? ""}${raw}`;
|
|
201
|
+
const thoughtText = extractThoughtText(state.liveThinking);
|
|
202
|
+
if (thoughtText) state.thoughtText = thoughtText;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (ev.type === "content_block_stop") {
|
|
209
|
+
state.currentToolName = undefined;
|
|
210
|
+
state.currentToolInput = "";
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (ev.type === "message_delta") {
|
|
215
|
+
const usage = ev.usage;
|
|
216
|
+
if (usage) {
|
|
217
|
+
state.currentMessageUsage = toClaudeUsageTotals(usage);
|
|
218
|
+
syncUsageTotals(state);
|
|
219
|
+
finalizeCurrentMessageUsage(state);
|
|
220
|
+
}
|
|
221
|
+
if (ev.delta?.stop_reason) {
|
|
222
|
+
state.stopReason = ev.delta.stop_reason;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractThoughtTextFromClaudeMessage(msg: any): string | undefined {
|
|
231
|
+
for (const block of msg.content ?? []) {
|
|
232
|
+
if (block.type !== "thinking") continue;
|
|
233
|
+
const thoughtText = extractThoughtText(block.thinking ?? "");
|
|
234
|
+
if (thoughtText) return thoughtText;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function processAssistantSnapshot(state: ClaudeStreamState, event: any): boolean {
|
|
240
|
+
const msg = event.message;
|
|
241
|
+
if (!msg) return false;
|
|
242
|
+
|
|
243
|
+
if (msg.model && !state.model) state.model = msg.model;
|
|
244
|
+
|
|
245
|
+
if (event.error) {
|
|
246
|
+
state.errorMessage = event.error;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (msg.usage) {
|
|
250
|
+
state.currentMessageUsage = toClaudeUsageTotals(msg.usage);
|
|
251
|
+
state.lastTurnContextTokens = getContextTokensFromUsage(state.currentMessageUsage);
|
|
252
|
+
syncUsageTotals(state);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const piMessage = claudeMessageToPi(msg);
|
|
256
|
+
state.messages.push(piMessage);
|
|
257
|
+
state.usage.turns++;
|
|
258
|
+
|
|
259
|
+
const thoughtText = extractThoughtTextFromClaudeMessage(msg);
|
|
260
|
+
if (thoughtText) state.thoughtText = thoughtText;
|
|
261
|
+
|
|
262
|
+
state.liveText = undefined;
|
|
263
|
+
state.liveThinking = undefined;
|
|
264
|
+
state.currentToolName = undefined;
|
|
265
|
+
state.currentToolInput = "";
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function processUserEvent(state: ClaudeStreamState, event: any): void {
|
|
271
|
+
const msg = event.message;
|
|
272
|
+
if (!msg) return;
|
|
273
|
+
|
|
274
|
+
const contentBlocks: any[] = [];
|
|
275
|
+
for (const block of msg.content ?? []) {
|
|
276
|
+
if (block.type === "tool_result") {
|
|
277
|
+
const textContent = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
|
|
278
|
+
contentBlocks.push({ type: "text", text: textContent });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const piMessage = {
|
|
283
|
+
role: "user" as const,
|
|
284
|
+
content: contentBlocks.length > 0 ? contentBlocks : [{ type: "text" as const, text: "" }],
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
} as Message;
|
|
287
|
+
|
|
288
|
+
state.messages.push(piMessage);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function processResultEvent(state: ClaudeStreamState, event: any): void {
|
|
292
|
+
state.resultReceived = true;
|
|
293
|
+
state.resultEvent = event;
|
|
294
|
+
state.isError = event.is_error === true;
|
|
295
|
+
state.stopReason = event.stop_reason;
|
|
296
|
+
state.permissionDenials = event.permission_denials ?? [];
|
|
297
|
+
|
|
298
|
+
if (event.session_id) state.sessionId = event.session_id;
|
|
299
|
+
|
|
300
|
+
if (event.is_error && event.result) {
|
|
301
|
+
state.errorMessage = event.result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (event.usage) {
|
|
305
|
+
const totals = toClaudeUsageTotals(event.usage);
|
|
306
|
+
state.completedUsage = totals;
|
|
307
|
+
state.currentMessageUsage = undefined;
|
|
308
|
+
state.usage = {
|
|
309
|
+
input: totals.input,
|
|
310
|
+
output: totals.output,
|
|
311
|
+
cacheRead: totals.cacheRead,
|
|
312
|
+
cacheWrite: totals.cacheWrite,
|
|
313
|
+
cost: event.total_cost_usd || 0,
|
|
314
|
+
contextTokens:
|
|
315
|
+
(state.lastTurnContextTokens ?? 0) > 0 ? (state.lastTurnContextTokens ?? 0) : getContextTokensFromUsage(totals),
|
|
316
|
+
turns: event.num_turns || state.usage.turns,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function claudeMessageToPi(msg: any): Message {
|
|
322
|
+
const content: any[] = [];
|
|
323
|
+
|
|
324
|
+
for (const block of msg.content ?? []) {
|
|
325
|
+
if (block.type === "text") {
|
|
326
|
+
content.push({ type: "text", text: block.text });
|
|
327
|
+
} else if (block.type === "tool_use") {
|
|
328
|
+
content.push({
|
|
329
|
+
type: "toolCall",
|
|
330
|
+
id: block.id,
|
|
331
|
+
name: block.name,
|
|
332
|
+
arguments: block.input ?? {},
|
|
333
|
+
});
|
|
334
|
+
} else if (block.type === "thinking") {
|
|
335
|
+
content.push({ type: "thinking", thinking: block.thinking ?? "" });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
role: msg.role ?? "assistant",
|
|
341
|
+
content,
|
|
342
|
+
model: msg.model,
|
|
343
|
+
stopReason: msg.stop_reason ?? undefined,
|
|
344
|
+
usage: msg.usage
|
|
345
|
+
? {
|
|
346
|
+
input: msg.usage.input_tokens || 0,
|
|
347
|
+
output: msg.usage.output_tokens || 0,
|
|
348
|
+
cacheRead: msg.usage.cache_read_input_tokens || 0,
|
|
349
|
+
cacheWrite: msg.usage.cache_creation_input_tokens || 0,
|
|
350
|
+
}
|
|
351
|
+
: undefined,
|
|
352
|
+
} as any;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function stateToSingleResult(
|
|
356
|
+
state: ClaudeStreamState,
|
|
357
|
+
agent: string,
|
|
358
|
+
agentSource: "user" | "project" | "unknown",
|
|
359
|
+
task: string,
|
|
360
|
+
exitCode: number,
|
|
361
|
+
step: number | undefined,
|
|
362
|
+
stderr: string,
|
|
363
|
+
): SingleResult {
|
|
364
|
+
const result: SingleResult = {
|
|
365
|
+
agent,
|
|
366
|
+
agentSource,
|
|
367
|
+
task,
|
|
368
|
+
exitCode,
|
|
369
|
+
messages: state.messages,
|
|
370
|
+
stderr,
|
|
371
|
+
usage: { ...state.usage },
|
|
372
|
+
model: state.model,
|
|
373
|
+
stopReason: state.stopReason,
|
|
374
|
+
errorMessage: state.errorMessage,
|
|
375
|
+
step,
|
|
376
|
+
runtime: "claude",
|
|
377
|
+
claudeSessionId: state.sessionId,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (state.liveThinking) {
|
|
381
|
+
result.liveThinking = state.liveThinking;
|
|
382
|
+
}
|
|
383
|
+
if (state.thoughtText) {
|
|
384
|
+
result.thoughtText = state.thoughtText;
|
|
385
|
+
}
|
|
386
|
+
if (state.liveActivityPreview) {
|
|
387
|
+
result.liveActivityPreview = state.liveActivityPreview;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (state.isError && !result.errorMessage && state.resultEvent?.result) {
|
|
391
|
+
result.errorMessage = state.resultEvent.result;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (state.permissionDenials.length > 0) {
|
|
395
|
+
const names = state.permissionDenials.map((d: any) => d.tool_name).join(", ");
|
|
396
|
+
result.errorMessage = `Permission denied for tools: ${names}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
}
|