pi-agent-flow 1.0.7 → 1.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/runner-events.js CHANGED
@@ -2,34 +2,36 @@
2
2
  * Helpers for parsing Pi JSON mode events and summarizing flow results.
3
3
  */
4
4
 
5
+ // WeakMap-based side tables to avoid polluting caller objects and survive frozen/sealed objects.
6
+ const seenSignaturesMap = new WeakMap();
7
+ const streamingTextBufferMap = new WeakMap();
8
+ const lastEmittedWordCountMap = new WeakMap();
9
+ const streamingEstimateMap = new WeakMap();
10
+ const smoothedTpsMap = new WeakMap();
11
+ const lastEmitTimeMap = new WeakMap();
12
+ const pendingTokensMap = new WeakMap();
13
+ const ctxBaselineMap = new WeakMap();
14
+ const ctxStreamingCharsMap = new WeakMap();
15
+ const pauseAfterNextEmitMap = new WeakMap();
16
+
5
17
  function getSeenFlowMessageSignatures(result) {
6
- if (!Object.prototype.hasOwnProperty.call(result, "__seenMessageSignatures")) {
7
- Object.defineProperty(result, "__seenMessageSignatures", {
8
- value: new Set(),
9
- enumerable: false,
10
- configurable: false,
11
- writable: false,
12
- });
18
+ if (!seenSignaturesMap.has(result)) {
19
+ seenSignaturesMap.set(result, new Set());
13
20
  }
14
- return result.__seenMessageSignatures;
21
+ return seenSignaturesMap.get(result);
15
22
  }
16
23
 
17
- function getStreamingTextBuffer(result) {
18
- if (!Object.prototype.hasOwnProperty.call(result, "__streamingTextBuffer")) {
19
- Object.defineProperty(result, "__streamingTextBuffer", {
20
- value: "",
21
- enumerable: false,
22
- configurable: false,
23
- writable: true,
24
- });
25
- Object.defineProperty(result, "__lastEmittedWordCount", {
26
- value: 0,
27
- enumerable: false,
28
- configurable: false,
29
- writable: true,
30
- });
24
+ function getStreamingTextState(result) {
25
+ if (!streamingTextBufferMap.has(result)) {
26
+ streamingTextBufferMap.set(result, "");
27
+ lastEmittedWordCountMap.set(result, 0);
31
28
  }
32
- return result.__streamingTextBuffer;
29
+ return {
30
+ get buffer() { return streamingTextBufferMap.get(result); },
31
+ set buffer(v) { streamingTextBufferMap.set(result, v); },
32
+ get lastEmittedWordCount() { return lastEmittedWordCountMap.get(result); },
33
+ set lastEmittedWordCount(v) { lastEmittedWordCountMap.set(result, v); },
34
+ };
33
35
  }
34
36
 
35
37
  /**
@@ -37,10 +39,11 @@ function getStreamingTextBuffer(result) {
37
39
  * Updates the last-emitted word count for threshold tracking.
38
40
  */
39
41
  export function drainStreamingText(result) {
40
- const buf = getStreamingTextBuffer(result);
42
+ const state = getStreamingTextState(result);
43
+ const buf = state.buffer;
41
44
  if (!buf) return "";
42
- result.__streamingTextBuffer = "";
43
- result.__lastEmittedWordCount = 0;
45
+ state.buffer = "";
46
+ state.lastEmittedWordCount = 0;
44
47
  return buf;
45
48
  }
46
49
 
@@ -51,98 +54,126 @@ export function drainStreamingText(result) {
51
54
  /** Chars per token heuristic for output estimation. */
52
55
  const CHARS_PER_TOKEN = 4;
53
56
 
57
+ /** Minimum elapsed ms between TPS samples. */
58
+ const MIN_TPS_SAMPLE_MS = 100;
59
+ /** Cap on instantaneous TPS to suppress burst artifacts. */
60
+ const MAX_INSTANT_TPS = 300;
61
+ /** Calibration scale to align heuristic tokens with empirical display range. */
62
+ const TPS_CALIBRATION = 0.1;
54
63
  /** EMA smoothing factor for tokens-per-second (higher = more responsive). */
55
- const EMA_ALPHA = 0.15;
64
+ const EMA_ALPHA = 0.1;
56
65
 
57
66
  function getStreamingEstimate(result) {
58
- if (!Object.prototype.hasOwnProperty.call(result, "__streamingEstimate")) {
59
- Object.defineProperty(result, "__streamingEstimate", {
60
- value: { chars: 0 },
61
- enumerable: false,
62
- configurable: false,
63
- writable: true,
64
- });
67
+ if (!streamingEstimateMap.has(result)) {
68
+ streamingEstimateMap.set(result, { chars: 0 });
65
69
  }
66
- return result.__streamingEstimate;
70
+ return streamingEstimateMap.get(result);
67
71
  }
68
72
 
69
73
  /**
70
- * Lazily initialize TPS tracking properties on the result object.
71
- * - __lastEmitTime: timestamp (ms) of the last streaming emit
72
- * - __smoothedTps: EMA-smoothed tokens-per-second value
74
+ * Lazily initialize TPS tracking state on the result object.
75
+ * Returns an accessor object backed by a WeakMap so frozen/sealed
76
+ * objects do not throw.
73
77
  */
74
- function getTpsTracker(result) {
75
- if (!Object.prototype.hasOwnProperty.call(result, "__smoothedTps")) {
76
- Object.defineProperty(result, "__smoothedTps", {
77
- value: 0,
78
- enumerable: false,
79
- configurable: false,
80
- writable: true,
81
- });
82
- Object.defineProperty(result, "__lastEmitTime", {
83
- value: 0,
84
- enumerable: false,
85
- configurable: false,
86
- writable: true,
87
- });
78
+ function getTpsState(result) {
79
+ if (!smoothedTpsMap.has(result)) {
80
+ smoothedTpsMap.set(result, 0);
81
+ lastEmitTimeMap.set(result, 0);
82
+ pendingTokensMap.set(result, 0);
83
+ pauseAfterNextEmitMap.set(result, false);
88
84
  }
89
- return result;
85
+ return {
86
+ get smoothedTps() { return smoothedTpsMap.get(result); },
87
+ set smoothedTps(v) { smoothedTpsMap.set(result, v); },
88
+ get lastEmitTime() { return lastEmitTimeMap.get(result); },
89
+ set lastEmitTime(v) { lastEmitTimeMap.set(result, v); },
90
+ get pendingTokens() { return pendingTokensMap.get(result); },
91
+ set pendingTokens(v) { pendingTokensMap.set(result, v); },
92
+ get pauseAfterNextEmit() { return pauseAfterNextEmitMap.get(result); },
93
+ set pauseAfterNextEmit(v) { pauseAfterNextEmitMap.set(result, v); },
94
+ };
90
95
  }
91
96
 
92
97
  /**
93
98
  * Update the EMA-smoothed tokens-per-second based on a new sample.
94
99
  * Called from emitUpdate() with the estimated output tokens since last emit.
95
- * Skips the update when delta time or tokens are zero (e.g., first emit).
100
+ * Accumulates tokens in pendingTokens and only computes a rate when
101
+ * MIN_TPS_SAMPLE_MS has elapsed. Applies MAX_INSTANT_TPS cap before EMA.
96
102
  */
97
103
  export function updateSmoothedTps(result, estimatedTokens) {
98
- if (estimatedTokens <= 0) return;
99
- const tracker = getTpsTracker(result);
104
+ const tracker = getTpsState(result);
105
+
106
+ if (estimatedTokens <= 0) {
107
+ if (tracker.pauseAfterNextEmit) {
108
+ tracker.lastEmitTime = 0;
109
+ tracker.pauseAfterNextEmit = false;
110
+ }
111
+ return;
112
+ }
113
+
114
+ if (tracker.lastEmitTime === 0) {
115
+ // First emit after a gap — seed the value directly
116
+ tracker.lastEmitTime = Date.now();
117
+ tracker.pauseAfterNextEmit = false;
118
+ return;
119
+ }
120
+
121
+ tracker.pendingTokens += estimatedTokens;
100
122
  const now = Date.now();
101
- if (tracker.__lastEmitTime === 0) {
102
- // First emit seed the value directly
103
- tracker.__lastEmitTime = now;
123
+ const deltaMs = now - tracker.lastEmitTime;
124
+ if (deltaMs < MIN_TPS_SAMPLE_MS) {
125
+ // If a pause was requested but we can't compute yet, reset the timer
126
+ // so the upcoming gap (e.g., tool execution) isn't counted.
127
+ if (tracker.pauseAfterNextEmit) {
128
+ tracker.lastEmitTime = 0;
129
+ tracker.pauseAfterNextEmit = false;
130
+ tracker.pendingTokens = 0;
131
+ }
104
132
  return;
105
133
  }
106
- const deltaSec = (now - tracker.__lastEmitTime) / 1000;
107
- if (deltaSec <= 0) return;
108
- const instantRate = estimatedTokens / deltaSec;
109
- if (tracker.__smoothedTps === 0) {
110
- tracker.__smoothedTps = instantRate;
134
+ const deltaSec = deltaMs / 1000;
135
+ let instantRate = (tracker.pendingTokens * TPS_CALIBRATION) / deltaSec;
136
+ if (instantRate > MAX_INSTANT_TPS) {
137
+ instantRate = MAX_INSTANT_TPS;
138
+ }
139
+ if (tracker.smoothedTps === 0) {
140
+ tracker.smoothedTps = instantRate;
111
141
  } else {
112
- tracker.__smoothedTps = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * tracker.__smoothedTps;
142
+ tracker.smoothedTps = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * tracker.smoothedTps;
143
+ }
144
+ tracker.lastEmitTime = now;
145
+ tracker.pendingTokens = 0;
146
+
147
+ if (tracker.pauseAfterNextEmit) {
148
+ tracker.lastEmitTime = 0;
149
+ tracker.pauseAfterNextEmit = false;
113
150
  }
114
- tracker.__lastEmitTime = now;
115
151
  }
116
152
 
117
153
  /**
118
154
  * Return the current EMA-smoothed tokens-per-second value.
119
155
  */
120
156
  export function drainSmoothedTps(result) {
121
- const tracker = getTpsTracker(result);
122
- return tracker.__smoothedTps;
157
+ const tracker = getTpsState(result);
158
+ return tracker.smoothedTps;
123
159
  }
124
160
 
125
161
  /**
126
- * Lazily initialize ctx baseline tracking properties on the result object.
127
- * - __ctxBaseline: last known real totalTokens from message_end
128
- * - __ctxStreamingChars: cumulative output chars since last baseline reset
162
+ * Lazily initialize ctx baseline tracking state on the result object.
163
+ * Returns an accessor object backed by a WeakMap so frozen/sealed
164
+ * objects do not throw.
129
165
  */
130
- function getCtxBaseline(result) {
131
- if (!Object.prototype.hasOwnProperty.call(result, "__ctxBaseline")) {
132
- Object.defineProperty(result, "__ctxBaseline", {
133
- value: 0,
134
- enumerable: false,
135
- configurable: false,
136
- writable: true,
137
- });
138
- Object.defineProperty(result, "__ctxStreamingChars", {
139
- value: 0,
140
- enumerable: false,
141
- configurable: false,
142
- writable: true,
143
- });
166
+ function getCtxState(result) {
167
+ if (!ctxBaselineMap.has(result)) {
168
+ ctxBaselineMap.set(result, 0);
169
+ ctxStreamingCharsMap.set(result, 0);
144
170
  }
145
- return result.__ctxBaseline;
171
+ return {
172
+ get baseline() { return ctxBaselineMap.get(result); },
173
+ set baseline(v) { ctxBaselineMap.set(result, v); },
174
+ get streamingChars() { return ctxStreamingCharsMap.get(result); },
175
+ set streamingChars(v) { ctxStreamingCharsMap.set(result, v); },
176
+ };
146
177
  }
147
178
 
148
179
  /**
@@ -154,8 +185,8 @@ function updateStreamingEstimate(result, deltaLength) {
154
185
  const est = getStreamingEstimate(result);
155
186
  est.chars += deltaLength;
156
187
  // Also accumulate chars for ctx estimation (not drained on emit)
157
- getCtxBaseline(result);
158
- result.__ctxStreamingChars += deltaLength;
188
+ const ctxState = getCtxState(result);
189
+ ctxState.streamingChars += deltaLength;
159
190
  }
160
191
 
161
192
  /**
@@ -176,9 +207,9 @@ export function drainStreamingEstimate(result) {
176
207
  * in which case the caller should fall back to the streaming output estimate.
177
208
  */
178
209
  export function drainCtxEstimate(result) {
179
- getCtxBaseline(result);
180
- const streamingTokens = Math.floor(result.__ctxStreamingChars / CHARS_PER_TOKEN);
181
- return result.__ctxBaseline + streamingTokens;
210
+ const ctxState = getCtxState(result);
211
+ const streamingTokens = Math.floor(ctxState.streamingChars / CHARS_PER_TOKEN);
212
+ return ctxState.baseline + streamingTokens;
182
213
  }
183
214
 
184
215
  /**
@@ -187,29 +218,38 @@ export function drainCtxEstimate(result) {
187
218
  */
188
219
  function accumulateStreamingDelta(result, delta) {
189
220
  if (!delta) return false;
190
- const buf = getStreamingTextBuffer(result);
191
- result.__streamingTextBuffer = buf + delta;
221
+ const state = getStreamingTextState(result);
222
+ state.buffer = state.buffer + delta;
192
223
  updateStreamingEstimate(result, delta.length);
193
- if (result.__streamingTextBuffer.length - result.__lastEmittedWordCount >= 40) {
194
- result.__lastEmittedWordCount = result.__streamingTextBuffer.length;
224
+ if (state.buffer.length - state.lastEmittedWordCount >= 40) {
225
+ state.lastEmittedWordCount = state.buffer.length;
195
226
  return true;
196
227
  }
197
228
  return false;
198
229
  }
199
230
 
200
- function stableStringify(value) {
231
+ export function stableStringify(value, seen = new WeakSet()) {
201
232
  if (value === null || typeof value !== "object") {
202
233
  return JSON.stringify(value);
203
234
  }
204
235
 
236
+ if (seen.has(value)) {
237
+ return '"[Circular]"';
238
+ }
239
+ seen.add(value);
240
+
205
241
  if (Array.isArray(value)) {
206
- return `[${value.map((item) => stableStringify(item)).join(",")}]`;
242
+ const out = `[${value.map((item) => stableStringify(item, seen)).join(",")}]`;
243
+ seen.delete(value);
244
+ return out;
207
245
  }
208
246
 
209
247
  const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
210
- return `{${entries
211
- .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`)
248
+ const out = `{${entries
249
+ .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue, seen)}`)
212
250
  .join(",")}}`;
251
+ seen.delete(value);
252
+ return out;
213
253
  }
214
254
 
215
255
  function getMessageSignature(message) {
@@ -250,27 +290,52 @@ function addFlowAssistantMessage(result, message) {
250
290
  result.usage.contextTokens = usage.totalTokens || 0;
251
291
 
252
292
  // Snapshot ctx baseline for smooth streaming estimation
253
- result.__ctxBaseline = usage.totalTokens || 0;
254
- result.__ctxStreamingChars = 0;
293
+ const ctxState = getCtxState(result);
294
+ ctxState.baseline = usage.totalTokens || 0;
295
+ ctxState.streamingChars = 0;
255
296
  }
256
297
 
257
- // Count tool call parts in the message content
298
+ // Count tool call parts in the message content and estimate their tokens
258
299
  if (Array.isArray(message.content)) {
300
+ let toolCallChars = 0;
259
301
  for (const part of message.content) {
260
302
  if (part.type === "toolCall") {
261
303
  result.usage.toolCalls++;
304
+ const tcText = JSON.stringify({ name: part.name, args: part.arguments || part.input || {} });
305
+ toolCallChars += tcText.length;
262
306
  }
263
307
  }
308
+ if (toolCallChars > 0) {
309
+ updateStreamingEstimate(result, toolCallChars);
310
+ const tracker = getTpsState(result);
311
+ tracker.pauseAfterNextEmit = true;
312
+ }
264
313
  }
265
314
 
266
315
  return true;
267
316
  }
268
317
 
269
- function addFlowAssistantMessages(result, messages) {
318
+ function addFlowToolMessage(result, message) {
319
+ if (!message || message.role !== "tool") return false;
320
+
321
+ const signature = getMessageSignature(message);
322
+ const seen = getSeenFlowMessageSignatures(result);
323
+ if (seen.has(signature)) return false;
324
+ seen.add(signature);
325
+
326
+ result.messages.push(message);
327
+ return true;
328
+ }
329
+
330
+ function addFlowMessages(result, messages) {
270
331
  if (!Array.isArray(messages)) return false;
271
332
  let changed = false;
272
333
  for (const message of messages) {
273
- if (addFlowAssistantMessage(result, message)) changed = true;
334
+ if (message && message.role === "tool") {
335
+ if (addFlowToolMessage(result, message)) changed = true;
336
+ } else if (message && message.role === "assistant") {
337
+ if (addFlowAssistantMessage(result, message)) changed = true;
338
+ }
274
339
  }
275
340
  return changed;
276
341
  }
@@ -280,14 +345,14 @@ function processFlowEvent(event, result) {
280
345
 
281
346
  switch (event.type) {
282
347
  case "message_end":
283
- return addFlowAssistantMessage(result, event.message);
348
+ return addFlowMessages(result, [event.message]);
284
349
 
285
350
  case "turn_end":
286
- return addFlowAssistantMessage(result, event.message);
351
+ return addFlowMessages(result, [event.message]);
287
352
 
288
353
  case "agent_end":
289
354
  result.sawAgentEnd = true;
290
- return addFlowAssistantMessages(result, event.messages);
355
+ return addFlowMessages(result, event.messages);
291
356
 
292
357
  case "message_update": {
293
358
  const evt = event.assistantMessageEvent;
@@ -324,7 +389,13 @@ export function getFlowFinalText(messages) {
324
389
 
325
390
  for (let i = messages.length - 1; i >= 0; i--) {
326
391
  const message = messages[i];
327
- if (!message || message.role !== "assistant" || !Array.isArray(message.content)) {
392
+ if (!message || message.role !== "assistant") {
393
+ continue;
394
+ }
395
+ if (typeof message.content === "string" && message.content.length > 0) {
396
+ return message.content;
397
+ }
398
+ if (!Array.isArray(message.content)) {
328
399
  continue;
329
400
  }
330
401
 
@@ -368,38 +439,121 @@ function formatToolCallShort(tc) {
368
439
  return `find ${args.pattern || "*"} in ${args.path || "."}`;
369
440
  case "ls":
370
441
  return `ls ${args.path || "."}`;
442
+ case "batch": {
443
+ const ops = Array.isArray(args.o) ? args.o : Array.isArray(args.op) ? args.op : Array.isArray(args.operations) ? args.operations : Array.isArray(args) ? args : [];
444
+ if (ops.length === 0) return "batch (empty)";
445
+ const first = ops[0] || {};
446
+ const firstPath = (first.p ?? first.path ?? "?").split("/").pop();
447
+ const opType = first.o ?? first.op ?? "?";
448
+ const label = ops.length === 1 ? `${opType} ${firstPath}` : `${opType} ${firstPath} +${ops.length - 1} more`;
449
+ return `batch ${label}`;
450
+ }
371
451
  default:
372
452
  return tc.name;
373
453
  }
374
454
  }
375
455
 
456
+ /**
457
+ * Match tool calls with their results to build a paired list.
458
+ * Returns [{ name, command/args, output }] limited to the most recent pairs.
459
+ */
460
+ function matchToolCallsWithResults(messages, maxPairs) {
461
+ if (!Array.isArray(messages)) return [];
462
+ const pairs = [];
463
+
464
+ // Build a map of toolCallId -> tool result output
465
+ const resultMap = new Map();
466
+ for (const msg of messages) {
467
+ if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
468
+ const id = msg.toolCallId || msg.tool_call_id || "";
469
+ if (!id) continue;
470
+ const text = msg.content
471
+ .filter((p) => p.type === "text" && typeof p.text === "string")
472
+ .map((p) => p.text)
473
+ .join("\n");
474
+ resultMap.set(id, text);
475
+ }
476
+
477
+ // Walk assistant messages to find tool calls that have matching results
478
+ for (const msg of messages) {
479
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
480
+ for (const part of msg.content) {
481
+ if (part.type !== "toolCall") continue;
482
+ const id = part.toolCallId || part.tool_call_id || "";
483
+ if (!id || !resultMap.has(id)) continue;
484
+ const name = part.name || part.toolName || "unknown";
485
+ const args = part.arguments || part.input || {};
486
+ const output = resultMap.get(id);
487
+ pairs.push({ name, args, output });
488
+ }
489
+ }
490
+
491
+ // Return the most recent pairs
492
+ return pairs.slice(-maxPairs);
493
+ }
494
+
495
+ /** Max tool result output chars to include per tool call in the summary. */
496
+ const TOOL_RESULT_MAX_CHARS = 2000;
497
+
376
498
  export function getFlowSummaryText(result) {
377
499
  const finalText = getFlowFinalText(result?.messages);
378
- if (finalText) return finalText;
379
-
380
500
  const isError =
381
501
  (typeof result?.exitCode === "number" && result.exitCode > 0) ||
382
502
  result?.stopReason === "error" ||
383
503
  result?.stopReason === "aborted";
384
504
 
385
- // Build base message for failed/aborted flows
386
- let base = "";
387
- if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
388
- base = result.errorMessage.trim();
389
- } else if (isError && typeof result?.stderr === "string" && result.stderr.trim()) {
390
- base = result.stderr.trim();
391
- } else if (isError) {
392
- base = "Flow failed";
393
- } else {
394
- return "(no output)";
505
+ // Build error base for failed flows
506
+ let errorBase = "";
507
+ if (isError) {
508
+ if (typeof result?.errorMessage === "string" && result.errorMessage.trim()) {
509
+ errorBase = result.errorMessage.trim();
510
+ } else if (typeof result?.stderr === "string" && result.stderr.trim()) {
511
+ errorBase = result.stderr.trim();
512
+ } else {
513
+ errorBase = "Flow failed";
514
+ }
515
+ }
516
+
517
+ // Extract tool call/result pairs for context
518
+ const toolPairs = matchToolCallsWithResults(result?.messages, 10);
519
+ const toolSummaryParts = [];
520
+
521
+ for (const pair of toolPairs) {
522
+ const callLabel = formatToolCallShort({ name: pair.name, args: pair.args });
523
+ if (pair.output.trim()) {
524
+ const truncated = pair.output.length > TOOL_RESULT_MAX_CHARS
525
+ ? pair.output.slice(0, TOOL_RESULT_MAX_CHARS) + "\n... (truncated)"
526
+ : pair.output;
527
+ toolSummaryParts.push(`${callLabel}:\n${truncated}`);
528
+ } else {
529
+ toolSummaryParts.push(`${callLabel}: (no output)`);
530
+ }
531
+ }
532
+
533
+ const toolContext = toolSummaryParts.length > 0
534
+ ? "\n\n[Tool Results]\n" + toolSummaryParts.join("\n---\n")
535
+ : "";
536
+
537
+ // If there's final text, include it plus tool context
538
+ if (finalText) {
539
+ return finalText + toolContext;
540
+ }
541
+
542
+ // No final text
543
+ if (isError) {
544
+ // Surface partial tool calls (excluding read) for failed/aborted flows
545
+ const toolCalls = extractNonReadToolCalls(result?.messages);
546
+ if (toolCalls.length > 0) {
547
+ const formatted = toolCalls.map(formatToolCallShort).join(", ");
548
+ return `${errorBase}\nPartial work: ${formatted}${toolContext}`;
549
+ }
550
+ return errorBase;
395
551
  }
396
552
 
397
- // Surface partial tool calls (excluding read) for failed/aborted flows
398
- const toolCalls = extractNonReadToolCalls(result?.messages);
399
- if (toolCalls.length > 0) {
400
- const formatted = toolCalls.map(formatToolCallShort).join(", ");
401
- return `${base}\nPartial work: ${formatted}`;
553
+ // Success but no final text show tool results if any
554
+ if (toolContext) {
555
+ return toolContext.trim();
402
556
  }
403
557
 
404
- return base;
558
+ return "(no output)";
405
559
  }
package/types.ts CHANGED
@@ -21,8 +21,9 @@ export interface UsageStats {
21
21
  /** Result of a single flow invocation. */
22
22
  export interface SingleResult {
23
23
  type: string;
24
- agentSource: "user" | "project" | "unknown";
24
+ agentSource: "user" | "project" | "bundled" | "unknown";
25
25
  intent: string;
26
+ aim: string;
26
27
  exitCode: number;
27
28
  messages: Message[];
28
29
  stderr: string;
@@ -155,7 +156,7 @@ export function getFlowDisplayItems(messages: Message[]): DisplayItem[] {
155
156
  }
156
157
 
157
158
  /** Extract the last tool call from message history. */
158
- export function getLastToolCall(messages: Message[]): DisplayItem | undefined {
159
+ export function getLastToolCall(messages: Message[]): { type: "toolCall"; name: string; args: Record<string, unknown> } | undefined {
159
160
  for (let i = messages.length - 1; i >= 0; i--) {
160
161
  const msg = messages[i];
161
162
  if (msg.role === "assistant") {