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.
Files changed (44) hide show
  1. package/CLAUDE.md +48 -0
  2. package/dist/concepts/Conditional.js +2 -2
  3. package/dist/concepts/FlowChart.js +5 -4
  4. package/dist/concepts/FlowChart.js.map +1 -1
  5. package/dist/concepts/Parallel.js +3 -1
  6. package/dist/concepts/Parallel.js.map +1 -1
  7. package/dist/concepts/Swarm.js +4 -1
  8. package/dist/concepts/Swarm.js.map +1 -1
  9. package/dist/esm/concepts/Conditional.js +2 -2
  10. package/dist/esm/concepts/FlowChart.js +5 -4
  11. package/dist/esm/concepts/FlowChart.js.map +1 -1
  12. package/dist/esm/concepts/Parallel.js +3 -1
  13. package/dist/esm/concepts/Parallel.js.map +1 -1
  14. package/dist/esm/concepts/Swarm.js +4 -1
  15. package/dist/esm/concepts/Swarm.js.map +1 -1
  16. package/dist/esm/index.js +1 -1
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/lib/concepts/AgentRunner.js +1 -1
  19. package/dist/esm/observe.barrel.js.map +1 -1
  20. package/dist/esm/recorders/AgentTimelineRecorder.js +673 -74
  21. package/dist/esm/recorders/AgentTimelineRecorder.js.map +1 -1
  22. package/dist/index.js +2 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/lib/concepts/AgentRunner.js +1 -1
  25. package/dist/observe.barrel.js.map +1 -1
  26. package/dist/recorders/AgentTimelineRecorder.js +672 -73
  27. package/dist/recorders/AgentTimelineRecorder.js.map +1 -1
  28. package/dist/types/concepts/Conditional.d.ts +2 -2
  29. package/dist/types/concepts/FlowChart.d.ts +5 -4
  30. package/dist/types/concepts/FlowChart.d.ts.map +1 -1
  31. package/dist/types/concepts/Parallel.d.ts +3 -1
  32. package/dist/types/concepts/Parallel.d.ts.map +1 -1
  33. package/dist/types/concepts/Swarm.d.ts +4 -1
  34. package/dist/types/concepts/Swarm.d.ts.map +1 -1
  35. package/dist/types/index.d.ts +2 -2
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/lib/concepts/AgentRunner.d.ts +1 -1
  38. package/dist/types/observe.barrel.d.ts +1 -1
  39. package/dist/types/observe.barrel.d.ts.map +1 -1
  40. package/dist/types/recorders/AgentTimelineRecorder.d.ts +268 -39
  41. package/dist/types/recorders/AgentTimelineRecorder.d.ts.map +1 -1
  42. package/dist/types/recorders/index.d.ts +1 -1
  43. package/dist/types/recorders/index.d.ts.map +1 -1
  44. 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
- * t.getTimeline(); // AgentTimeline { turns, messages, tools, ... }
38
- * t.getEntryRanges(); // O(1) per-step range index for sliders
39
- * t.aggregate(...); // reduce all entries
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
- * Single entry point: every emit event the executor dispatches passes
62
- * through here. Translates the event into a TimelineEntry and stores
63
- * it via `SequenceRecorder.emit()`. Unknown events are silently
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
- // ── Derived view ────────────────────────────────────────────────────
86
- /**
87
- * Fold the recorder's flat entry sequence into the agent-shaped
88
- * AgentTimeline. Pure derivation — same input always produces same
89
- * output. Cheap because entry count is bounded by run length.
90
- */
91
- getTimeline() {
92
- return foldEntries(this.getEntries(), { id: this.id, name: this.name });
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, agent, deriveSubs) {
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 (!currentTurn)
288
- continue;
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
- function foldEntries(entries, agent) {
447
- return foldCore(entries, agent, true);
448
- }
624
+ // ── Slice helpers — used by selectors ─────────────────────────────────
449
625
  /**
450
- * Group timeline entries by their `subflowPath[0]` (the top-level
451
- * sub-agent name) and fold each group into its own SubAgentTimeline.
452
- *
453
- * Single-agent runs have `subflowPath = []` on every event → produces
454
- * empty array (UIs render single container).
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
- * Multi-agent runs (Pipeline / Swarm) events from each sub-agent
457
- * fire with `subflowPath = ["classify"]`, `["analyze"]`, etc. — one
458
- * SubAgentTimeline per distinct first segment.
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
- * Each sub-agent slice is its own self-contained timeline (re-folded
461
- * from the subset of entries belonging to it). The OUTER timeline
462
- * still contains everything; sub-agents are an additional view, not
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
- function deriveSubAgents(entries) {
466
- // Group by subflowPath[0]. Skip entries with empty subflowPath
467
- // (those belong to the root parent agent, not a sub-agent).
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 entry of entries) {
470
- const subAgentId = entry.subflowPath[0];
471
- if (!subAgentId)
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(subAgentId) ?? [];
474
- arr.push(entry);
475
- bySubAgent.set(subAgentId, arr);
692
+ const arr = bySubAgent.get(id) ?? [];
693
+ arr.push(e);
694
+ bySubAgent.set(id, arr);
476
695
  }
477
- if (bySubAgent.size === 0)
478
- return [];
479
- // Find the parent's user message — sub-agent slices don't get their
480
- // own turn_start (the parent owns the conversation), but the fold
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
- // Fold each group with `deriveSubs=false` to short-circuit recursion.
486
- // Sub-agents inherit the subflow id as their name — upstream wiring
487
- // (Agent.create({ name }) on sub-agents) can carry the real human
488
- // name through future enhancements.
489
- const out = [];
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: subEntries[0]?.timestamp ?? Date.now(),
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 subTimeline = foldCore([synthTurnStart, ...subEntries], { id, name: id }, false);
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
- id,
501
- name: id,
502
- turns: subTimeline.turns,
503
- tools: subTimeline.tools,
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