pi-agent-flow 1.8.1 → 1.8.3

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.
Files changed (154) hide show
  1. package/README.md +4 -30
  2. package/agents/audit.md +1 -2
  3. package/agents/build.md +1 -0
  4. package/agents/craft.md +12 -8
  5. package/agents/debug.md +2 -2
  6. package/agents/ideas.md +1 -0
  7. package/agents/scout.md +1 -0
  8. package/dist/agents.d.ts +41 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +283 -0
  11. package/dist/agents.js.map +1 -0
  12. package/dist/batch/batch-bash.d.ts +87 -0
  13. package/dist/batch/batch-bash.d.ts.map +1 -0
  14. package/dist/batch/batch-bash.js +369 -0
  15. package/dist/batch/batch-bash.js.map +1 -0
  16. package/dist/batch/constants.d.ts +100 -0
  17. package/dist/batch/constants.d.ts.map +1 -0
  18. package/dist/batch/constants.js +15 -0
  19. package/dist/batch/constants.js.map +1 -0
  20. package/dist/batch/execute.d.ts +21 -0
  21. package/dist/batch/execute.d.ts.map +1 -0
  22. package/dist/batch/execute.js +440 -0
  23. package/dist/batch/execute.js.map +1 -0
  24. package/dist/batch/fuzzy-edit.d.ts +29 -0
  25. package/dist/batch/fuzzy-edit.d.ts.map +1 -0
  26. package/dist/batch/fuzzy-edit.js +257 -0
  27. package/dist/batch/fuzzy-edit.js.map +1 -0
  28. package/dist/batch/index.d.ts +85 -0
  29. package/dist/batch/index.d.ts.map +1 -0
  30. package/dist/batch/index.js +422 -0
  31. package/dist/batch/index.js.map +1 -0
  32. package/dist/batch/render.d.ts +14 -0
  33. package/dist/batch/render.d.ts.map +1 -0
  34. package/dist/batch/render.js +74 -0
  35. package/dist/batch/render.js.map +1 -0
  36. package/dist/batch/symbols.d.ts +9 -0
  37. package/dist/batch/symbols.d.ts.map +1 -0
  38. package/dist/batch/symbols.js +310 -0
  39. package/dist/batch/symbols.js.map +1 -0
  40. package/dist/batch.d.ts +12 -0
  41. package/dist/batch.d.ts.map +1 -0
  42. package/dist/batch.js +11 -0
  43. package/dist/batch.js.map +1 -0
  44. package/dist/cli-args.d.ts +27 -0
  45. package/dist/cli-args.d.ts.map +1 -0
  46. package/dist/cli-args.js +265 -0
  47. package/dist/cli-args.js.map +1 -0
  48. package/dist/config.d.ts +58 -0
  49. package/dist/config.d.ts.map +1 -0
  50. package/dist/config.js +296 -0
  51. package/dist/config.js.map +1 -0
  52. package/dist/depth.d.ts +25 -0
  53. package/dist/depth.d.ts.map +1 -0
  54. package/dist/depth.js +160 -0
  55. package/dist/depth.js.map +1 -0
  56. package/dist/executor.d.ts +87 -0
  57. package/dist/executor.d.ts.map +1 -0
  58. package/dist/executor.js +295 -0
  59. package/dist/executor.js.map +1 -0
  60. package/dist/flow-prompt.d.ts +23 -0
  61. package/dist/flow-prompt.d.ts.map +1 -0
  62. package/dist/flow-prompt.js +99 -0
  63. package/dist/flow-prompt.js.map +1 -0
  64. package/dist/flow.d.ts +76 -0
  65. package/dist/flow.d.ts.map +1 -0
  66. package/dist/flow.js +704 -0
  67. package/dist/flow.js.map +1 -0
  68. package/dist/index.d.ts +10 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +327 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/reasoning-strip.d.ts +26 -0
  73. package/dist/reasoning-strip.d.ts.map +1 -0
  74. package/dist/reasoning-strip.js +58 -0
  75. package/dist/reasoning-strip.js.map +1 -0
  76. package/dist/render-utils.d.ts +42 -0
  77. package/dist/render-utils.d.ts.map +1 -0
  78. package/dist/render-utils.js +182 -0
  79. package/dist/render-utils.js.map +1 -0
  80. package/dist/render.d.ts +24 -0
  81. package/dist/render.d.ts.map +1 -0
  82. package/dist/render.js +409 -0
  83. package/dist/render.js.map +1 -0
  84. package/dist/runner-events.d.ts +59 -0
  85. package/dist/runner-events.d.ts.map +1 -0
  86. package/dist/runner-events.js +539 -0
  87. package/dist/runner-events.js.map +1 -0
  88. package/dist/session-mode.d.ts +10 -0
  89. package/dist/session-mode.d.ts.map +1 -0
  90. package/dist/session-mode.js +25 -0
  91. package/dist/session-mode.js.map +1 -0
  92. package/dist/settings-resolver.d.ts +28 -0
  93. package/dist/settings-resolver.d.ts.map +1 -0
  94. package/dist/settings-resolver.js +148 -0
  95. package/dist/settings-resolver.js.map +1 -0
  96. package/dist/sliding-prompt.d.ts +40 -0
  97. package/dist/sliding-prompt.d.ts.map +1 -0
  98. package/dist/sliding-prompt.js +121 -0
  99. package/dist/sliding-prompt.js.map +1 -0
  100. package/dist/snapshot.d.ts +29 -0
  101. package/dist/snapshot.d.ts.map +1 -0
  102. package/dist/snapshot.js +199 -0
  103. package/dist/snapshot.js.map +1 -0
  104. package/dist/structured-output.d.ts +36 -0
  105. package/dist/structured-output.d.ts.map +1 -0
  106. package/dist/structured-output.js +244 -0
  107. package/dist/structured-output.js.map +1 -0
  108. package/dist/timed-bash.d.ts +45 -0
  109. package/dist/timed-bash.d.ts.map +1 -0
  110. package/dist/timed-bash.js +219 -0
  111. package/dist/timed-bash.js.map +1 -0
  112. package/dist/tool-utils.d.ts +20 -0
  113. package/dist/tool-utils.d.ts.map +1 -0
  114. package/dist/tool-utils.js +38 -0
  115. package/dist/tool-utils.js.map +1 -0
  116. package/dist/transitions.d.ts +39 -0
  117. package/dist/transitions.d.ts.map +1 -0
  118. package/dist/transitions.js +59 -0
  119. package/dist/transitions.js.map +1 -0
  120. package/dist/types.d.ts +207 -0
  121. package/dist/types.d.ts.map +1 -0
  122. package/dist/types.js +143 -0
  123. package/dist/types.js.map +1 -0
  124. package/dist/web-tool.d.ts +35 -0
  125. package/dist/web-tool.d.ts.map +1 -0
  126. package/dist/web-tool.js +545 -0
  127. package/dist/web-tool.js.map +1 -0
  128. package/package.json +7 -5
  129. package/src/agents.ts +0 -299
  130. package/src/ambient.d.ts +0 -107
  131. package/src/batch/batch-bash.ts +0 -443
  132. package/src/batch/constants.ts +0 -128
  133. package/src/batch/execute.ts +0 -551
  134. package/src/batch/fuzzy-edit.ts +0 -323
  135. package/src/batch/index.ts +0 -494
  136. package/src/batch/render.ts +0 -81
  137. package/src/batch/symbols.ts +0 -341
  138. package/src/batch.ts +0 -28
  139. package/src/cli-args.ts +0 -315
  140. package/src/config.ts +0 -391
  141. package/src/executor.ts +0 -445
  142. package/src/flow.ts +0 -834
  143. package/src/hooks.ts +0 -294
  144. package/src/index.ts +0 -1132
  145. package/src/render-utils.ts +0 -205
  146. package/src/render.ts +0 -524
  147. package/src/runner-events.ts +0 -692
  148. package/src/session-mode.ts +0 -33
  149. package/src/sliding-prompt.ts +0 -144
  150. package/src/structured-output.ts +0 -195
  151. package/src/timed-bash.ts +0 -270
  152. package/src/transitions.ts +0 -86
  153. package/src/types.ts +0 -386
  154. package/src/web-tool.ts +0 -663
@@ -1,692 +0,0 @@
1
- /**
2
- * Helpers for parsing Pi JSON mode events and summarizing flow results.
3
- */
4
-
5
- import type { Message } from "@mariozechner/pi-ai";
6
-
7
- // WeakMap-based side tables to avoid polluting caller objects and survive frozen/sealed objects.
8
- const seenSignaturesMap = new WeakMap<object, Set<string>>();
9
- const streamingTextBufferMap = new WeakMap<object, string>();
10
- const lastEmittedWordCountMap = new WeakMap<object, number>();
11
- const streamingEstimateMap = new WeakMap<object, { chars: number }>();
12
- const smoothedTpsMap = new WeakMap<object, number>();
13
- const lastEmitTimeMap = new WeakMap<object, number>();
14
- const pendingTokensMap = new WeakMap<object, number>();
15
- const pauseAfterNextEmitMap = new WeakMap<object, boolean>();
16
- const ctxBaselineMap = new WeakMap<object, number>();
17
- const ctxStreamingCharsMap = new WeakMap<object, number>();
18
-
19
- function getSeenFlowMessageSignatures(result: object): Set<string> {
20
- if (!seenSignaturesMap.has(result)) {
21
- seenSignaturesMap.set(result, new Set());
22
- }
23
- return seenSignaturesMap.get(result)!;
24
- }
25
-
26
- interface StreamingTextState {
27
- buffer: string;
28
- lastEmittedWordCount: number;
29
- }
30
-
31
- function getStreamingTextState(result: object): StreamingTextState {
32
- if (!streamingTextBufferMap.has(result)) {
33
- streamingTextBufferMap.set(result, "");
34
- lastEmittedWordCountMap.set(result, 0);
35
- }
36
- return {
37
- get buffer() { return streamingTextBufferMap.get(result)!; },
38
- set buffer(v) { streamingTextBufferMap.set(result, v); },
39
- get lastEmittedWordCount() { return lastEmittedWordCountMap.get(result)!; },
40
- set lastEmittedWordCount(v) { lastEmittedWordCountMap.set(result, v); },
41
- };
42
- }
43
-
44
- /**
45
- * Drain the accumulated streaming text buffer and return it.
46
- * Updates the last-emitted word count for threshold tracking.
47
- */
48
- export function drainStreamingText(result: object): string {
49
- const state = getStreamingTextState(result);
50
- const buf = state.buffer;
51
- if (!buf) return "";
52
- state.buffer = "";
53
- state.lastEmittedWordCount = 0;
54
- return buf;
55
- }
56
-
57
- // ---------------------------------------------------------------------------
58
- // Streaming token estimate
59
- // ---------------------------------------------------------------------------
60
-
61
- /** Chars per token heuristic for output estimation. */
62
- const CHARS_PER_TOKEN = 4;
63
-
64
- /** Minimum elapsed ms between TPS samples. */
65
- const MIN_TPS_SAMPLE_MS = 50;
66
- /** Cap on instantaneous TPS to suppress burst artifacts. */
67
- const MAX_INSTANT_TPS = 300;
68
- /** Calibration scale to align heuristic tokens with empirical display range. */
69
- const TPS_CALIBRATION = 1.0;
70
- /** EMA smoothing factor for tokens-per-second (higher = more responsive). */
71
- const EMA_ALPHA = 0.35;
72
- /** Emit streaming text as soon as any non-empty delta arrives. */
73
- const STREAMING_EMIT_CHARS = 1;
74
-
75
- function getStreamingEstimate(result: object): { chars: number } {
76
- if (!streamingEstimateMap.has(result)) {
77
- streamingEstimateMap.set(result, { chars: 0 });
78
- }
79
- return streamingEstimateMap.get(result)!;
80
- }
81
-
82
- interface TpsState {
83
- smoothedTps: number;
84
- lastEmitTime: number;
85
- pendingTokens: number;
86
- pauseAfterNextEmit: boolean;
87
- }
88
-
89
- /**
90
- * Lazily initialize TPS tracking state on the result object.
91
- * Returns an accessor object backed by a WeakMap so frozen/sealed
92
- * objects do not throw.
93
- */
94
- function getTpsState(result: object): TpsState {
95
- if (!smoothedTpsMap.has(result)) {
96
- smoothedTpsMap.set(result, 0);
97
- lastEmitTimeMap.set(result, 0);
98
- pendingTokensMap.set(result, 0);
99
- pauseAfterNextEmitMap.set(result, false);
100
- }
101
- return {
102
- get smoothedTps() { return smoothedTpsMap.get(result)!; },
103
- set smoothedTps(v) { smoothedTpsMap.set(result, v); },
104
- get lastEmitTime() { return lastEmitTimeMap.get(result)!; },
105
- set lastEmitTime(v) { lastEmitTimeMap.set(result, v); },
106
- get pendingTokens() { return pendingTokensMap.get(result)!; },
107
- set pendingTokens(v) { pendingTokensMap.set(result, v); },
108
- get pauseAfterNextEmit() { return pauseAfterNextEmitMap.get(result)!; },
109
- set pauseAfterNextEmit(v) { pauseAfterNextEmitMap.set(result, v); },
110
- };
111
- }
112
-
113
- /**
114
- * Update the EMA-smoothed tokens-per-second based on a new sample.
115
- * Called from emitUpdate() with the estimated output tokens since last emit.
116
- * Accumulates tokens in pendingTokens and only computes a rate when
117
- * MIN_TPS_SAMPLE_MS has elapsed. Applies MAX_INSTANT_TPS cap before EMA.
118
- */
119
- export function updateSmoothedTps(result: object, estimatedTokens: number): void {
120
- const tracker = getTpsState(result);
121
-
122
- if (estimatedTokens <= 0) {
123
- if (tracker.pauseAfterNextEmit) {
124
- tracker.lastEmitTime = 0;
125
- tracker.pauseAfterNextEmit = false;
126
- }
127
- return;
128
- }
129
-
130
- if (tracker.lastEmitTime === 0) {
131
- // First emit after a gap — seed the value directly
132
- tracker.lastEmitTime = Date.now();
133
- tracker.pauseAfterNextEmit = false;
134
- return;
135
- }
136
-
137
- tracker.pendingTokens += estimatedTokens;
138
- const now = Date.now();
139
- const deltaMs = now - tracker.lastEmitTime;
140
- if (deltaMs < MIN_TPS_SAMPLE_MS) {
141
- // If a pause was requested but we can't compute yet, reset the timer
142
- // so the upcoming gap (e.g., tool execution) isn't counted.
143
- if (tracker.pauseAfterNextEmit) {
144
- tracker.lastEmitTime = 0;
145
- tracker.pauseAfterNextEmit = false;
146
- tracker.pendingTokens = 0;
147
- }
148
- return;
149
- }
150
- const deltaSec = deltaMs / 1000;
151
- let instantRate = (tracker.pendingTokens * TPS_CALIBRATION) / deltaSec;
152
- if (instantRate > MAX_INSTANT_TPS) {
153
- instantRate = MAX_INSTANT_TPS;
154
- }
155
- if (tracker.smoothedTps === 0) {
156
- tracker.smoothedTps = instantRate;
157
- } else {
158
- tracker.smoothedTps = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * tracker.smoothedTps;
159
- }
160
- tracker.lastEmitTime = now;
161
- tracker.pendingTokens = 0;
162
-
163
- if (tracker.pauseAfterNextEmit) {
164
- tracker.lastEmitTime = 0;
165
- tracker.pauseAfterNextEmit = false;
166
- }
167
- }
168
-
169
- /**
170
- * Return the current EMA-smoothed tokens-per-second value.
171
- */
172
- export function drainSmoothedTps(result: object): number {
173
- const tracker = getTpsState(result);
174
- return tracker.smoothedTps;
175
- }
176
-
177
- interface CtxState {
178
- baseline: number;
179
- streamingChars: number;
180
- }
181
-
182
- /**
183
- * Lazily initialize ctx baseline tracking state on the result object.
184
- * Returns an accessor object backed by a WeakMap so frozen/sealed
185
- * objects do not throw.
186
- */
187
- function getCtxState(result: object): CtxState {
188
- if (!ctxBaselineMap.has(result)) {
189
- ctxBaselineMap.set(result, 0);
190
- ctxStreamingCharsMap.set(result, 0);
191
- }
192
- return {
193
- get baseline() { return ctxBaselineMap.get(result)!; },
194
- set baseline(v) { ctxBaselineMap.set(result, v); },
195
- get streamingChars() { return ctxStreamingCharsMap.get(result)!; },
196
- set streamingChars(v) { ctxStreamingCharsMap.set(result, v); },
197
- };
198
- }
199
-
200
- /**
201
- * Track streaming characters and estimate output tokens.
202
- * Called on every streaming delta.
203
- */
204
- function updateStreamingEstimate(result: object, deltaLength: number): void {
205
- if (deltaLength <= 0) return;
206
- const est = getStreamingEstimate(result);
207
- est.chars += deltaLength;
208
- // Also accumulate chars for ctx estimation (not drained on emit)
209
- const ctxState = getCtxState(result);
210
- ctxState.streamingChars += deltaLength;
211
- }
212
-
213
- /**
214
- * Drain the current streaming estimate and return estimated output tokens.
215
- * Returns 0 when no streaming has occurred.
216
- */
217
- export function drainStreamingEstimate(result: object): number {
218
- const est = getStreamingEstimate(result);
219
- const tokens = Math.floor(est.chars / CHARS_PER_TOKEN);
220
- est.chars = est.chars % CHARS_PER_TOKEN;
221
- return tokens;
222
- }
223
-
224
- /**
225
- * Return the estimated context tokens: last known real totalTokens (baseline)
226
- * plus any additional output tokens estimated since that baseline was set.
227
- * Returns 0 before the first message_end when no baseline exists yet,
228
- * in which case the caller should fall back to the streaming output estimate.
229
- */
230
- export function drainCtxEstimate(result: object): number {
231
- const ctxState = getCtxState(result);
232
- const streamingTokens = Math.floor(ctxState.streamingChars / CHARS_PER_TOKEN);
233
- return ctxState.baseline + streamingTokens;
234
- }
235
-
236
- /**
237
- * Accumulate a text or thinking delta into the streaming buffer.
238
- * Returns true if the caller should emit an update.
239
- */
240
- function accumulateStreamingDelta(result: object, delta: string): boolean {
241
- if (!delta) return false;
242
- const state = getStreamingTextState(result);
243
- state.buffer = state.buffer + delta;
244
- updateStreamingEstimate(result, delta.length);
245
- if (state.buffer.length - state.lastEmittedWordCount >= STREAMING_EMIT_CHARS) {
246
- state.lastEmittedWordCount = state.buffer.length;
247
- return true;
248
- }
249
- return false;
250
- }
251
-
252
- export function stableStringify(value: unknown, seen = new WeakSet<object>()): string {
253
- if (value === null || typeof value !== "object") {
254
- return JSON.stringify(value);
255
- }
256
-
257
- if (seen.has(value)) {
258
- return '"[Circular]"';
259
- }
260
- seen.add(value);
261
-
262
- if (Array.isArray(value)) {
263
- const out = `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
264
- seen.delete(value);
265
- return out;
266
- }
267
-
268
- const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
269
- const out = `{${entries
270
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue, seen)}`)
271
- .join(",")}}`;
272
- seen.delete(value);
273
- return out;
274
- }
275
-
276
- function getMessageSignature(message: unknown): string {
277
- return stableStringify(message);
278
- }
279
-
280
- interface AssistantMessage extends Record<string, unknown> {
281
- role: string;
282
- model?: string;
283
- stopReason?: string;
284
- errorMessage?: string;
285
- content?: unknown;
286
- usage?: {
287
- input?: number;
288
- output?: number;
289
- cacheRead?: number;
290
- cacheWrite?: number;
291
- cost?: { total?: number };
292
- totalTokens?: number;
293
- };
294
- }
295
-
296
- function updateAssistantMetadata(result: { model?: string; stopReason?: string; errorMessage?: string }, message: AssistantMessage): void {
297
- if (!message || message.role !== "assistant") return;
298
- if (!result.model && message.model) result.model = message.model;
299
- if (message.stopReason) result.stopReason = message.stopReason;
300
- if (message.errorMessage) result.errorMessage = message.errorMessage;
301
- }
302
-
303
- // Reasoning/thinking part types to strip from flow results
304
- const REASONING_PART_TYPES = new Set([
305
- "thinking",
306
- "reasoning",
307
- "reasoning_content",
308
- "reasoningContent",
309
- ]);
310
-
311
- interface MessagePart {
312
- type: string;
313
- text?: string;
314
- }
315
-
316
- /**
317
- * Strip thinking/reasoning content from assistant messages.
318
- * Removes top-level thinking/reasoning fields and filters content parts.
319
- * Preserves text, toolCall, and other non-reasoning parts.
320
- */
321
- function stripReasoningFromMessage(message: AssistantMessage): AssistantMessage {
322
- if (!message || message.role !== "assistant") return message;
323
-
324
- const sanitized: AssistantMessage = { ...message };
325
-
326
- // Remove top-level reasoning fields
327
- delete (sanitized as Record<string, unknown>).thinking;
328
- delete (sanitized as Record<string, unknown>).thinkingSignature;
329
- delete (sanitized as Record<string, unknown>).reasoning;
330
- delete (sanitized as Record<string, unknown>).reasoning_content;
331
- delete (sanitized as Record<string, unknown>).reasoningContent;
332
-
333
- // Filter out reasoning parts from content array
334
- if (Array.isArray(sanitized.content)) {
335
- sanitized.content = (sanitized.content as MessagePart[]).filter(
336
- (part) => !REASONING_PART_TYPES.has(part?.type),
337
- );
338
- }
339
-
340
- return sanitized;
341
- }
342
-
343
- export interface FlowResult {
344
- messages: Message[];
345
- model?: string;
346
- stopReason?: string;
347
- exitCode?: number;
348
- stderr?: string;
349
- errorMessage?: string;
350
- usage?: {
351
- input: number;
352
- output: number;
353
- cacheRead: number;
354
- cacheWrite: number;
355
- cost: number;
356
- totalTokens?: number;
357
- turns: number;
358
- toolCalls: number;
359
- smoothedTps?: number;
360
- contextTokens: number;
361
- };
362
- sawAgentEnd?: boolean;
363
- streamingText?: string;
364
- }
365
-
366
- function addFlowAssistantMessage(result: FlowResult, message: AssistantMessage): boolean {
367
- if (!message || message.role !== "assistant") return false;
368
-
369
- // Strip reasoning/thinking from the message before storing
370
- const sanitized = stripReasoningFromMessage(message);
371
-
372
- updateAssistantMetadata(result, sanitized);
373
-
374
- const signature = getMessageSignature(sanitized);
375
- const seen = getSeenFlowMessageSignatures(result);
376
- if (seen.has(signature)) return false;
377
- seen.add(signature);
378
-
379
- result.messages.push(sanitized as Message);
380
-
381
- // Reset streaming estimate when actual usage arrives
382
- const est = getStreamingEstimate(result);
383
- est.chars = 0;
384
-
385
- result.usage!.turns++;
386
- const usage = message.usage;
387
- if (usage) {
388
- result.usage!.input += usage.input || 0;
389
- result.usage!.output += usage.output || 0;
390
- result.usage!.cacheRead += usage.cacheRead || 0;
391
- result.usage!.cacheWrite += usage.cacheWrite || 0;
392
- result.usage!.cost += usage.cost?.total || 0;
393
- result.usage!.contextTokens = usage.totalTokens || 0;
394
-
395
- // Snapshot ctx baseline for smooth streaming estimation
396
- const ctxState = getCtxState(result);
397
- ctxState.baseline = usage.totalTokens || 0;
398
- ctxState.streamingChars = 0;
399
- }
400
-
401
- // Count tool call parts in the message content and estimate their tokens
402
- if (Array.isArray(message.content)) {
403
- let toolCallChars = 0;
404
- for (const part of message.content as Array<{ type: string; name?: string; toolName?: string; arguments?: unknown; input?: unknown }>) {
405
- if (part.type === "toolCall") {
406
- result.usage!.toolCalls++;
407
- const tcText = JSON.stringify({ name: part.name, args: part.arguments || part.input || {} });
408
- toolCallChars += tcText.length;
409
- }
410
- }
411
- if (toolCallChars > 0) {
412
- updateStreamingEstimate(result, toolCallChars);
413
- const tracker = getTpsState(result);
414
- tracker.pauseAfterNextEmit = true;
415
- }
416
- }
417
-
418
- return true;
419
- }
420
-
421
- interface ToolMessage {
422
- role: string;
423
- toolCallId?: string;
424
- tool_call_id?: string;
425
- content?: unknown;
426
- }
427
-
428
- function addFlowToolMessage(result: FlowResult, message: ToolMessage): boolean {
429
- if (!message || message.role !== "tool") return false;
430
-
431
- const signature = getMessageSignature(message);
432
- const seen = getSeenFlowMessageSignatures(result);
433
- if (seen.has(signature)) return false;
434
- seen.add(signature);
435
-
436
- result.messages.push(message as Message);
437
- return true;
438
- }
439
-
440
- function addFlowMessages(result: FlowResult, messages: unknown[]): boolean {
441
- if (!Array.isArray(messages)) return false;
442
- let changed = false;
443
- for (const message of messages) {
444
- if (message && (message as Record<string, unknown>).role === "tool") {
445
- if (addFlowToolMessage(result, message as ToolMessage)) changed = true;
446
- } else if (message && (message as Record<string, unknown>).role === "assistant") {
447
- if (addFlowAssistantMessage(result, message as AssistantMessage)) changed = true;
448
- }
449
- }
450
- return changed;
451
- }
452
-
453
- interface FlowEvent {
454
- type: string;
455
- message?: AssistantMessage | ToolMessage;
456
- messages?: unknown[];
457
- assistantMessageEvent?: {
458
- type: string;
459
- delta?: string;
460
- };
461
- }
462
-
463
- function processFlowEvent(event: FlowEvent, result: FlowResult): boolean {
464
- if (!event || typeof event !== "object") return false;
465
-
466
- switch (event.type) {
467
- case "message_end":
468
- return addFlowMessages(result, [event.message]);
469
-
470
- case "turn_end":
471
- return addFlowMessages(result, [event.message]);
472
-
473
- case "agent_end":
474
- result.sawAgentEnd = true;
475
- return addFlowMessages(result, event.messages ?? []);
476
-
477
- case "message_update": {
478
- const evt = event.assistantMessageEvent;
479
- if (!evt || typeof evt !== "object") return false;
480
- if (evt.type === "text_delta") {
481
- return accumulateStreamingDelta(result, evt.delta ?? "");
482
- }
483
- // thinking_delta is intentionally NOT accumulated into the streaming buffer.
484
- // Reasoning content is stripped from flow results to keep output clean.
485
- if (evt.type === "thinking_delta") {
486
- return false;
487
- }
488
- return false;
489
- }
490
-
491
- default:
492
- return false;
493
- }
494
- }
495
-
496
- export function processFlowJsonLine(line: string, result: FlowResult): boolean {
497
- if (!line.trim()) return false;
498
-
499
- let event: FlowEvent;
500
- try {
501
- event = JSON.parse(line) as FlowEvent;
502
- } catch {
503
- return false;
504
- }
505
-
506
- return processFlowEvent(event, result);
507
- }
508
-
509
- export function getFlowFinalText(messages: Message[]): string {
510
- if (!Array.isArray(messages)) return "";
511
-
512
- for (let i = messages.length - 1; i >= 0; i--) {
513
- const message = messages[i];
514
- if (!message || message.role !== "assistant") {
515
- continue;
516
- }
517
- if (typeof message.content === "string" && message.content.length > 0) {
518
- return message.content;
519
- }
520
- if (!Array.isArray(message.content)) {
521
- continue;
522
- }
523
-
524
- for (const part of message.content) {
525
- if (part?.type === "text" && typeof part.text === "string" && part.text.length > 0) {
526
- return part.text;
527
- }
528
- }
529
- }
530
-
531
- return "";
532
- }
533
-
534
- interface ToolCallEntry {
535
- name: string;
536
- args: Record<string, unknown>;
537
- }
538
-
539
- function extractNonReadToolCalls(messages: Message[]): ToolCallEntry[] {
540
- const calls: ToolCallEntry[] = [];
541
- if (!Array.isArray(messages)) return calls;
542
- for (const msg of messages) {
543
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
544
- for (const part of msg.content) {
545
- if (part.type === "toolCall" && part.name !== "read") {
546
- calls.push({ name: part.name, args: (part.arguments || part.input || {}) as Record<string, unknown> });
547
- }
548
- }
549
- }
550
- return calls;
551
- }
552
-
553
- function formatToolCallShort(tc: ToolCallEntry): string {
554
- const args = tc.args || {};
555
- switch (tc.name) {
556
- case "edit":
557
- case "write":
558
- return `${tc.name} ${(args.file_path as string || args.path as string || "?").split("/").pop()}`;
559
- case "bash": {
560
- const cmd = (args.command as string || "").replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
561
- return `bash ${cmd.length > 40 ? cmd.slice(0, 40) + "..." : cmd}`;
562
- }
563
- case "grep":
564
- return `grep /${args.pattern || "?"}/ in ${args.path || "."}`;
565
- case "find":
566
- return `find ${args.pattern || "*"} in ${args.path || "."}`;
567
- case "ls":
568
- return `ls ${args.path || "."}`;
569
- case "batch": {
570
- const ops = Array.isArray(args.o) ? args.o : Array.isArray(args.op) ? args.op : Array.isArray(args.operations) ? args.operations : Array.isArray(args) ? args : [];
571
- if (ops.length === 0) return "batch (empty)";
572
- const first = ops[0] || {};
573
- const firstPath = ((first.p ?? first.path) as string ?? "?").split("/").pop();
574
- const opType = (first.o ?? first.op) as string ?? "?";
575
- const label = ops.length === 1 ? `${opType} ${firstPath}` : `${opType} ${firstPath} +${ops.length - 1} more`;
576
- return `batch ${label}`;
577
- }
578
- default:
579
- return tc.name;
580
- }
581
- }
582
-
583
- interface ToolPair {
584
- name: string;
585
- args: Record<string, unknown>;
586
- output: string;
587
- }
588
-
589
- /**
590
- * Match tool calls with their results to build a paired list.
591
- * Returns [{ name, command/args, output }] limited to the most recent pairs.
592
- */
593
- function matchToolCallsWithResults(messages: Message[], maxPairs: number): ToolPair[] {
594
- if (!Array.isArray(messages)) return [];
595
- const pairs: ToolPair[] = [];
596
-
597
- // Build a map of toolCallId -> tool result output
598
- const resultMap = new Map<string, string>();
599
- for (const msg of messages) {
600
- if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
601
- const id = (msg as unknown as { toolCallId?: string }).toolCallId || (msg as unknown as { tool_call_id?: string }).tool_call_id || "";
602
- if (!id) continue;
603
- const text = msg.content
604
- .filter((p: { type: string; text?: string }) => p.type === "text" && typeof p.text === "string")
605
- .map((p: { text: string }) => p.text)
606
- .join("\n");
607
- resultMap.set(id, text);
608
- }
609
-
610
- // Walk assistant messages to find tool calls that have matching results
611
- for (const msg of messages) {
612
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
613
- for (const part of msg.content) {
614
- if (part.type !== "toolCall") continue;
615
- const id = part.toolCallId || (part as unknown as { tool_call_id?: string }).tool_call_id || "";
616
- if (!id || !resultMap.has(id)) continue;
617
- const name = part.name || (part as unknown as { toolName?: string }).toolName || "unknown";
618
- const args = part.arguments || part.input || {};
619
- const output = resultMap.get(id)!;
620
- pairs.push({ name, args: args as Record<string, unknown>, output });
621
- }
622
- }
623
-
624
- // Return the most recent pairs
625
- return pairs.slice(-maxPairs);
626
- }
627
-
628
- /** Max tool result output chars to include per tool call in the summary. */
629
- const TOOL_RESULT_MAX_CHARS = 2000;
630
-
631
- export function getFlowSummaryText(result?: FlowResult | null): string {
632
- const finalText = getFlowFinalText(result?.messages ?? []);
633
- const isError =
634
- (typeof result?.exitCode === "number" && result.exitCode > 0) ||
635
- result?.stopReason === "error" ||
636
- result?.stopReason === "aborted";
637
-
638
- // Build error base for failed flows
639
- let errorBase = "";
640
- if (isError) {
641
- if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
642
- errorBase = result.errorMessage.trim();
643
- } else if (typeof result?.stderr === "string" && result.stderr.trim()) {
644
- errorBase = result.stderr.trim();
645
- } else {
646
- errorBase = "Flow failed";
647
- }
648
- }
649
-
650
- // Extract tool call/result pairs for context
651
- const toolPairs = matchToolCallsWithResults(result?.messages ?? [], 10);
652
- const toolSummaryParts: string[] = [];
653
-
654
- for (const pair of toolPairs) {
655
- const callLabel = formatToolCallShort({ name: pair.name, args: pair.args });
656
- if (pair.output.trim()) {
657
- const truncated = pair.output.length > TOOL_RESULT_MAX_CHARS
658
- ? pair.output.slice(0, TOOL_RESULT_MAX_CHARS) + "\n... (truncated)"
659
- : pair.output;
660
- toolSummaryParts.push(`${callLabel}:\n${truncated}`);
661
- } else {
662
- toolSummaryParts.push(`${callLabel}: (no output)`);
663
- }
664
- }
665
-
666
- const toolContext = toolSummaryParts.length > 0
667
- ? "\n\n[Tool Results]\n" + toolSummaryParts.join("\n---\n")
668
- : "";
669
-
670
- // If there's final text, include it plus tool context
671
- if (finalText) {
672
- return finalText + toolContext;
673
- }
674
-
675
- // No final text
676
- if (isError) {
677
- // Surface partial tool calls (excluding read) for failed/aborted flows
678
- const toolCalls = extractNonReadToolCalls(result?.messages ?? []);
679
- if (toolCalls.length > 0) {
680
- const formatted = toolCalls.map(formatToolCallShort).join(", ");
681
- return `${errorBase}\nPartial work: ${formatted}${toolContext}`;
682
- }
683
- return errorBase;
684
- }
685
-
686
- // Success but no final text — show tool results if any
687
- if (toolContext) {
688
- return toolContext.trim();
689
- }
690
-
691
- return "(no output)";
692
- }