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/README.md +2 -2
- package/agents/audit.md +36 -0
- package/agents/{code.md → build.md} +10 -4
- package/agents/craft.md +37 -0
- package/agents/debug.md +2 -1
- package/agents/{brainstorm.md → ideas.md} +4 -4
- package/agents/scout.md +31 -0
- package/agents.ts +5 -8
- package/batch.ts +1083 -0
- package/config.ts +35 -0
- package/flow.ts +84 -18
- package/hooks.ts +31 -13
- package/index.ts +126 -28
- package/package.json +11 -3
- package/render-utils.ts +3 -3
- package/render.ts +55 -24
- package/runner-events.js +280 -126
- package/types.ts +3 -2
- package/web-tool.ts +663 -0
- package/agents/architect.md +0 -35
- package/agents/explore.md +0 -29
- package/agents/review.md +0 -35
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 (!
|
|
7
|
-
|
|
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
|
|
21
|
+
return seenSignaturesMap.get(result);
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
function
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
42
|
+
const state = getStreamingTextState(result);
|
|
43
|
+
const buf = state.buffer;
|
|
41
44
|
if (!buf) return "";
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
64
|
+
const EMA_ALPHA = 0.1;
|
|
56
65
|
|
|
57
66
|
function getStreamingEstimate(result) {
|
|
58
|
-
if (!
|
|
59
|
-
|
|
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
|
|
70
|
+
return streamingEstimateMap.get(result);
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
|
-
* Lazily initialize TPS tracking
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
75
|
-
if (!
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 =
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
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 =
|
|
122
|
-
return tracker.
|
|
157
|
+
const tracker = getTpsState(result);
|
|
158
|
+
return tracker.smoothedTps;
|
|
123
159
|
}
|
|
124
160
|
|
|
125
161
|
/**
|
|
126
|
-
* Lazily initialize ctx baseline tracking
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
|
131
|
-
if (!
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
180
|
-
const streamingTokens = Math.floor(
|
|
181
|
-
return
|
|
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
|
|
191
|
-
|
|
221
|
+
const state = getStreamingTextState(result);
|
|
222
|
+
state.buffer = state.buffer + delta;
|
|
192
223
|
updateStreamingEstimate(result, delta.length);
|
|
193
|
-
if (
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
|
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 (
|
|
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
|
|
348
|
+
return addFlowMessages(result, [event.message]);
|
|
284
349
|
|
|
285
350
|
case "turn_end":
|
|
286
|
-
return
|
|
351
|
+
return addFlowMessages(result, [event.message]);
|
|
287
352
|
|
|
288
353
|
case "agent_end":
|
|
289
354
|
result.sawAgentEnd = true;
|
|
290
|
-
return
|
|
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"
|
|
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
|
|
386
|
-
let
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
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[]):
|
|
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") {
|