agentfootprint 1.22.0 → 1.23.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/CLAUDE.md +48 -0
- package/dist/concepts/Conditional.js +2 -2
- package/dist/concepts/FlowChart.js +5 -4
- package/dist/concepts/FlowChart.js.map +1 -1
- package/dist/concepts/Parallel.js +3 -1
- package/dist/concepts/Parallel.js.map +1 -1
- package/dist/concepts/Swarm.js +4 -1
- package/dist/concepts/Swarm.js.map +1 -1
- package/dist/esm/concepts/Conditional.js +2 -2
- package/dist/esm/concepts/FlowChart.js +5 -4
- package/dist/esm/concepts/FlowChart.js.map +1 -1
- package/dist/esm/concepts/Parallel.js +3 -1
- package/dist/esm/concepts/Parallel.js.map +1 -1
- package/dist/esm/concepts/Swarm.js +4 -1
- package/dist/esm/concepts/Swarm.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/concepts/AgentRunner.js +1 -1
- package/dist/esm/observe.barrel.js.map +1 -1
- package/dist/esm/recorders/AgentTimelineRecorder.js +673 -74
- package/dist/esm/recorders/AgentTimelineRecorder.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/concepts/AgentRunner.js +1 -1
- package/dist/observe.barrel.js.map +1 -1
- package/dist/recorders/AgentTimelineRecorder.js +672 -73
- package/dist/recorders/AgentTimelineRecorder.js.map +1 -1
- package/dist/types/concepts/Conditional.d.ts +2 -2
- package/dist/types/concepts/FlowChart.d.ts +5 -4
- package/dist/types/concepts/FlowChart.d.ts.map +1 -1
- package/dist/types/concepts/Parallel.d.ts +3 -1
- package/dist/types/concepts/Parallel.d.ts.map +1 -1
- package/dist/types/concepts/Swarm.d.ts +4 -1
- package/dist/types/concepts/Swarm.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/concepts/AgentRunner.d.ts +1 -1
- package/dist/types/observe.barrel.d.ts +1 -1
- package/dist/types/observe.barrel.d.ts.map +1 -1
- package/dist/types/recorders/AgentTimelineRecorder.d.ts +268 -39
- package/dist/types/recorders/AgentTimelineRecorder.d.ts.map +1 -1
- package/dist/types/recorders/index.d.ts +1 -1
- package/dist/types/recorders/index.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -34,9 +34,14 @@
|
|
|
34
34
|
*
|
|
35
35
|
* await agent.run('Investigate port errors on switch-3');
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
* t.
|
|
39
|
-
* t.
|
|
37
|
+
* // v2 API — call selectors, not a pre-shaped bundle
|
|
38
|
+
* t.selectTurns(); // AgentTurn[] — iterations with tool calls + context
|
|
39
|
+
* t.selectActivities(); // Activity[] — humanized breadcrumb list (ThinkKit)
|
|
40
|
+
* t.selectStatus(); // StatusLine — typing-bubble one-liner
|
|
41
|
+
* t.selectCommentary(); // CommentaryLine[] — human narrative per event
|
|
42
|
+
* t.selectTopology(); // Topology — composition graph for flowchart view
|
|
43
|
+
* t.selectRunSummary(); // RunSummary — tokens, tools, duration totals
|
|
44
|
+
* t.setHumanizer({ describeToolStart: ... }); // swap domain phrasings
|
|
40
45
|
* ```
|
|
41
46
|
*
|
|
42
47
|
* Multi-agent: each sub-agent in a Pipeline/Swarm gets its own named
|
|
@@ -44,25 +49,70 @@
|
|
|
44
49
|
* its own `executor.getSnapshot().recorders[id]` slot. Multi-agent
|
|
45
50
|
* shells aggregate them by id.
|
|
46
51
|
*/
|
|
47
|
-
import { SequenceRecorder } from 'footprintjs/trace';
|
|
52
|
+
import { SequenceRecorder, TopologyRecorder } from 'footprintjs/trace';
|
|
53
|
+
/**
|
|
54
|
+
* AgentTimelineRecorder v2 — event stream + selectors + humanizer.
|
|
55
|
+
*
|
|
56
|
+
* THE ARCHITECTURE (one shape, many renderers):
|
|
57
|
+
*
|
|
58
|
+
* EVENT STREAM (structured, canonical — single source of truth)
|
|
59
|
+
* ↓
|
|
60
|
+
* SELECTORS (typed, memoized, lazy, composable — THE API)
|
|
61
|
+
* ↓
|
|
62
|
+
* VIEWS (React / Vue / Angular / CLI / Grafana / replay)
|
|
63
|
+
*
|
|
64
|
+
* Consumers never reshape data themselves — they call selectors. New
|
|
65
|
+
* renderer view? Add a selector. Never add pre-computed fields to some
|
|
66
|
+
* timeline blob (that's the anti-pattern this design replaces).
|
|
67
|
+
*
|
|
68
|
+
* OBSERVER CHANNELS (from footprintjs):
|
|
69
|
+
*
|
|
70
|
+
* 1. EmitRecorder — `agentfootprint.stream.*` / `.context.*`
|
|
71
|
+
* events translate into the `AgentEvent` stream.
|
|
72
|
+
* 2. FlowRecorder — subflow / fork / decision / loop events
|
|
73
|
+
* forwarded to a composed `TopologyRecorder`.
|
|
74
|
+
* 3. SequenceRecorder<AgentEvent> base — storage + O(1) per-step lookup.
|
|
75
|
+
*
|
|
76
|
+
* Footprintjs's `attachCombinedRecorder` detects which methods this
|
|
77
|
+
* recorder implements and routes events accordingly. Consumers attach
|
|
78
|
+
* once; all three channels wire up automatically.
|
|
79
|
+
*
|
|
80
|
+
* HUMANIZATION: swap `setHumanizer(custom)` to override generic phrasings
|
|
81
|
+
* ("Running toolName") with domain-specific strings ("Checking port status
|
|
82
|
+
* on switch-3"). The library NEVER bakes rendered strings into events —
|
|
83
|
+
* they appear only at selector time, through the humanizer, so translation,
|
|
84
|
+
* localization, and UX tone changes don't require data-model changes.
|
|
85
|
+
*/
|
|
48
86
|
export class AgentTimelineRecorder extends SequenceRecorder {
|
|
49
87
|
id;
|
|
50
88
|
name;
|
|
51
89
|
/** True between an iter's llm_start and llm_end. Drives context-event
|
|
52
90
|
* routing (THIS iter vs NEXT iter). */
|
|
53
91
|
llmPhaseActive = false;
|
|
92
|
+
/** Composed topology accumulator — selectors use this for subflow/
|
|
93
|
+
* fork/decision queries. Private; consumers query via `selectTopology()`. */
|
|
94
|
+
topology;
|
|
95
|
+
/** Active humanizer. Starts as the default; consumer swaps via
|
|
96
|
+
* `setHumanizer`. */
|
|
97
|
+
humanizer = {};
|
|
98
|
+
/**
|
|
99
|
+
* Memoization version — incremented on every `emit()` and `clear()`.
|
|
100
|
+
* Selector results are keyed by `(selectorName, version, cursor)`. A
|
|
101
|
+
* long run renders many frames without recomputing unchanged views.
|
|
102
|
+
*/
|
|
103
|
+
version = 0;
|
|
104
|
+
cache = new Map();
|
|
54
105
|
constructor(options) {
|
|
55
106
|
super();
|
|
56
107
|
this.id = options?.id ?? 'agentfootprint-agent-timeline';
|
|
57
108
|
this.name = options?.name ?? 'Agent';
|
|
109
|
+
this.topology = new TopologyRecorder({ id: `${this.id}-topology` });
|
|
58
110
|
}
|
|
59
111
|
// ── EmitRecorder ─────────────────────────────────────────────────────
|
|
60
112
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* ignored — the executor delivers events from many subsystems and we
|
|
65
|
-
* only care about agent-shaped ones.
|
|
113
|
+
* Translate the incoming emit event into an `AgentEvent` and append to
|
|
114
|
+
* the stream. Events not in the agent shape are silently dropped —
|
|
115
|
+
* executors deliver events from many subsystems.
|
|
66
116
|
*/
|
|
67
117
|
onEmit(event) {
|
|
68
118
|
const entry = translate(event, this.llmPhaseActive);
|
|
@@ -73,23 +123,142 @@ export class AgentTimelineRecorder extends SequenceRecorder {
|
|
|
73
123
|
if (entry.type === 'llm_end' || entry.type === 'turn_end') {
|
|
74
124
|
this.llmPhaseActive = false;
|
|
75
125
|
}
|
|
76
|
-
// SequenceRecorder.emit is protected — fine, we're a subclass.
|
|
77
126
|
this.emit(entry);
|
|
127
|
+
this.bumpVersion();
|
|
128
|
+
}
|
|
129
|
+
// ── FlowRecorder hooks (forward to composed TopologyRecorder) ────────
|
|
130
|
+
onSubflowEntry(event) {
|
|
131
|
+
this.topology.onSubflowEntry(event);
|
|
132
|
+
this.bumpVersion();
|
|
133
|
+
}
|
|
134
|
+
onSubflowExit(event) {
|
|
135
|
+
this.topology.onSubflowExit(event);
|
|
136
|
+
this.bumpVersion();
|
|
137
|
+
}
|
|
138
|
+
onFork(event) {
|
|
139
|
+
this.topology.onFork(event);
|
|
140
|
+
this.bumpVersion();
|
|
141
|
+
}
|
|
142
|
+
onDecision(event) {
|
|
143
|
+
this.topology.onDecision(event);
|
|
144
|
+
this.bumpVersion();
|
|
145
|
+
}
|
|
146
|
+
onLoop(event) {
|
|
147
|
+
this.topology.onLoop(event);
|
|
148
|
+
this.bumpVersion();
|
|
78
149
|
}
|
|
79
|
-
/** Reset state. Called automatically by the executor before each
|
|
80
|
-
* `run()` (recorder-pattern lifecycle hook). */
|
|
81
150
|
clear() {
|
|
82
151
|
super.clear();
|
|
83
152
|
this.llmPhaseActive = false;
|
|
153
|
+
this.topology.clear();
|
|
154
|
+
this.bumpVersion();
|
|
84
155
|
}
|
|
85
|
-
// ──
|
|
86
|
-
/**
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
156
|
+
// ── Raw event-stream access ──────────────────────────────────────────
|
|
157
|
+
/** The canonical event stream. Most consumers call selectors instead;
|
|
158
|
+
* this is for low-level tools (custom renderers, debug bundles). */
|
|
159
|
+
getEvents() {
|
|
160
|
+
return this.getEntries();
|
|
161
|
+
}
|
|
162
|
+
/** Direct access to the composed topology recorder for consumers that
|
|
163
|
+
* need the full composition graph (fork-branches, decision-branches,
|
|
164
|
+
* edges). Equivalent for rendering: use `selectTopology()`. */
|
|
165
|
+
getTopology() {
|
|
166
|
+
return this.topology;
|
|
167
|
+
}
|
|
168
|
+
// ── Humanizer ────────────────────────────────────────────────────────
|
|
169
|
+
/** Override or replace the active humanizer. Invalidates memoized
|
|
170
|
+
* selector results so the next read re-phrases. */
|
|
171
|
+
setHumanizer(humanizer) {
|
|
172
|
+
this.humanizer = humanizer;
|
|
173
|
+
this.bumpVersion();
|
|
174
|
+
}
|
|
175
|
+
/** Returns the currently-active humanizer (consumer-supplied). */
|
|
176
|
+
getHumanizer() {
|
|
177
|
+
return this.humanizer;
|
|
178
|
+
}
|
|
179
|
+
// ── Selectors (memoized, lazy, the API) ──────────────────────────────
|
|
180
|
+
/** Agent identity — { id, name } from constructor options. */
|
|
181
|
+
selectAgent() {
|
|
182
|
+
return { id: this.id, name: this.name };
|
|
183
|
+
}
|
|
184
|
+
/** All turns with their iterations, tool calls, context injections. */
|
|
185
|
+
selectTurns() {
|
|
186
|
+
return this.memo('turns', () => foldTurns(this.getEvents()));
|
|
187
|
+
}
|
|
188
|
+
/** Message list mirroring `sharedState.messages` (reconstructed from
|
|
189
|
+
* `turn_start.userMessage` + `llm_end.content` + assistant tool calls
|
|
190
|
+
* + `tool_end.result`). */
|
|
191
|
+
selectMessages() {
|
|
192
|
+
return this.memo('messages', () => foldMessages(this.getEvents()));
|
|
193
|
+
}
|
|
194
|
+
/** All tool invocations across all turns, in chronological order. */
|
|
195
|
+
selectTools() {
|
|
196
|
+
return this.memo('tools', () => foldTools(this.getEvents()));
|
|
197
|
+
}
|
|
198
|
+
/** Sub-agent slices for multi-agent runs. Identity from the composed
|
|
199
|
+
* topology's subflow nodes; per-sub-agent content (turns, tools) folded
|
|
200
|
+
* from emit events tagged with matching `subflowPath[0]`. Empty for
|
|
201
|
+
* single-agent runs. */
|
|
202
|
+
selectSubAgents() {
|
|
203
|
+
return this.memo('subAgents', () => deriveSubAgentSlices(this.getEvents(), this.topology));
|
|
204
|
+
}
|
|
205
|
+
/** Final decision object captured from `agentfootprint.agent.turn_complete`. */
|
|
206
|
+
selectFinalDecision() {
|
|
207
|
+
return this.memo('finalDecision', () => foldFinalDecision(this.getEvents()));
|
|
208
|
+
}
|
|
209
|
+
/** Composition graph snapshot. Renderers use `nodes`/`edges` for layout. */
|
|
210
|
+
selectTopology() {
|
|
211
|
+
// Topology has its own internal state; snapshot once per query.
|
|
212
|
+
return this.topology.getTopology();
|
|
213
|
+
}
|
|
214
|
+
/** Event-reduced activity list for status/progress renderers (ThinkKit).
|
|
215
|
+
* Humanized labels. Optional cursor: only include events up to that
|
|
216
|
+
* index (progressive reveal / time-travel scrubbing). */
|
|
217
|
+
selectActivities(cursor) {
|
|
218
|
+
const key = cursor === undefined ? 'activities:all' : `activities:${cursor}`;
|
|
219
|
+
return this.memo(key, () => reduceActivities(this.getEvents(), this.humanizer, cursor));
|
|
220
|
+
}
|
|
221
|
+
/** Single-line current status — for typing bubbles / "now running…" pills.
|
|
222
|
+
* Cursor defaults to the latest event. */
|
|
223
|
+
selectStatus(cursor) {
|
|
224
|
+
const key = cursor === undefined ? 'status:latest' : `status:${cursor}`;
|
|
225
|
+
return this.memo(key, () => deriveStatus(this.getEvents(), this.humanizer, cursor));
|
|
226
|
+
}
|
|
227
|
+
/** Human-readable narrative — one line per significant event. For
|
|
228
|
+
* analyst-style commentary panels. Humanized. */
|
|
229
|
+
selectCommentary(cursor) {
|
|
230
|
+
const key = cursor === undefined ? 'commentary:all' : `commentary:${cursor}`;
|
|
231
|
+
return this.memo(key, () => buildCommentary(this.getEvents(), this.humanizer, cursor));
|
|
232
|
+
}
|
|
233
|
+
/** Numeric totals — turn count, token usage, duration, tool usage. */
|
|
234
|
+
selectRunSummary() {
|
|
235
|
+
return this.memo('runSummary', () => computeRunSummary(this.getEvents()));
|
|
236
|
+
}
|
|
237
|
+
/** Context-engineering injection summary — per slot, grouped by source
|
|
238
|
+
* (rag / skill / memory / instructions / custom). Powers slot-row badges
|
|
239
|
+
* on the flowchart AND the analyst Commentary panel. Cursor-aware:
|
|
240
|
+
* pass an event index to see injections up to that point. */
|
|
241
|
+
selectContextBySource(cursor) {
|
|
242
|
+
const key = cursor === undefined ? 'contextBySource:all' : `contextBySource:${cursor}`;
|
|
243
|
+
return this.memo(key, () => computeContextBySource(this.getEvents(), cursor));
|
|
244
|
+
}
|
|
245
|
+
/** Iteration ↔ event-stream index map for scrubbers / time-travel. */
|
|
246
|
+
selectIterationRanges() {
|
|
247
|
+
return this.memo('iterationRanges', () => computeIterationRanges(this.getEvents()));
|
|
248
|
+
}
|
|
249
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
250
|
+
bumpVersion() {
|
|
251
|
+
this.version++;
|
|
252
|
+
this.cache.clear();
|
|
253
|
+
}
|
|
254
|
+
memo(key, compute) {
|
|
255
|
+
const cacheKey = `${key}@${this.version}`;
|
|
256
|
+
const cached = this.cache.get(cacheKey);
|
|
257
|
+
if (cached !== undefined)
|
|
258
|
+
return cached;
|
|
259
|
+
const value = compute();
|
|
260
|
+
this.cache.set(cacheKey, value);
|
|
261
|
+
return value;
|
|
93
262
|
}
|
|
94
263
|
}
|
|
95
264
|
/**
|
|
@@ -256,7 +425,7 @@ function maybeUsage(u) {
|
|
|
256
425
|
out.outputTokens = x.outputTokens;
|
|
257
426
|
return out;
|
|
258
427
|
}
|
|
259
|
-
function foldCore(entries
|
|
428
|
+
function foldCore(entries) {
|
|
260
429
|
const turns = [];
|
|
261
430
|
const messages = [];
|
|
262
431
|
const toolByCallId = new Map();
|
|
@@ -284,8 +453,23 @@ function foldCore(entries, agent, deriveSubs) {
|
|
|
284
453
|
break;
|
|
285
454
|
}
|
|
286
455
|
case 'llm_start': {
|
|
287
|
-
if
|
|
288
|
-
|
|
456
|
+
// Synthesize a turn anchor if none exists. Real executors call
|
|
457
|
+
// recorder.clear() at run-start, wiping any turn_start that
|
|
458
|
+
// arrived via observe() BEFORE run. Rather than forcing every
|
|
459
|
+
// caller to emit turn_start on the emit channel, we recover
|
|
460
|
+
// here: first llm_start without a turn creates a synthetic one.
|
|
461
|
+
if (!currentTurn) {
|
|
462
|
+
currentTurn = {
|
|
463
|
+
index: turns.length,
|
|
464
|
+
userPrompt: '',
|
|
465
|
+
iterations: [],
|
|
466
|
+
finalContent: '',
|
|
467
|
+
totalInputTokens: 0,
|
|
468
|
+
totalOutputTokens: 0,
|
|
469
|
+
totalDurationMs: 0,
|
|
470
|
+
};
|
|
471
|
+
turns.push(currentTurn);
|
|
472
|
+
}
|
|
289
473
|
currentIter = {
|
|
290
474
|
index: entry.iteration,
|
|
291
475
|
assistantContent: '',
|
|
@@ -431,78 +615,493 @@ function foldCore(entries, agent, deriveSubs) {
|
|
|
431
615
|
};
|
|
432
616
|
});
|
|
433
617
|
return {
|
|
434
|
-
agent,
|
|
435
618
|
turns: frozenTurns,
|
|
436
619
|
messages: [...messages],
|
|
437
620
|
tools: allTools,
|
|
438
621
|
finalDecision: {},
|
|
439
|
-
// `subAgents` is the multi-agent dimension. When the caller is
|
|
440
|
-
// `foldEntries` (the public path), we derive it. When the caller
|
|
441
|
-
// is `deriveSubAgents` (already inside a sub-agent fold), we don't
|
|
442
|
-
// recurse — passing `deriveSubs: false` short-circuits the recursion.
|
|
443
|
-
subAgents: deriveSubs ? deriveSubAgents(entries) : [],
|
|
444
622
|
};
|
|
445
623
|
}
|
|
446
|
-
|
|
447
|
-
return foldCore(entries, agent, true);
|
|
448
|
-
}
|
|
624
|
+
// ── Slice helpers — used by selectors ─────────────────────────────────
|
|
449
625
|
/**
|
|
450
|
-
*
|
|
451
|
-
* sub-agent
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
626
|
+
* Multi-agent slices: the topology supplies sub-agent identity (id, name);
|
|
627
|
+
* per-sub-agent content (turns, tools) is folded from emit events whose
|
|
628
|
+
* `subflowPath[0]` matches the node's id. Both sources come from the same
|
|
629
|
+
* executor traversal — topology via FlowRecorder events, subflowPath via
|
|
630
|
+
* emit-event metadata — but it's the recorder's job to stitch them.
|
|
455
631
|
*
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
|
|
632
|
+
* For sub-agents that have no matching emit events (e.g. a plain-stage
|
|
633
|
+
* fork child), turns/tools are empty arrays.
|
|
634
|
+
*/
|
|
635
|
+
/**
|
|
636
|
+
* An Agent's signature — the set of API-slot subflows every Agent
|
|
637
|
+
* mounts via `buildAgentLoop`. A topology subflow that CONTAINS any of
|
|
638
|
+
* these as a child is an Agent wrapper (and therefore a sub-agent when
|
|
639
|
+
* nested inside a composition runner). Subflows that ARE one of these
|
|
640
|
+
* slots themselves, or other leaf subflows not containing a slot, are
|
|
641
|
+
* internal structure — not sub-agents.
|
|
459
642
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
* a replacement.
|
|
643
|
+
* This heuristic replaces a hardcoded deny-list. Robust against new
|
|
644
|
+
* internal-agent subflows added later (they'll auto-classify as
|
|
645
|
+
* "internal" because they don't wrap slots).
|
|
464
646
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
647
|
+
const AGENT_SLOT_SUBFLOW_IDS = new Set([
|
|
648
|
+
'sf-system-prompt',
|
|
649
|
+
'sf-messages',
|
|
650
|
+
'sf-tools',
|
|
651
|
+
]);
|
|
652
|
+
/** TopologyRecorder disambiguates re-entered subflows with a `#n`
|
|
653
|
+
* suffix. Strip that suffix so a re-entered slot still matches the
|
|
654
|
+
* signature set. */
|
|
655
|
+
function baseSubflowId(id) {
|
|
656
|
+
const hashIdx = id.indexOf('#');
|
|
657
|
+
return hashIdx === -1 ? id : id.slice(0, hashIdx);
|
|
658
|
+
}
|
|
659
|
+
function isAgentWrapper(nodeId, topology) {
|
|
660
|
+
// A sub-agent is a subflow whose descendants in the topology tree
|
|
661
|
+
// include at least one of the API-slot signature subflows.
|
|
662
|
+
const stack = [...topology.getChildren(nodeId)];
|
|
663
|
+
while (stack.length > 0) {
|
|
664
|
+
const child = stack.pop();
|
|
665
|
+
if (AGENT_SLOT_SUBFLOW_IDS.has(baseSubflowId(child.id)))
|
|
666
|
+
return true;
|
|
667
|
+
stack.push(...topology.getChildren(child.id));
|
|
668
|
+
}
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
function deriveSubAgentSlices(events, topology) {
|
|
672
|
+
const allNodes = topology.getSubflowNodes();
|
|
673
|
+
// Keep only subflows that WRAP an Agent (have API-slot descendants).
|
|
674
|
+
// In single-agent runs, the slots are top-level — nothing wraps them
|
|
675
|
+
// → empty array → Lens renders the single-agent flowchart.
|
|
676
|
+
// In multi-agent (Pipeline/Parallel/Swarm/Conditional), each sub-agent
|
|
677
|
+
// root wraps its own slots → returned as a sub-agent.
|
|
678
|
+
const nodes = allNodes.filter((n) => isAgentWrapper(n.id, topology));
|
|
679
|
+
if (nodes.length === 0)
|
|
680
|
+
return [];
|
|
681
|
+
// Group events by first subflowPath segment. Keep only events whose
|
|
682
|
+
// top-of-path IS one of the classified sub-agents (filter out events
|
|
683
|
+
// belonging to internal-agent subflows of the root agent, which are
|
|
684
|
+
// technically at subflowPath[0] in single-agent runs but aren't
|
|
685
|
+
// sub-agents).
|
|
686
|
+
const subAgentIds = new Set(nodes.map((n) => n.id));
|
|
468
687
|
const bySubAgent = new Map();
|
|
469
|
-
for (const
|
|
470
|
-
const
|
|
471
|
-
if (!
|
|
688
|
+
for (const e of events) {
|
|
689
|
+
const id = e.subflowPath[0];
|
|
690
|
+
if (!id || !subAgentIds.has(id))
|
|
472
691
|
continue;
|
|
473
|
-
const arr = bySubAgent.get(
|
|
474
|
-
arr.push(
|
|
475
|
-
bySubAgent.set(
|
|
692
|
+
const arr = bySubAgent.get(id) ?? [];
|
|
693
|
+
arr.push(e);
|
|
694
|
+
bySubAgent.set(id, arr);
|
|
476
695
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
// requires one to initialize `currentTurn`. Synthesize one prepended
|
|
482
|
-
// to each sub-agent's entries so the fold has a turn anchor.
|
|
483
|
-
const parentTurnStart = entries.find((e) => e.type === 'turn_start');
|
|
696
|
+
// Synthesize a turn_start so the fold has a turn anchor (sub-agents
|
|
697
|
+
// inherit the parent's conversation; they don't emit their own
|
|
698
|
+
// turn_start events).
|
|
699
|
+
const parentTurnStart = events.find((e) => e.type === 'turn_start');
|
|
484
700
|
const parentUserMessage = parentTurnStart?.type === 'turn_start' ? parentTurnStart.userMessage : '';
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
for (const [id, subEntries] of bySubAgent) {
|
|
701
|
+
return nodes.map((node) => {
|
|
702
|
+
const subEvents = bySubAgent.get(node.id);
|
|
703
|
+
if (!subEvents || subEvents.length === 0) {
|
|
704
|
+
return { id: node.id, name: node.name, turns: [], tools: [] };
|
|
705
|
+
}
|
|
491
706
|
const synthTurnStart = {
|
|
492
707
|
type: 'turn_start',
|
|
493
|
-
runtimeStageId: `synth:turn_start:${id}`,
|
|
494
|
-
timestamp:
|
|
495
|
-
subflowPath: [id],
|
|
708
|
+
runtimeStageId: `synth:turn_start:${node.id}`,
|
|
709
|
+
timestamp: subEvents[0]?.timestamp ?? Date.now(),
|
|
710
|
+
subflowPath: [node.id],
|
|
496
711
|
userMessage: parentUserMessage,
|
|
497
712
|
};
|
|
498
|
-
const
|
|
713
|
+
const bundle = foldCore([synthTurnStart, ...subEvents]);
|
|
714
|
+
return { id: node.id, name: node.name, turns: bundle.turns, tools: bundle.tools };
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function foldTurns(events) {
|
|
718
|
+
return foldCore(events).turns;
|
|
719
|
+
}
|
|
720
|
+
function foldMessages(events) {
|
|
721
|
+
return foldCore(events).messages;
|
|
722
|
+
}
|
|
723
|
+
function foldTools(events) {
|
|
724
|
+
return foldCore(events).tools;
|
|
725
|
+
}
|
|
726
|
+
function foldFinalDecision(events) {
|
|
727
|
+
return foldCore(events).finalDecision;
|
|
728
|
+
}
|
|
729
|
+
// ── Default humanizer — generic phrasings for every event kind ────────
|
|
730
|
+
//
|
|
731
|
+
// Consumer-supplied `Humanizer` overrides win. Each describeXxx returns
|
|
732
|
+
// a short phrase ready for Activity.label / StatusLine.text.
|
|
733
|
+
function defaultTurnStart() {
|
|
734
|
+
return 'Getting started';
|
|
735
|
+
}
|
|
736
|
+
function defaultTurnEnd() {
|
|
737
|
+
return 'Done';
|
|
738
|
+
}
|
|
739
|
+
function defaultLLMStart() {
|
|
740
|
+
return 'Thinking';
|
|
741
|
+
}
|
|
742
|
+
function defaultLLMEnd(e) {
|
|
743
|
+
return e.toolCallCount > 0
|
|
744
|
+
? `Running ${e.toolCallCount} step${e.toolCallCount === 1 ? '' : 's'}`
|
|
745
|
+
: 'Writing response';
|
|
746
|
+
}
|
|
747
|
+
function defaultToolStart(e) {
|
|
748
|
+
return `Running ${e.toolName}`;
|
|
749
|
+
}
|
|
750
|
+
function defaultToolEnd(e) {
|
|
751
|
+
return e.error ? 'Tool errored' : 'Got result';
|
|
752
|
+
}
|
|
753
|
+
function defaultContextInjection(e) {
|
|
754
|
+
return `${e.source} → ${e.slot}`;
|
|
755
|
+
}
|
|
756
|
+
/** Pick the humanizer's phrasing or fall through to the default. */
|
|
757
|
+
function humanize(custom, fallback, e) {
|
|
758
|
+
if (custom) {
|
|
759
|
+
const r = custom(e);
|
|
760
|
+
if (typeof r === 'string')
|
|
761
|
+
return r;
|
|
762
|
+
}
|
|
763
|
+
return fallback(e);
|
|
764
|
+
}
|
|
765
|
+
// ── Activity reduction state machine ──────────────────────────────────
|
|
766
|
+
//
|
|
767
|
+
// `selectActivities()` uses this to turn the event stream into an
|
|
768
|
+
// ordered breadcrumb list {id, label, done, meta, kind}. The humanizer
|
|
769
|
+
// shapes every user-visible string.
|
|
770
|
+
function reduceActivities(events, h, cursor) {
|
|
771
|
+
const end = cursor === undefined ? events.length : Math.min(cursor + 1, events.length);
|
|
772
|
+
const out = [];
|
|
773
|
+
const toolIdxByCallId = new Map();
|
|
774
|
+
let llmIdx = null;
|
|
775
|
+
for (let i = 0; i < end; i++) {
|
|
776
|
+
const e = events[i];
|
|
777
|
+
switch (e.type) {
|
|
778
|
+
case 'llm_start': {
|
|
779
|
+
out.push({
|
|
780
|
+
id: `llm-${e.iteration}-${i}`,
|
|
781
|
+
label: humanize(h.describeLLMStart, defaultLLMStart, e),
|
|
782
|
+
done: false,
|
|
783
|
+
kind: 'llm',
|
|
784
|
+
runtimeStageId: e.runtimeStageId,
|
|
785
|
+
iterationIndex: e.iteration,
|
|
786
|
+
});
|
|
787
|
+
llmIdx = out.length - 1;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case 'llm_end': {
|
|
791
|
+
if (llmIdx !== null) {
|
|
792
|
+
out[llmIdx] = {
|
|
793
|
+
...out[llmIdx],
|
|
794
|
+
done: true,
|
|
795
|
+
meta: humanize(h.describeLLMEnd, defaultLLMEnd, e),
|
|
796
|
+
};
|
|
797
|
+
llmIdx = null;
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case 'tool_start': {
|
|
802
|
+
out.push({
|
|
803
|
+
id: e.toolCallId || `tool-${i}`,
|
|
804
|
+
label: humanize(h.describeToolStart, defaultToolStart, e),
|
|
805
|
+
done: false,
|
|
806
|
+
kind: 'tool',
|
|
807
|
+
runtimeStageId: e.runtimeStageId,
|
|
808
|
+
});
|
|
809
|
+
toolIdxByCallId.set(e.toolCallId || `tool-${i}`, out.length - 1);
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
case 'tool_end': {
|
|
813
|
+
const idx = toolIdxByCallId.get(e.toolCallId);
|
|
814
|
+
if (idx !== undefined) {
|
|
815
|
+
const prev = out[idx];
|
|
816
|
+
// Look up the tool name from the matching start event for the humanizer.
|
|
817
|
+
const toolName = findToolNameForCallId(events, e.toolCallId) ?? '';
|
|
818
|
+
out[idx] = {
|
|
819
|
+
...prev,
|
|
820
|
+
done: true,
|
|
821
|
+
meta: humanize(h.describeToolEnd, defaultToolEnd, {
|
|
822
|
+
toolName,
|
|
823
|
+
result: e.result,
|
|
824
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
825
|
+
}),
|
|
826
|
+
};
|
|
827
|
+
toolIdxByCallId.delete(e.toolCallId);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'turn_start':
|
|
832
|
+
case 'turn_end':
|
|
833
|
+
case 'context_injection':
|
|
834
|
+
default:
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
function findToolNameForCallId(events, toolCallId) {
|
|
841
|
+
for (const e of events) {
|
|
842
|
+
if (e.type === 'tool_start' && e.toolCallId === toolCallId)
|
|
843
|
+
return e.toolName;
|
|
844
|
+
}
|
|
845
|
+
return undefined;
|
|
846
|
+
}
|
|
847
|
+
// ── Status one-liner ──────────────────────────────────────────────────
|
|
848
|
+
function deriveStatus(events, h, cursor) {
|
|
849
|
+
const end = cursor === undefined ? events.length - 1 : Math.min(cursor, events.length - 1);
|
|
850
|
+
if (end < 0) {
|
|
851
|
+
return {
|
|
852
|
+
text: humanize(h.describeTurnStart, defaultTurnStart, { userMessage: '' }),
|
|
853
|
+
kind: 'idle',
|
|
854
|
+
eventIndex: -1,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
const e = events[end];
|
|
858
|
+
switch (e.type) {
|
|
859
|
+
case 'turn_start':
|
|
860
|
+
return {
|
|
861
|
+
text: humanize(h.describeTurnStart, defaultTurnStart, e),
|
|
862
|
+
kind: 'turn',
|
|
863
|
+
eventIndex: end,
|
|
864
|
+
};
|
|
865
|
+
case 'turn_end':
|
|
866
|
+
return {
|
|
867
|
+
text: humanize(h.describeTurnEnd, defaultTurnEnd, e),
|
|
868
|
+
kind: 'turn',
|
|
869
|
+
eventIndex: end,
|
|
870
|
+
};
|
|
871
|
+
case 'llm_start':
|
|
872
|
+
return {
|
|
873
|
+
text: humanize(h.describeLLMStart, defaultLLMStart, e),
|
|
874
|
+
kind: 'llm',
|
|
875
|
+
eventIndex: end,
|
|
876
|
+
};
|
|
877
|
+
case 'llm_end':
|
|
878
|
+
return { text: humanize(h.describeLLMEnd, defaultLLMEnd, e), kind: 'llm', eventIndex: end };
|
|
879
|
+
case 'tool_start':
|
|
880
|
+
return {
|
|
881
|
+
text: humanize(h.describeToolStart, defaultToolStart, e),
|
|
882
|
+
kind: 'tool',
|
|
883
|
+
eventIndex: end,
|
|
884
|
+
};
|
|
885
|
+
case 'tool_end': {
|
|
886
|
+
const toolName = findToolNameForCallId(events, e.toolCallId) ?? '';
|
|
887
|
+
return {
|
|
888
|
+
text: humanize(h.describeToolEnd, defaultToolEnd, {
|
|
889
|
+
toolName,
|
|
890
|
+
result: e.result,
|
|
891
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
892
|
+
}),
|
|
893
|
+
kind: 'tool',
|
|
894
|
+
eventIndex: end,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
default:
|
|
898
|
+
return { text: '', kind: 'idle', eventIndex: end };
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// ── Commentary builder — one narrative line per event ─────────────────
|
|
902
|
+
function buildCommentary(events, h, cursor) {
|
|
903
|
+
const end = cursor === undefined ? events.length : Math.min(cursor + 1, events.length);
|
|
904
|
+
const out = [];
|
|
905
|
+
for (let i = 0; i < end; i++) {
|
|
906
|
+
const e = events[i];
|
|
907
|
+
let text = '';
|
|
908
|
+
let kind = 'llm';
|
|
909
|
+
switch (e.type) {
|
|
910
|
+
case 'turn_start':
|
|
911
|
+
text = humanize(h.describeTurnStart, defaultTurnStart, e);
|
|
912
|
+
kind = 'turn';
|
|
913
|
+
break;
|
|
914
|
+
case 'turn_end':
|
|
915
|
+
text = humanize(h.describeTurnEnd, defaultTurnEnd, e);
|
|
916
|
+
kind = 'turn';
|
|
917
|
+
break;
|
|
918
|
+
case 'llm_start':
|
|
919
|
+
text = humanize(h.describeLLMStart, defaultLLMStart, e);
|
|
920
|
+
kind = 'llm';
|
|
921
|
+
break;
|
|
922
|
+
case 'llm_end':
|
|
923
|
+
text = humanize(h.describeLLMEnd, defaultLLMEnd, e);
|
|
924
|
+
kind = 'llm';
|
|
925
|
+
break;
|
|
926
|
+
case 'tool_start':
|
|
927
|
+
text = humanize(h.describeToolStart, defaultToolStart, e);
|
|
928
|
+
kind = 'tool';
|
|
929
|
+
break;
|
|
930
|
+
case 'tool_end': {
|
|
931
|
+
const toolName = findToolNameForCallId(events, e.toolCallId) ?? '';
|
|
932
|
+
text = humanize(h.describeToolEnd, defaultToolEnd, {
|
|
933
|
+
toolName,
|
|
934
|
+
result: e.result,
|
|
935
|
+
...(e.error !== undefined ? { error: e.error } : {}),
|
|
936
|
+
});
|
|
937
|
+
kind = 'tool';
|
|
938
|
+
break;
|
|
939
|
+
}
|
|
940
|
+
case 'context_injection':
|
|
941
|
+
text = humanize(h.describeContextInjection, defaultContextInjection, e);
|
|
942
|
+
kind = 'context';
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
if (text === '')
|
|
946
|
+
continue;
|
|
499
947
|
out.push({
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
948
|
+
text,
|
|
949
|
+
kind,
|
|
950
|
+
...(e.runtimeStageId !== undefined ? { runtimeStageId: e.runtimeStageId } : {}),
|
|
951
|
+
timestamp: e.timestamp,
|
|
504
952
|
});
|
|
505
953
|
}
|
|
506
954
|
return out;
|
|
507
955
|
}
|
|
956
|
+
// ── Run summary totals ────────────────────────────────────────────────
|
|
957
|
+
function computeRunSummary(events) {
|
|
958
|
+
let turnCount = 0;
|
|
959
|
+
let iterationCount = 0;
|
|
960
|
+
let toolCallCount = 0;
|
|
961
|
+
let inputTokens = 0;
|
|
962
|
+
let outputTokens = 0;
|
|
963
|
+
let totalDurationMs = 0;
|
|
964
|
+
const toolUsage = new Map();
|
|
965
|
+
const skillsActivated = new Set();
|
|
966
|
+
const toolStarts = new Map(); // toolCallId → toolName
|
|
967
|
+
for (const e of events) {
|
|
968
|
+
switch (e.type) {
|
|
969
|
+
case 'turn_start':
|
|
970
|
+
turnCount++;
|
|
971
|
+
break;
|
|
972
|
+
case 'llm_start':
|
|
973
|
+
iterationCount++;
|
|
974
|
+
break;
|
|
975
|
+
case 'llm_end':
|
|
976
|
+
if (e.inputTokens !== undefined)
|
|
977
|
+
inputTokens += e.inputTokens;
|
|
978
|
+
if (e.outputTokens !== undefined)
|
|
979
|
+
outputTokens += e.outputTokens;
|
|
980
|
+
if (e.durationMs !== undefined)
|
|
981
|
+
totalDurationMs += e.durationMs;
|
|
982
|
+
break;
|
|
983
|
+
case 'tool_start':
|
|
984
|
+
toolCallCount++;
|
|
985
|
+
toolStarts.set(e.toolCallId, e.toolName);
|
|
986
|
+
if (e.toolName === 'read_skill' && typeof e.args.id === 'string') {
|
|
987
|
+
skillsActivated.add(e.args.id);
|
|
988
|
+
}
|
|
989
|
+
break;
|
|
990
|
+
case 'tool_end': {
|
|
991
|
+
const name = toolStarts.get(e.toolCallId);
|
|
992
|
+
if (!name)
|
|
993
|
+
break;
|
|
994
|
+
const prev = toolUsage.get(name) ?? { count: 0, totalDurationMs: 0 };
|
|
995
|
+
toolUsage.set(name, {
|
|
996
|
+
count: prev.count + 1,
|
|
997
|
+
totalDurationMs: prev.totalDurationMs + (e.durationMs ?? 0),
|
|
998
|
+
});
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
turnCount,
|
|
1005
|
+
iterationCount,
|
|
1006
|
+
toolCallCount,
|
|
1007
|
+
inputTokens,
|
|
1008
|
+
outputTokens,
|
|
1009
|
+
totalDurationMs,
|
|
1010
|
+
toolUsage: Object.fromEntries(toolUsage),
|
|
1011
|
+
skillsActivated: [...skillsActivated],
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
// ── Context-engineering summary ───────────────────────────────────────
|
|
1015
|
+
function computeContextBySource(events, cursor) {
|
|
1016
|
+
const end = cursor === undefined ? events.length : Math.min(cursor + 1, events.length);
|
|
1017
|
+
const slotOrder = ['system-prompt', 'messages', 'tools'];
|
|
1018
|
+
const slotBuckets = new Map();
|
|
1019
|
+
const aggregatedLedger = {};
|
|
1020
|
+
for (const slot of slotOrder)
|
|
1021
|
+
slotBuckets.set(slot, new Map());
|
|
1022
|
+
for (let i = 0; i < end; i++) {
|
|
1023
|
+
const e = events[i];
|
|
1024
|
+
if (e.type !== 'context_injection')
|
|
1025
|
+
continue;
|
|
1026
|
+
const slot = e.slot;
|
|
1027
|
+
const bucket = slotBuckets.get(slot);
|
|
1028
|
+
if (!bucket)
|
|
1029
|
+
continue;
|
|
1030
|
+
const group = bucket.get(e.source) ?? { count: 0, deltaCount: {}, labels: [] };
|
|
1031
|
+
group.count += 1;
|
|
1032
|
+
group.labels.push(e.label);
|
|
1033
|
+
if (e.deltaCount) {
|
|
1034
|
+
for (const [k, v] of Object.entries(e.deltaCount)) {
|
|
1035
|
+
if (typeof v === 'number') {
|
|
1036
|
+
group.deltaCount[k] = (group.deltaCount[k] ?? 0) + v;
|
|
1037
|
+
aggregatedLedger[k] = (aggregatedLedger[k] ?? 0) + v;
|
|
1038
|
+
}
|
|
1039
|
+
else if (typeof v === 'boolean') {
|
|
1040
|
+
// Boolean flags: true wins (indicates feature was active at least once).
|
|
1041
|
+
group.deltaCount[k] = group.deltaCount[k] || v;
|
|
1042
|
+
aggregatedLedger[k] = aggregatedLedger[k] || v;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
bucket.set(e.source, group);
|
|
1047
|
+
}
|
|
1048
|
+
const slots = slotOrder.map((slot) => {
|
|
1049
|
+
const bucket = slotBuckets.get(slot);
|
|
1050
|
+
const sources = [];
|
|
1051
|
+
let totalInjections = 0;
|
|
1052
|
+
for (const [source, group] of bucket) {
|
|
1053
|
+
sources.push({
|
|
1054
|
+
source,
|
|
1055
|
+
count: group.count,
|
|
1056
|
+
deltaCount: group.deltaCount,
|
|
1057
|
+
labels: group.labels,
|
|
1058
|
+
});
|
|
1059
|
+
totalInjections += group.count;
|
|
1060
|
+
}
|
|
1061
|
+
return { slot, sources, totalInjections };
|
|
1062
|
+
});
|
|
1063
|
+
return { slots, aggregatedLedger };
|
|
1064
|
+
}
|
|
1065
|
+
// ── Iteration ↔ event-stream range index ──────────────────────────────
|
|
1066
|
+
function computeIterationRanges(events) {
|
|
1067
|
+
const iterations = [];
|
|
1068
|
+
const byEventIndex = [];
|
|
1069
|
+
let turnIndex = -1;
|
|
1070
|
+
let currentIterStart = -1;
|
|
1071
|
+
let currentIter = null;
|
|
1072
|
+
const flushCurrent = (endIdx) => {
|
|
1073
|
+
if (currentIter === null || currentIterStart < 0)
|
|
1074
|
+
return;
|
|
1075
|
+
iterations.push({
|
|
1076
|
+
turnIndex,
|
|
1077
|
+
iterationIndex: currentIter,
|
|
1078
|
+
firstEventIndex: currentIterStart,
|
|
1079
|
+
lastEventIndex: endIdx,
|
|
1080
|
+
...(events[currentIterStart]?.runtimeStageId !== undefined
|
|
1081
|
+
? { runtimeStageId: events[currentIterStart].runtimeStageId }
|
|
1082
|
+
: {}),
|
|
1083
|
+
});
|
|
1084
|
+
for (let i = currentIterStart; i <= endIdx; i++)
|
|
1085
|
+
byEventIndex[i] = iterations.length - 1;
|
|
1086
|
+
};
|
|
1087
|
+
for (let i = 0; i < events.length; i++) {
|
|
1088
|
+
const e = events[i];
|
|
1089
|
+
if (e.type === 'turn_start') {
|
|
1090
|
+
flushCurrent(i - 1);
|
|
1091
|
+
currentIter = null;
|
|
1092
|
+
currentIterStart = -1;
|
|
1093
|
+
turnIndex++;
|
|
1094
|
+
byEventIndex[i] = iterations.length; // belongs to the next iteration slot
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
1097
|
+
if (e.type === 'llm_start') {
|
|
1098
|
+
flushCurrent(i - 1);
|
|
1099
|
+
currentIter = e.iteration;
|
|
1100
|
+
currentIterStart = i;
|
|
1101
|
+
}
|
|
1102
|
+
byEventIndex[i] = iterations.length; // pending iteration
|
|
1103
|
+
}
|
|
1104
|
+
flushCurrent(events.length - 1);
|
|
1105
|
+
return { iterations, byEventIndex };
|
|
1106
|
+
}
|
|
508
1107
|
//# sourceMappingURL=AgentTimelineRecorder.js.map
|