agentfootprint 6.22.0 → 6.24.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 (92) hide show
  1. package/AGENTS.md +17 -0
  2. package/CLAUDE.md +17 -0
  3. package/README.md +58 -0
  4. package/dist/core/Agent.js +109 -18
  5. package/dist/core/Agent.js.map +1 -1
  6. package/dist/core/agent/stages/toolCalls.js +19 -4
  7. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  8. package/dist/esm/core/Agent.js +109 -18
  9. package/dist/esm/core/Agent.js.map +1 -1
  10. package/dist/esm/core/agent/stages/toolCalls.js +19 -4
  11. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/lib/influence-core/cache.js +149 -0
  14. package/dist/esm/lib/influence-core/cache.js.map +1 -0
  15. package/dist/esm/lib/influence-core/index.js +32 -0
  16. package/dist/esm/lib/influence-core/index.js.map +1 -0
  17. package/dist/esm/lib/influence-core/margin.js +110 -0
  18. package/dist/esm/lib/influence-core/margin.js.map +1 -0
  19. package/dist/esm/lib/influence-core/signals.js +232 -0
  20. package/dist/esm/lib/influence-core/signals.js.map +1 -0
  21. package/dist/esm/lib/influence-core/similarity.js +79 -0
  22. package/dist/esm/lib/influence-core/similarity.js.map +1 -0
  23. package/dist/esm/lib/influence-core/types.js +35 -0
  24. package/dist/esm/lib/influence-core/types.js.map +1 -0
  25. package/dist/esm/lib/trace-toolpack/bounded.js +76 -0
  26. package/dist/esm/lib/trace-toolpack/bounded.js.map +1 -0
  27. package/dist/esm/lib/trace-toolpack/index.js +10 -0
  28. package/dist/esm/lib/trace-toolpack/index.js.map +1 -0
  29. package/dist/esm/lib/trace-toolpack/traceToolpack.js +699 -0
  30. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -0
  31. package/dist/esm/lib/trace-toolpack/types.js +24 -0
  32. package/dist/esm/lib/trace-toolpack/types.js.map +1 -0
  33. package/dist/esm/observe.js +12 -0
  34. package/dist/esm/observe.js.map +1 -1
  35. package/dist/esm/recorders/core/typedEmit.js +26 -0
  36. package/dist/esm/recorders/core/typedEmit.js.map +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/influence-core/cache.js +155 -0
  39. package/dist/lib/influence-core/cache.js.map +1 -0
  40. package/dist/lib/influence-core/index.js +50 -0
  41. package/dist/lib/influence-core/index.js.map +1 -0
  42. package/dist/lib/influence-core/margin.js +114 -0
  43. package/dist/lib/influence-core/margin.js.map +1 -0
  44. package/dist/lib/influence-core/signals.js +242 -0
  45. package/dist/lib/influence-core/signals.js.map +1 -0
  46. package/dist/lib/influence-core/similarity.js +83 -0
  47. package/dist/lib/influence-core/similarity.js.map +1 -0
  48. package/dist/lib/influence-core/types.js +38 -0
  49. package/dist/lib/influence-core/types.js.map +1 -0
  50. package/dist/lib/trace-toolpack/bounded.js +86 -0
  51. package/dist/lib/trace-toolpack/bounded.js.map +1 -0
  52. package/dist/lib/trace-toolpack/index.js +16 -0
  53. package/dist/lib/trace-toolpack/index.js.map +1 -0
  54. package/dist/lib/trace-toolpack/traceToolpack.js +704 -0
  55. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -0
  56. package/dist/lib/trace-toolpack/types.js +28 -0
  57. package/dist/lib/trace-toolpack/types.js.map +1 -0
  58. package/dist/observe.js +31 -1
  59. package/dist/observe.js.map +1 -1
  60. package/dist/recorders/core/typedEmit.js +26 -0
  61. package/dist/recorders/core/typedEmit.js.map +1 -1
  62. package/dist/types/core/Agent.d.ts +40 -3
  63. package/dist/types/core/Agent.d.ts.map +1 -1
  64. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  65. package/dist/types/core/agent/types.d.ts +61 -1
  66. package/dist/types/core/agent/types.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +2 -1
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/dist/types/lib/influence-core/cache.d.ts +95 -0
  70. package/dist/types/lib/influence-core/cache.d.ts.map +1 -0
  71. package/dist/types/lib/influence-core/index.d.ts +33 -0
  72. package/dist/types/lib/influence-core/index.d.ts.map +1 -0
  73. package/dist/types/lib/influence-core/margin.d.ts +34 -0
  74. package/dist/types/lib/influence-core/margin.d.ts.map +1 -0
  75. package/dist/types/lib/influence-core/signals.d.ts +104 -0
  76. package/dist/types/lib/influence-core/signals.d.ts.map +1 -0
  77. package/dist/types/lib/influence-core/similarity.d.ts +26 -0
  78. package/dist/types/lib/influence-core/similarity.d.ts.map +1 -0
  79. package/dist/types/lib/influence-core/types.d.ts +158 -0
  80. package/dist/types/lib/influence-core/types.d.ts.map +1 -0
  81. package/dist/types/lib/trace-toolpack/bounded.d.ts +48 -0
  82. package/dist/types/lib/trace-toolpack/bounded.d.ts.map +1 -0
  83. package/dist/types/lib/trace-toolpack/index.d.ts +10 -0
  84. package/dist/types/lib/trace-toolpack/index.d.ts.map +1 -0
  85. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts +70 -0
  86. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -0
  87. package/dist/types/lib/trace-toolpack/types.d.ts +60 -0
  88. package/dist/types/lib/trace-toolpack/types.d.ts.map +1 -0
  89. package/dist/types/observe.d.ts +2 -0
  90. package/dist/types/observe.d.ts.map +1 -1
  91. package/dist/types/recorders/core/typedEmit.d.ts.map +1 -1
  92. package/package.json +3 -3
@@ -0,0 +1,699 @@
1
+ /**
2
+ * traceToolpack — footprintjs trace evidence exposed as TOOLS an LLM calls
3
+ * (RFC-003 Part C: the introspection toolpack).
4
+ *
5
+ * "The framework's internal tool for itself": after a run completes, a
6
+ * debugging LLM (a cheap model in a SEPARATE session) navigates the run's
7
+ * evidence by ids instead of reading dumps — the same just-in-time,
8
+ * token-efficient loading pattern as `read_skill`. Feed the slice, not the
9
+ * trace; the LLM ranks by navigating, so no embedder is needed.
10
+ *
11
+ * Pattern: Factory over frozen artifacts. `traceToolpack(artifacts)` returns
12
+ * plain `Tool[]` — mount them on any Agent, or drive them scripted
13
+ * via `callTraceTool` (the offline auditor pattern, like
14
+ * examples/features/20). Nothing re-runs; every tool is a bounded
15
+ * read-only VIEW over a COMPLETED run's snapshot + commit log.
16
+ *
17
+ * The toolpack's three contracts (B13 posture):
18
+ *
19
+ * 1. BOUNDED BY DEFAULT — every output is capped (previews, slice
20
+ * depth/nodes, value chars, narrative lines). Per-call params raise
21
+ * the budget only up to hard caps the LLM cannot exceed.
22
+ * 2. HONEST — truncation and incompleteness are ALWAYS marked (⚠), never
23
+ * silent: truncated slices, untracked sources (args/env/silent reads),
24
+ * missing read tracking, missing control-dependence lookup, values the
25
+ * commit log cannot see (pre-run state, closures).
26
+ * 3. REDACTION-RESPECTING — the commit log already carries redacted
27
+ * payloads (footprintjs scrubs at commit time); the toolpack passes
28
+ * placeholders through verbatim and flags redacted keys. It never
29
+ * reconstructs around a redaction.
30
+ *
31
+ * Why ids: every view names steps by `runtimeStageId`
32
+ * (`stageId#executionIndex`) — the universal key linking the commit log,
33
+ * the execution tree, and recorder events. The LLM drills like a debugger:
34
+ * overview → slice → node → value, paying only for what it opens.
35
+ */
36
+ import { causalChain, commitValueAt, findLastWriter, formatCausalChain } from 'footprintjs/trace';
37
+ import { formatToolArgIssues, validateToolArgs } from '../../core/agent/toolArgsValidation.js';
38
+ import { defineTool } from '../../core/tools.js';
39
+ import { unconfiguredCredentialProvider } from '../../identity/types.js';
40
+ import { boundedPreview, clampParam, displayKey, displayText, normalizeKey, renderPreview, safeStringify, } from './bounded.js';
41
+ import { resolveToolpackOptions, TOOLPACK_HARD_CAPS, } from './types.js';
42
+ // ── Display caps that are structural (not consumer-tunable) ───────────────
43
+ const OVERVIEW_STAGE_CAP = 40;
44
+ const OVERVIEW_KEY_CAP = 40;
45
+ const OVERVIEW_ERROR_CAP = 10;
46
+ const OVERVIEW_DESCRIPTION_CAP = 140;
47
+ const NODE_WRITE_CAP = 20;
48
+ const NODE_READ_CAP = 30;
49
+ const NARRATIVE_LINE_CHAR_CAP = 400;
50
+ const UNKNOWN_ID_SUGGESTION_CAP = 8;
51
+ const KEY_SUGGESTION_CAP = 12;
52
+ /** Schemas embed an `enum` of valid ids/keys only when the set is small —
53
+ * free #9 validation without bloating the tools block on long runs. */
54
+ const SCHEMA_ENUM_CAP = 48;
55
+ function stagePartOf(runtimeStageId) {
56
+ const hash = runtimeStageId.lastIndexOf('#');
57
+ return hash > 0 ? runtimeStageId.slice(0, hash) : runtimeStageId;
58
+ }
59
+ function buildIndex(artifacts) {
60
+ const commitLog = artifacts.snapshot.commitLog ?? [];
61
+ const firstIdxOf = new Map();
62
+ const lastIdxOf = new Map();
63
+ const bundlesOf = new Map();
64
+ const knownPaths = new Set();
65
+ let untrackedStepCount = 0;
66
+ for (let i = 0; i < commitLog.length; i++) {
67
+ const bundle = commitLog[i];
68
+ const id = bundle.runtimeStageId;
69
+ if (!firstIdxOf.has(id))
70
+ firstIdxOf.set(id, i);
71
+ lastIdxOf.set(id, i);
72
+ const list = bundlesOf.get(id);
73
+ if (list)
74
+ list.push(bundle);
75
+ else {
76
+ bundlesOf.set(id, [bundle]);
77
+ if (bundle.untrackedSources && bundle.untrackedSources.length > 0)
78
+ untrackedStepCount++;
79
+ }
80
+ for (const entry of bundle.trace)
81
+ knownPaths.add(entry.path);
82
+ }
83
+ // Walk the execution tree: node → children → next (≈ execution order).
84
+ const nodes = new Map();
85
+ const orderedIds = [];
86
+ const errorSteps = [];
87
+ let hasReadTracking = false;
88
+ const visit = (node) => {
89
+ if (!node)
90
+ return;
91
+ const id = node.runtimeStageId;
92
+ if (id && !nodes.has(id)) {
93
+ nodes.set(id, node);
94
+ orderedIds.push(id);
95
+ if (node.stageReads && Object.keys(node.stageReads).length > 0)
96
+ hasReadTracking = true;
97
+ const errorKeys = Object.keys(node.errors ?? {});
98
+ if (errorKeys.length > 0)
99
+ errorSteps.push({ id, keys: errorKeys });
100
+ }
101
+ for (const child of node.children ?? [])
102
+ visit(child);
103
+ visit(node.next);
104
+ };
105
+ visit(artifacts.snapshot.executionTree);
106
+ // Steps present in the commit log but missing from the tree (defensive).
107
+ for (const id of bundlesOf.keys()) {
108
+ if (!nodes.has(id))
109
+ orderedIds.push(id);
110
+ }
111
+ const idsByStagePart = new Map();
112
+ const groupsByStagePart = new Map();
113
+ const groups = [];
114
+ for (const id of orderedIds) {
115
+ const stagePart = stagePartOf(id);
116
+ const ids = idsByStagePart.get(stagePart);
117
+ if (ids)
118
+ ids.push(id);
119
+ else
120
+ idsByStagePart.set(stagePart, [id]);
121
+ let group = groupsByStagePart.get(stagePart);
122
+ if (!group) {
123
+ group = { stagePart, ids: [], isDecider: false, isSubflow: false, errorIds: [] };
124
+ groupsByStagePart.set(stagePart, group);
125
+ groups.push(group);
126
+ }
127
+ group.ids.push(id);
128
+ const node = nodes.get(id);
129
+ if (node) {
130
+ if (group.name === undefined && node.name !== undefined)
131
+ group.name = node.name;
132
+ if (group.description === undefined && node.description !== undefined) {
133
+ group.description = node.description;
134
+ }
135
+ if (node.isDecider)
136
+ group.isDecider = true;
137
+ if (node.subflowId !== undefined)
138
+ group.isSubflow = true;
139
+ if (Object.keys(node.errors ?? {}).length > 0)
140
+ group.errorIds.push(id);
141
+ }
142
+ else {
143
+ const bundle = bundlesOf.get(id)?.[0];
144
+ if (group.name === undefined && bundle)
145
+ group.name = bundle.stage;
146
+ }
147
+ }
148
+ return {
149
+ commitLog,
150
+ firstIdxOf,
151
+ lastIdxOf,
152
+ bundlesOf,
153
+ nodes,
154
+ orderedIds,
155
+ idsByStagePart,
156
+ knownPaths,
157
+ groups,
158
+ hasReadTracking,
159
+ errorSteps,
160
+ untrackedStepCount,
161
+ };
162
+ }
163
+ // ── Shared message helpers ─────────────────────────────────────────────────
164
+ function unknownIdMessage(id, index) {
165
+ const stagePart = stagePartOf(id);
166
+ const siblings = index.idsByStagePart.get(stagePart);
167
+ if (siblings && siblings.length > 0) {
168
+ return (`unknown runtimeStageId '${id}'. Stage '${stagePart}' has ${siblings.length} ` +
169
+ `execution(s): ${siblings.slice(0, UNKNOWN_ID_SUGGESTION_CAP).join(', ')}` +
170
+ (siblings.length > UNKNOWN_ID_SUGGESTION_CAP ? ', …' : '') +
171
+ `. Retry with one of those ids.`);
172
+ }
173
+ const sample = index.orderedIds.slice(0, UNKNOWN_ID_SUGGESTION_CAP).join(', ');
174
+ return (`unknown runtimeStageId '${id}'. Ids look like stageId#executionIndex` +
175
+ (sample
176
+ ? ` — known steps include: ${sample}${index.orderedIds.length > UNKNOWN_ID_SUGGESTION_CAP ? ', …' : ''}`
177
+ : '') +
178
+ `. Call run_overview to list every stage.`);
179
+ }
180
+ function unknownKeySuffix(index) {
181
+ if (index.knownPaths.size === 0)
182
+ return '';
183
+ const sample = [...index.knownPaths].slice(0, KEY_SUGGESTION_CAP).map(displayKey).join(', ');
184
+ return ` Known keys include: ${sample}${index.knownPaths.size > KEY_SUGGESTION_CAP ? ', …' : ''}.`;
185
+ }
186
+ /** Is `path` redacted in any bundle of this step (or, keyless, of the writer)? */
187
+ function redactionNote(bundles, path) {
188
+ const redacted = (bundles ?? []).some((b) => b.redactedPaths.includes(path));
189
+ return redacted ? ' (redacted by policy)' : '';
190
+ }
191
+ /** Union of untracked sources across a step's bundles. */
192
+ function untrackedSourcesOf(bundles) {
193
+ const set = new Set();
194
+ for (const bundle of bundles ?? []) {
195
+ for (const source of bundle.untrackedSources ?? [])
196
+ set.add(source);
197
+ }
198
+ return [...set];
199
+ }
200
+ function truncateText(text, cap) {
201
+ return text.length > cap ? `${text.slice(0, cap)}…` : text;
202
+ }
203
+ // ── The factory ────────────────────────────────────────────────────────────
204
+ /**
205
+ * Build the introspection toolpack over a COMPLETED run's artifacts.
206
+ *
207
+ * Returns plain `Tool[]`:
208
+ *
209
+ * | Tool | Question it answers |
210
+ * |------------------|-----------------------------------------------------------|
211
+ * | `run_overview` | What happened, broadly? (the entry point) |
212
+ * | `trace_node` | What did step X read/write, and where did its inputs come from? |
213
+ * | `trace_slice` | Which chain of steps produced the data at X? (causal slice) |
214
+ * | `who_wrote` | Which step last wrote key K? |
215
+ * | `get_value` | The full value of K as of step X (capped, truncation-marked) |
216
+ * | `read_narrative` | The human-readable story, paginated (only when narrative provided) |
217
+ *
218
+ * Mount on an Agent (`Agent.create({...}).tool(...tools)`) or drive scripted
219
+ * via {@link callTraceTool}. The tools NEVER throw on bad ids/keys — they
220
+ * return corrective, model-visible messages (the #9 philosophy), and their
221
+ * strict input schemas give Agent-dispatched calls free arg validation.
222
+ *
223
+ * Security note (B13 posture): trace content can carry adversarial text from
224
+ * the original run (tool results, user input). Serve these tools to a
225
+ * SEPARATE debugging session over completed runs — not to the production
226
+ * agent mid-run — and treat tool outputs as data, not instructions.
227
+ */
228
+ export function traceToolpack(artifacts, options) {
229
+ const opts = resolveToolpackOptions(options);
230
+ const index = buildIndex(artifacts);
231
+ const tools = [
232
+ buildRunOverview(artifacts, index),
233
+ buildTraceNode(artifacts, index, opts),
234
+ buildTraceSlice(artifacts, index, opts),
235
+ buildWhoWrote(index, opts),
236
+ buildGetValue(index, opts),
237
+ ];
238
+ if (artifacts.narrative !== undefined) {
239
+ tools.push(buildReadNarrative(artifacts.narrative));
240
+ }
241
+ return tools;
242
+ }
243
+ // ── Schema fragments ───────────────────────────────────────────────────────
244
+ function idProperty(index, description) {
245
+ const property = { type: 'string', description };
246
+ if (index.orderedIds.length > 0 && index.orderedIds.length <= SCHEMA_ENUM_CAP) {
247
+ property.enum = index.orderedIds;
248
+ }
249
+ return property;
250
+ }
251
+ /**
252
+ * Key params get guidance (known keys in the description) but NO enum:
253
+ * unlike step ids — whose universe is complete — a key OUTSIDE the commit
254
+ * log is a legitimate question with an honest answer ("args/env/pre-run
255
+ * values never enter the commit log ⚠"), and an enum would block that path.
256
+ */
257
+ function keyProperty(index, description) {
258
+ let hint = '';
259
+ if (index.knownPaths.size > 0 && index.knownPaths.size <= SCHEMA_ENUM_CAP) {
260
+ hint = ` Tracked keys: ${[...index.knownPaths].map(displayKey).join(', ')}.`;
261
+ }
262
+ return { type: 'string', description: `${description}${hint}` };
263
+ }
264
+ // ── run_overview ───────────────────────────────────────────────────────────
265
+ function buildRunOverview(artifacts, index) {
266
+ return defineTool({
267
+ name: 'run_overview',
268
+ description: 'Start here. One bounded summary of the completed run: status, step counts, every ' +
269
+ 'stage (id + name + description), loop counts, where errors appeared, and honesty notes. ' +
270
+ "Step ids look like 'stageId#executionIndex' (e.g. 'normalize#0'); every other trace " +
271
+ 'tool accepts them.',
272
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
273
+ execute: () => {
274
+ const lines = ['TRACE RUN OVERVIEW'];
275
+ lines.push(index.errorSteps.length > 0
276
+ ? `status: ⚠ completed with ${index.errorSteps.length} step error(s) — see ERRORS below`
277
+ : 'status: no step errors recorded');
278
+ lines.push(`execution steps: ${index.orderedIds.length} · distinct stages: ${index.groups.length} · ` +
279
+ `commit-log mode: ${artifacts.snapshot.commitValues}`);
280
+ const example = index.orderedIds[0] ?? 'stageId#0';
281
+ lines.push(`step ids look like stageId#executionIndex (e.g. '${example}') — pass them to ` +
282
+ `trace_node / trace_slice / get_value.`);
283
+ lines.push('', `STAGES (${index.groups.length} distinct, first-execution order):`);
284
+ for (const group of index.groups.slice(0, OVERVIEW_STAGE_CAP)) {
285
+ const flags = [
286
+ group.isDecider ? ' [decision]' : '',
287
+ group.isSubflow ? ' [subflow]' : '',
288
+ ].join('');
289
+ const name = group.name !== undefined ? ` — "${group.name}"` : '';
290
+ const description = group.description !== undefined
291
+ ? `: ${truncateText(group.description, OVERVIEW_DESCRIPTION_CAP)}`
292
+ : '';
293
+ const errors = group.errorIds.length > 0
294
+ ? ` ⚠ errors in ${group.errorIds.slice(0, 3).join(', ')}${group.errorIds.length > 3 ? ', …' : ''}`
295
+ : '';
296
+ lines.push(`- ${group.stagePart} ×${group.ids.length}${flags}${name}${description}${errors}`);
297
+ }
298
+ if (index.groups.length > OVERVIEW_STAGE_CAP) {
299
+ lines.push(`…and ${index.groups.length - OVERVIEW_STAGE_CAP} more stages (output capped)`);
300
+ }
301
+ const loops = index.groups.filter((g) => g.ids.length > 1);
302
+ if (loops.length > 0) {
303
+ const top = [...loops]
304
+ .sort((a, b) => b.ids.length - a.ids.length)
305
+ .slice(0, 5)
306
+ .map((g) => `${g.stagePart} ×${g.ids.length}`)
307
+ .join(' · ');
308
+ lines.push('', `LOOPS: ${top}`);
309
+ }
310
+ if (index.errorSteps.length > 0) {
311
+ lines.push('', 'ERRORS:');
312
+ for (const step of index.errorSteps.slice(0, OVERVIEW_ERROR_CAP)) {
313
+ lines.push(`- ⚠ ${step.id}: ${step.keys.join(', ')} (drill with trace_node('${step.id}'))`);
314
+ }
315
+ if (index.errorSteps.length > OVERVIEW_ERROR_CAP) {
316
+ lines.push(`…and ${index.errorSteps.length - OVERVIEW_ERROR_CAP} more error steps`);
317
+ }
318
+ }
319
+ if (index.untrackedStepCount > 0) {
320
+ lines.push('', `HONESTY: ⚠ ${index.untrackedStepCount} step(s) consumed untracked inputs (args/env/silent ` +
321
+ `reads) — causal slices through them may be incomplete; trace_node marks each one.`);
322
+ }
323
+ const stateKeys = Object.keys(artifacts.snapshot.sharedState ?? {});
324
+ const shown = stateKeys.slice(0, OVERVIEW_KEY_CAP).map(displayKey);
325
+ lines.push('', `SHARED STATE KEYS (${stateKeys.length}): ${shown.join(', ')}` +
326
+ (stateKeys.length > OVERVIEW_KEY_CAP
327
+ ? `, … +${stateKeys.length - OVERVIEW_KEY_CAP} more`
328
+ : '') +
329
+ ' — values via who_wrote / get_value.');
330
+ if (artifacts.narrative !== undefined) {
331
+ lines.push(`NARRATIVE: ${artifacts.narrative.length} line(s) available via read_narrative.`);
332
+ }
333
+ return lines.join('\n');
334
+ },
335
+ });
336
+ }
337
+ // ── trace_node ─────────────────────────────────────────────────────────────
338
+ function buildTraceNode(artifacts, index, opts) {
339
+ return defineTool({
340
+ name: 'trace_node',
341
+ description: 'Inspect ONE execution step by runtimeStageId: name + description, what it wrote ' +
342
+ '(bounded previews + true sizes), what it read, its parents (which step provided each ' +
343
+ 'input, plus the decision that routed to it when known), errors, and ⚠ honesty markers. ' +
344
+ 'The drill-down primitive — use ids from run_overview, trace_slice, or who_wrote.',
345
+ inputSchema: {
346
+ type: 'object',
347
+ properties: {
348
+ runtimeStageId: idProperty(index, "The step to inspect, e.g. 'normalize#0'."),
349
+ },
350
+ required: ['runtimeStageId'],
351
+ additionalProperties: false,
352
+ },
353
+ execute: ({ runtimeStageId }) => {
354
+ const node = index.nodes.get(runtimeStageId);
355
+ const bundles = index.bundlesOf.get(runtimeStageId);
356
+ if (!node && !bundles)
357
+ return unknownIdMessage(runtimeStageId, index);
358
+ const lines = [];
359
+ const flags = [
360
+ node?.isDecider ? ' [decision]' : '',
361
+ node?.subflowId ? ' [subflow]' : '',
362
+ ].join('');
363
+ const name = node?.name ?? bundles?.[0]?.stage ?? stagePartOf(runtimeStageId);
364
+ lines.push(`STEP ${runtimeStageId} — "${name}"${flags}`);
365
+ if (node?.description)
366
+ lines.push(`description: ${node.description}`);
367
+ // Writes — verb-aware values via commitValueAt (delta-mode safe).
368
+ const writes = new Map(); // path → verb (last wins)
369
+ for (const bundle of bundles ?? []) {
370
+ for (const entry of bundle.trace)
371
+ writes.set(entry.path, entry.verb);
372
+ }
373
+ if (writes.size === 0) {
374
+ lines.push('wrote: nothing committed');
375
+ }
376
+ else {
377
+ lines.push(`wrote (${writes.size}):`);
378
+ const lastIdx = index.lastIdxOf.get(runtimeStageId) ?? index.commitLog.length - 1;
379
+ let shown = 0;
380
+ for (const [path, verb] of writes) {
381
+ if (shown >= NODE_WRITE_CAP) {
382
+ lines.push(`…and ${writes.size - NODE_WRITE_CAP} more keys (output capped)`);
383
+ break;
384
+ }
385
+ const preview = boundedPreview(commitValueAt(index.commitLog, lastIdx, path), opts.previewChars);
386
+ const dotted = displayKey(path);
387
+ lines.push(`- ${dotted} (${verb}): ${displayText(renderPreview(preview, `get_value('${runtimeStageId}', '${dotted}') for full`))}${redactionNote(bundles, path)}`);
388
+ shown++;
389
+ }
390
+ }
391
+ // Reads + parents.
392
+ const reads = node?.stageReads ? Object.keys(node.stageReads) : [];
393
+ if (reads.length === 0) {
394
+ lines.push(index.hasReadTracking
395
+ ? 'read: no tracked reads recorded'
396
+ : 'read: no tracked reads recorded ⚠ (artifacts carry no read tracking)');
397
+ }
398
+ else {
399
+ const shownReads = reads.slice(0, NODE_READ_CAP).map(displayKey).join(', ');
400
+ lines.push(`read (${reads.length}): ${shownReads}${reads.length > NODE_READ_CAP ? ', … (output capped)' : ''}`);
401
+ }
402
+ const parentLines = [];
403
+ const anchorIdx = anchorIdxFor(runtimeStageId, index);
404
+ for (const key of reads.slice(0, NODE_READ_CAP)) {
405
+ const writer = findLastWriter(index.commitLog, key, anchorIdx);
406
+ parentLines.push(writer
407
+ ? `- data: ${displayKey(key)} ← ${writer.runtimeStageId} "${writer.stage}"`
408
+ : `- data: ${displayKey(key)} ← (no tracked writer — run input/env/pre-run state ⚠)`);
409
+ }
410
+ const controlDep = artifacts.controlDeps?.(runtimeStageId);
411
+ if (controlDep) {
412
+ parentLines.push(`- control: routed here by ${controlDep.deciderId}` +
413
+ (controlDep.label ? ` — rule "${controlDep.label}"` : ''));
414
+ }
415
+ if (parentLines.length > 0) {
416
+ lines.push('parents:');
417
+ lines.push(...parentLines);
418
+ }
419
+ if (!artifacts.controlDeps) {
420
+ lines.push('⚠ control-dependence lookup not provided — the decision that routed here is unknown.');
421
+ }
422
+ const untracked = untrackedSourcesOf(bundles);
423
+ if (untracked.length > 0) {
424
+ lines.push(`⚠ this step also consumed ${untracked.join('/')} — those inputs are NOT in the parents ` +
425
+ `list; the slice through this step may be incomplete.`);
426
+ }
427
+ const errorKeys = Object.keys(node?.errors ?? {});
428
+ if (errorKeys.length > 0) {
429
+ lines.push(`errors (${errorKeys.length}):`);
430
+ for (const key of errorKeys.slice(0, OVERVIEW_ERROR_CAP)) {
431
+ const preview = boundedPreview((node?.errors ?? {})[key], opts.previewChars);
432
+ lines.push(`- ⚠ ${key}: ${displayText(renderPreview(preview))}`);
433
+ }
434
+ }
435
+ return lines.join('\n');
436
+ },
437
+ });
438
+ }
439
+ /**
440
+ * Commit-log anchor for "strictly before this step" lookups. Committed steps
441
+ * anchor at their own first bundle. A step with no commit (defensive — every
442
+ * executed stage normally records at least an empty bundle) anchors at the
443
+ * next committed step in execution order.
444
+ */
445
+ function anchorIdxFor(runtimeStageId, index) {
446
+ const own = index.firstIdxOf.get(runtimeStageId);
447
+ if (own !== undefined)
448
+ return own;
449
+ const position = index.orderedIds.indexOf(runtimeStageId);
450
+ if (position >= 0) {
451
+ for (let i = position + 1; i < index.orderedIds.length; i++) {
452
+ const idx = index.firstIdxOf.get(index.orderedIds[i]);
453
+ if (idx !== undefined)
454
+ return idx;
455
+ }
456
+ }
457
+ return index.commitLog.length;
458
+ }
459
+ // ── trace_slice ────────────────────────────────────────────────────────────
460
+ function buildTraceSlice(artifacts, index, opts) {
461
+ return defineTool({
462
+ name: 'trace_slice',
463
+ description: 'The causal chain: which earlier steps produced the data at a step (backward ' +
464
+ 'read→write slice over the commit log, plus [control: rule] edges to the decisions ' +
465
+ 'that routed execution when available). Returns a compact indented tree of step ids — ' +
466
+ 'drill any (id) with trace_node, fetch values with get_value. Bounded by maxDepth ' +
467
+ `(default ${opts.sliceMaxDepth}, max ${TOOLPACK_HARD_CAPS.sliceMaxDepth}) and maxNodes ` +
468
+ `(default ${opts.sliceMaxNodes}, max ${TOOLPACK_HARD_CAPS.sliceMaxNodes}); truncation and ` +
469
+ "incomplete-slice ⚠ markers are always shown. Pass 'key' to slice one state key only.",
470
+ inputSchema: {
471
+ type: 'object',
472
+ properties: {
473
+ runtimeStageId: idProperty(index, "The step to slice back from, e.g. 'approve#0'."),
474
+ key: keyProperty(index, 'Optional: restrict the slice to the chain that produced THIS state key.'),
475
+ maxDepth: {
476
+ type: 'integer',
477
+ description: `Optional slice depth (default ${opts.sliceMaxDepth}, hard cap ${TOOLPACK_HARD_CAPS.sliceMaxDepth}).`,
478
+ },
479
+ maxNodes: {
480
+ type: 'integer',
481
+ description: `Optional node budget (default ${opts.sliceMaxNodes}, hard cap ${TOOLPACK_HARD_CAPS.sliceMaxNodes}).`,
482
+ },
483
+ },
484
+ required: ['runtimeStageId'],
485
+ additionalProperties: false,
486
+ },
487
+ execute: ({ runtimeStageId, key, maxDepth, maxNodes }) => {
488
+ if (!index.firstIdxOf.has(runtimeStageId)) {
489
+ if (index.nodes.has(runtimeStageId)) {
490
+ const reads = Object.keys(index.nodes.get(runtimeStageId)?.stageReads ?? {});
491
+ return (`step '${runtimeStageId}' committed nothing — the commit-log slice cannot root there. ` +
492
+ (reads.length > 0
493
+ ? `It read: ${reads
494
+ .map(displayKey)
495
+ .join(', ')} — use who_wrote on each, or trace_slice from a downstream step.`
496
+ : 'Use trace_node for its details, or trace_slice from a downstream step.'));
497
+ }
498
+ return unknownIdMessage(runtimeStageId, index);
499
+ }
500
+ const depth = clampParam(maxDepth, opts.sliceMaxDepth, 1, TOOLPACK_HARD_CAPS.sliceMaxDepth);
501
+ const nodeBudget = clampParam(maxNodes, opts.sliceMaxNodes, 2, TOOLPACK_HARD_CAPS.sliceMaxNodes);
502
+ const keysReadOf = (id) => {
503
+ const node = index.nodes.get(id);
504
+ return node?.stageReads ? Object.keys(node.stageReads) : [];
505
+ };
506
+ const normalizedKey = key !== undefined ? normalizeKey(key, index.knownPaths) : undefined;
507
+ const keysFn = normalizedKey !== undefined
508
+ ? (id) => (id === runtimeStageId ? [normalizedKey] : keysReadOf(id))
509
+ : keysReadOf;
510
+ const dag = causalChain(index.commitLog, runtimeStageId, keysFn, {
511
+ maxDepth: depth,
512
+ maxNodes: nodeBudget,
513
+ ...(artifacts.controlDeps ? { controlDeps: artifacts.controlDeps } : {}),
514
+ });
515
+ if (!dag)
516
+ return unknownIdMessage(runtimeStageId, index);
517
+ const lines = [
518
+ `CAUSAL SLICE from ${runtimeStageId}` +
519
+ (normalizedKey !== undefined ? ` for key '${displayKey(normalizedKey)}'` : '') +
520
+ ` (maxDepth ${depth}, maxNodes ${nodeBudget})`,
521
+ 'each line: Stage (stepId) ← via <the key it provided> [wrote: …]. Drill any (stepId) ' +
522
+ 'with trace_node; fetch values with get_value.',
523
+ '',
524
+ displayText(formatCausalChain(dag)),
525
+ ];
526
+ if (!artifacts.controlDeps) {
527
+ lines.push('⚠ control edges unavailable — artifacts carry no controlDeps lookup (attach ' +
528
+ 'controlDepRecorder() to the original run); decisions that routed execution are not shown.');
529
+ }
530
+ if (!index.hasReadTracking) {
531
+ lines.push('⚠ artifacts carry no per-step read tracking — read→write edges cannot be followed; ' +
532
+ 'the slice may show only the start step.');
533
+ }
534
+ return lines.join('\n');
535
+ },
536
+ });
537
+ }
538
+ // ── who_wrote ──────────────────────────────────────────────────────────────
539
+ function buildWhoWrote(index, opts) {
540
+ return defineTool({
541
+ name: 'who_wrote',
542
+ description: 'Find the LAST step that wrote a state key — optionally before a given step ' +
543
+ '(beforeStageId), for "who set this value that step X then read?" questions. Returns ' +
544
+ 'the writer step id + stage name + write verb + a bounded value preview.',
545
+ inputSchema: {
546
+ type: 'object',
547
+ properties: {
548
+ key: keyProperty(index, "The state key, e.g. 'dti' or 'customer.address.zip'."),
549
+ beforeStageId: idProperty(index, 'Optional: only consider writes strictly BEFORE this step.'),
550
+ },
551
+ required: ['key'],
552
+ additionalProperties: false,
553
+ },
554
+ execute: ({ key, beforeStageId }) => {
555
+ const path = normalizeKey(key, index.knownPaths);
556
+ let beforeIdx;
557
+ if (beforeStageId !== undefined) {
558
+ if (!index.firstIdxOf.has(beforeStageId) && !index.nodes.has(beforeStageId)) {
559
+ return unknownIdMessage(beforeStageId, index);
560
+ }
561
+ beforeIdx = anchorIdxFor(beforeStageId, index);
562
+ }
563
+ const writer = findLastWriter(index.commitLog, path, beforeIdx);
564
+ if (!writer) {
565
+ return (`no tracked write to '${displayKey(path)}'` +
566
+ (beforeStageId !== undefined ? ` before ${beforeStageId}` : '') +
567
+ ` in the commit log. ⚠ the value may come from run input (args), env, pre-run state, ` +
568
+ `or a closure — those never enter the commit log.` +
569
+ (index.knownPaths.has(path) ? '' : unknownKeySuffix(index)));
570
+ }
571
+ const writerIdx = index.commitLog.indexOf(writer);
572
+ const verb = writer.trace.find((entry) => entry.path === path)?.verb ?? 'set';
573
+ const preview = boundedPreview(commitValueAt(index.commitLog, writerIdx, path), opts.previewChars);
574
+ const untracked = untrackedSourcesOf([writer]);
575
+ return (`'${displayKey(path)}' was last written by ${writer.runtimeStageId} — "${writer.stage}" ` +
576
+ `(verb: ${verb}): ${displayText(renderPreview(preview, `get_value('${writer.runtimeStageId}', '${displayKey(path)}') for full`))}${redactionNote([writer], path)}` +
577
+ (untracked.length > 0
578
+ ? `\n⚠ that step also consumed ${untracked.join('/')} — its inputs may not be fully traceable.`
579
+ : ''));
580
+ },
581
+ });
582
+ }
583
+ // ── get_value ──────────────────────────────────────────────────────────────
584
+ function buildGetValue(index, opts) {
585
+ return defineTool({
586
+ name: 'get_value',
587
+ description: 'Fetch the FULL value of a state key as of a given step (verb-aware reconstruction — ' +
588
+ 'works on delta commit logs). Output is capped at maxChars ' +
589
+ `(default ${opts.valueMaxChars}, hard cap ${TOOLPACK_HARD_CAPS.valueMaxChars}) with an ` +
590
+ 'explicit truncation notice. Prefer fetching a narrower nested key over raising the cap.',
591
+ inputSchema: {
592
+ type: 'object',
593
+ properties: {
594
+ runtimeStageId: idProperty(index, 'The step whose post-commit view of the key you want.'),
595
+ key: keyProperty(index, "The state key, e.g. 'dti' or 'customer.address.zip'."),
596
+ maxChars: {
597
+ type: 'integer',
598
+ description: `Optional char budget (default ${opts.valueMaxChars}, hard cap ${TOOLPACK_HARD_CAPS.valueMaxChars}).`,
599
+ },
600
+ },
601
+ required: ['runtimeStageId', 'key'],
602
+ additionalProperties: false,
603
+ },
604
+ execute: ({ runtimeStageId, key, maxChars }) => {
605
+ const known = index.firstIdxOf.has(runtimeStageId) || index.nodes.has(runtimeStageId);
606
+ if (!known)
607
+ return unknownIdMessage(runtimeStageId, index);
608
+ const path = normalizeKey(key, index.knownPaths);
609
+ const cap = clampParam(maxChars, opts.valueMaxChars, 50, TOOLPACK_HARD_CAPS.valueMaxChars);
610
+ const lastIdx = index.lastIdxOf.get(runtimeStageId);
611
+ const atIdx = lastIdx !== undefined ? lastIdx : anchorIdxFor(runtimeStageId, index) - 1;
612
+ if (!index.knownPaths.has(path)) {
613
+ return (`no tracked write to '${displayKey(path)}' anywhere in the commit log. ⚠ run input ` +
614
+ `(args), env, pre-run state, and closure-carried values never enter the commit log.` +
615
+ unknownKeySuffix(index));
616
+ }
617
+ const value = atIdx >= 0 ? commitValueAt(index.commitLog, atIdx, path) : undefined;
618
+ if (value === undefined) {
619
+ const firstWriter = findLastWriter(index.commitLog, path);
620
+ return (`'${displayKey(path)}' has no value as of ${runtimeStageId} — it was ` +
621
+ (firstWriter !== undefined
622
+ ? `written later (last writer over the whole run: ${firstWriter.runtimeStageId}), or deleted by then.`
623
+ : 'never written.') +
624
+ ` ⚠ pre-run seeded values never enter the commit log.`);
625
+ }
626
+ const serialized = displayText(safeStringify(value));
627
+ const redacted = redactionNote(index.bundlesOf.get(runtimeStageId), path);
628
+ const header = `VALUE of '${displayKey(path)}' as of ${runtimeStageId}${redacted}:`;
629
+ if (serialized.length <= cap)
630
+ return `${header}\n${serialized}`;
631
+ return (`${header}\n${serialized.slice(0, cap)}\n` +
632
+ `⚠ truncated: served ${cap} of ${serialized.length} chars — raise maxChars ` +
633
+ `(hard cap ${TOOLPACK_HARD_CAPS.valueMaxChars}) or fetch a narrower nested key.`);
634
+ },
635
+ });
636
+ }
637
+ // ── read_narrative ─────────────────────────────────────────────────────────
638
+ function buildReadNarrative(narrative) {
639
+ return defineTool({
640
+ name: 'read_narrative',
641
+ description: "Read the run's human-readable narrative, paginated: offset (0-based line index) + " +
642
+ `maxLines (default 40, hard cap ${TOOLPACK_HARD_CAPS.narrativeMaxLines}). Long lines are ` +
643
+ 'capped. Use AFTER the structured tools when you need the story around a step.',
644
+ inputSchema: {
645
+ type: 'object',
646
+ properties: {
647
+ offset: { type: 'integer', description: '0-based first line to read (default 0).' },
648
+ maxLines: {
649
+ type: 'integer',
650
+ description: `Lines to read (default 40, hard cap ${TOOLPACK_HARD_CAPS.narrativeMaxLines}).`,
651
+ },
652
+ },
653
+ additionalProperties: false,
654
+ },
655
+ execute: ({ offset, maxLines }) => {
656
+ const total = narrative.length;
657
+ if (total === 0)
658
+ return 'NARRATIVE: empty (0 lines).';
659
+ const start = clampParam(offset, 0, 0, Math.max(0, total - 1));
660
+ const count = clampParam(maxLines, 40, 1, TOOLPACK_HARD_CAPS.narrativeMaxLines);
661
+ const slice = narrative.slice(start, start + count);
662
+ const lines = [
663
+ `NARRATIVE lines ${start}–${start + slice.length - 1} of ${total}:`,
664
+ ...slice.map((line) => truncateText(displayText(line), NARRATIVE_LINE_CHAR_CAP)),
665
+ ];
666
+ const remaining = total - (start + slice.length);
667
+ if (remaining > 0) {
668
+ lines.push(`…${remaining} more line(s) — call read_narrative({ offset: ${start + slice.length} }).`);
669
+ }
670
+ return lines.join('\n');
671
+ },
672
+ });
673
+ }
674
+ // ── Scripted/offline invocation (the auditor pattern) ─────────────────────
675
+ /** Minimal offline ToolExecutionContext — trace tools never use credentials. */
676
+ const OFFLINE_CONTEXT = {
677
+ toolCallId: 'trace-toolpack-offline',
678
+ iteration: 0,
679
+ credentials: unconfiguredCredentialProvider(),
680
+ hasCredentials: false,
681
+ };
682
+ /**
683
+ * Invoke a toolpack tool OUTSIDE an Agent (scripted debug sessions, tests,
684
+ * offline auditors). Mirrors the Agent's #9 boundary: args are validated
685
+ * against the tool's inputSchema first, and an invalid call returns the same
686
+ * model-visible correction string instead of executing.
687
+ */
688
+ export async function callTraceTool(tools, name, args = {}) {
689
+ const tool = tools.find((candidate) => candidate.schema.name === name);
690
+ if (!tool) {
691
+ const available = tools.map((candidate) => candidate.schema.name).join(', ');
692
+ throw new Error(`callTraceTool: no tool named '${name}'. Available: ${available}`);
693
+ }
694
+ const verdict = validateToolArgs(args, tool.schema.inputSchema);
695
+ if (!verdict.ok)
696
+ return formatToolArgIssues(name, verdict.issues);
697
+ return String(await tool.execute(args, OFFLINE_CONTEXT));
698
+ }
699
+ //# sourceMappingURL=traceToolpack.js.map