agentfootprint 9.6.0 → 9.8.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 (126) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +1 -1
  3. package/ai-instructions/claude-code/SKILL.md +1 -1
  4. package/dist/adapters/code/agentcore.js +294 -0
  5. package/dist/adapters/code/agentcore.js.map +1 -0
  6. package/dist/adapters/code/local.js +200 -0
  7. package/dist/adapters/code/local.js.map +1 -0
  8. package/dist/adapters/identity/vault.js +336 -0
  9. package/dist/adapters/identity/vault.js.map +1 -0
  10. package/dist/adapters/observability/file.js +307 -0
  11. package/dist/adapters/observability/file.js.map +1 -0
  12. package/dist/core/Agent.js +132 -0
  13. package/dist/core/Agent.js.map +1 -1
  14. package/dist/core/RunnerBase.js +67 -0
  15. package/dist/core/RunnerBase.js.map +1 -1
  16. package/dist/core/agent/stages/toolCalls.js +90 -0
  17. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  18. package/dist/core/codeRunnerTool.js +252 -0
  19. package/dist/core/codeRunnerTool.js.map +1 -0
  20. package/dist/core/toolSessions.js +396 -0
  21. package/dist/core/toolSessions.js.map +1 -0
  22. package/dist/core/tools.js.map +1 -1
  23. package/dist/doors/providers.js +13 -0
  24. package/dist/doors/providers.js.map +1 -1
  25. package/dist/esm/adapters/code/agentcore.d.ts +133 -0
  26. package/dist/esm/adapters/code/agentcore.js +290 -0
  27. package/dist/esm/adapters/code/agentcore.js.map +1 -0
  28. package/dist/esm/adapters/code/local.d.ts +99 -0
  29. package/dist/esm/adapters/code/local.js +196 -0
  30. package/dist/esm/adapters/code/local.js.map +1 -0
  31. package/dist/esm/adapters/identity/vault.d.ts +146 -0
  32. package/dist/esm/adapters/identity/vault.js +332 -0
  33. package/dist/esm/adapters/identity/vault.js.map +1 -0
  34. package/dist/esm/adapters/observability/file.d.ts +145 -0
  35. package/dist/esm/adapters/observability/file.js +303 -0
  36. package/dist/esm/adapters/observability/file.js.map +1 -0
  37. package/dist/esm/adapters/types.d.ts +87 -0
  38. package/dist/esm/core/Agent.d.ts +65 -0
  39. package/dist/esm/core/Agent.js +133 -1
  40. package/dist/esm/core/Agent.js.map +1 -1
  41. package/dist/esm/core/RunnerBase.d.ts +51 -0
  42. package/dist/esm/core/RunnerBase.js +67 -0
  43. package/dist/esm/core/RunnerBase.js.map +1 -1
  44. package/dist/esm/core/agent/stages/toolCalls.d.ts +31 -0
  45. package/dist/esm/core/agent/stages/toolCalls.js +90 -0
  46. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  47. package/dist/esm/core/agent/types.d.ts +23 -2
  48. package/dist/esm/core/codeRunnerTool.d.ts +120 -0
  49. package/dist/esm/core/codeRunnerTool.js +247 -0
  50. package/dist/esm/core/codeRunnerTool.js.map +1 -0
  51. package/dist/esm/core/toolSessions.d.ts +318 -0
  52. package/dist/esm/core/toolSessions.js +389 -0
  53. package/dist/esm/core/toolSessions.js.map +1 -0
  54. package/dist/esm/core/tools.d.ts +60 -0
  55. package/dist/esm/core/tools.js.map +1 -1
  56. package/dist/esm/doors/providers.d.ts +7 -0
  57. package/dist/esm/doors/providers.js +10 -0
  58. package/dist/esm/doors/providers.js.map +1 -1
  59. package/dist/esm/events/payloads.d.ts +50 -0
  60. package/dist/esm/events/registry.d.ts +9 -1
  61. package/dist/esm/events/registry.js +8 -0
  62. package/dist/esm/events/registry.js.map +1 -1
  63. package/dist/esm/identity.d.ts +1 -0
  64. package/dist/esm/identity.js +4 -0
  65. package/dist/esm/identity.js.map +1 -1
  66. package/dist/esm/index.d.ts +2 -0
  67. package/dist/esm/index.js +5 -0
  68. package/dist/esm/index.js.map +1 -1
  69. package/dist/esm/lib/mcp/mcpServe.js +37 -1
  70. package/dist/esm/lib/mcp/mcpServe.js.map +1 -1
  71. package/dist/esm/lib/trace-toolpack/traceToolpack.js +15 -1
  72. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  73. package/dist/esm/observability-providers.d.ts +4 -1
  74. package/dist/esm/observability-providers.js +7 -1
  75. package/dist/esm/observability-providers.js.map +1 -1
  76. package/dist/events/registry.js +8 -0
  77. package/dist/events/registry.js.map +1 -1
  78. package/dist/identity.js +6 -1
  79. package/dist/identity.js.map +1 -1
  80. package/dist/index.js +14 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/lib/mcp/mcpServe.js +37 -1
  83. package/dist/lib/mcp/mcpServe.js.map +1 -1
  84. package/dist/lib/trace-toolpack/traceToolpack.js +15 -1
  85. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  86. package/dist/observability-providers.js +9 -2
  87. package/dist/observability-providers.js.map +1 -1
  88. package/dist/types/adapters/code/agentcore.d.ts +134 -0
  89. package/dist/types/adapters/code/agentcore.d.ts.map +1 -0
  90. package/dist/types/adapters/code/local.d.ts +100 -0
  91. package/dist/types/adapters/code/local.d.ts.map +1 -0
  92. package/dist/types/adapters/identity/vault.d.ts +147 -0
  93. package/dist/types/adapters/identity/vault.d.ts.map +1 -0
  94. package/dist/types/adapters/observability/file.d.ts +146 -0
  95. package/dist/types/adapters/observability/file.d.ts.map +1 -0
  96. package/dist/types/adapters/types.d.ts +87 -0
  97. package/dist/types/adapters/types.d.ts.map +1 -1
  98. package/dist/types/core/Agent.d.ts +65 -0
  99. package/dist/types/core/Agent.d.ts.map +1 -1
  100. package/dist/types/core/RunnerBase.d.ts +51 -0
  101. package/dist/types/core/RunnerBase.d.ts.map +1 -1
  102. package/dist/types/core/agent/stages/toolCalls.d.ts +31 -0
  103. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  104. package/dist/types/core/agent/types.d.ts +23 -2
  105. package/dist/types/core/agent/types.d.ts.map +1 -1
  106. package/dist/types/core/codeRunnerTool.d.ts +121 -0
  107. package/dist/types/core/codeRunnerTool.d.ts.map +1 -0
  108. package/dist/types/core/toolSessions.d.ts +319 -0
  109. package/dist/types/core/toolSessions.d.ts.map +1 -0
  110. package/dist/types/core/tools.d.ts +60 -0
  111. package/dist/types/core/tools.d.ts.map +1 -1
  112. package/dist/types/doors/providers.d.ts +7 -0
  113. package/dist/types/doors/providers.d.ts.map +1 -1
  114. package/dist/types/events/payloads.d.ts +50 -0
  115. package/dist/types/events/payloads.d.ts.map +1 -1
  116. package/dist/types/events/registry.d.ts +9 -1
  117. package/dist/types/events/registry.d.ts.map +1 -1
  118. package/dist/types/identity.d.ts +1 -0
  119. package/dist/types/identity.d.ts.map +1 -1
  120. package/dist/types/index.d.ts +2 -0
  121. package/dist/types/index.d.ts.map +1 -1
  122. package/dist/types/lib/mcp/mcpServe.d.ts.map +1 -1
  123. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  124. package/dist/types/observability-providers.d.ts +4 -1
  125. package/dist/types/observability-providers.d.ts.map +1 -1
  126. package/package.json +1 -1
@@ -0,0 +1,303 @@
1
+ /**
2
+ * fileObservability — the typed event stream, one JSON line per event, in a
3
+ * local file.
4
+ *
5
+ * The sink for the shop that has no collector. Every other adapter in this
6
+ * folder ships somewhere: CloudWatch, X-Ray, an OTLP endpoint. A great many
7
+ * on-premises deployments have none of those — they have a directory, a log
8
+ * shipper (Filebeat, Fluent Bit, Vector, `promtail`, `journald`, or a person
9
+ * with `grep`), and a rule that nothing leaves the network. NDJSON on disk is
10
+ * the format all of those already read, so the sink is the file.
11
+ *
12
+ * Zero dependencies: `node:fs`, lazily required at construction so merely
13
+ * importing `agentfootprint/observe` stays browser-safe (this factory is
14
+ * Node-only; calling it in a browser throws by name).
15
+ *
16
+ * ## The line
17
+ *
18
+ * One `JSON.stringify(event)` per line, newline-terminated, appended in
19
+ * dispatch order — the SAME envelope `cloudwatchObservability` puts in a log
20
+ * event, so a query written against one reads the other:
21
+ *
22
+ * ```jsonl
23
+ * {"type":"agentfootprint.agent.turn_start","payload":{…},"meta":{"runId":"…","sessionId":"…"}}
24
+ * {"type":"agentfootprint.stream.tool_end","payload":{…},"meta":{…}}
25
+ * ```
26
+ *
27
+ * Nothing is summarized, bounded or redacted on the way out. **A payload that
28
+ * must not be on that disk must not reach this strategy** — narrow it with
29
+ * `eventTypes` / `tier` / `sampleRate`, or apply a footprintjs
30
+ * `RedactionPolicy` upstream, exactly as with every other sink. (For a bounded
31
+ * record by construction, `auditExport({ payloadMode: 'bounded' })` is the
32
+ * adapter that does that job.)
33
+ *
34
+ * ## Buffered, not synchronous
35
+ *
36
+ * `exportEvent` is sync and never touches the disk: it serializes, buffers, and
37
+ * returns. Batches are appended asynchronously on a size trigger
38
+ * (`maxBufferEvents` / `maxBufferBytes`), on a timer (`flushIntervalMs`), and on
39
+ * `flush()`. A hard kill therefore loses at most the buffer — the price of not
40
+ * making telemetry a term in agent-loop latency. Call `flush()` (or
41
+ * `agent.shutdown()`, which does) at process end; see the 8.12.0 lifecycle laws
42
+ * on {@link BaseStrategy.flush}.
43
+ *
44
+ * ## Rotation is ONE generation, and that is deliberate
45
+ *
46
+ * With `maxBytes` set, a batch that would push the file past the ceiling first
47
+ * renames it to `<path>.1` — **replacing any previous `.1`** — and starts a
48
+ * fresh file. That is the whole policy. There is no `.2`, no compression, no
49
+ * time-based schedule, no cross-process coordination (two processes writing one
50
+ * file each keep their own byte count and will both rotate it). It exists so an
51
+ * unattended agent cannot fill a disk, and for nothing else: **retention is a
52
+ * log-management daemon's job**, and `logrotate` with `copytruncate`, Fluent Bit,
53
+ * or a systemd timer will do it properly. Omit `maxBytes` — the default — and
54
+ * this adapter never renames anything, which is the right choice when a real
55
+ * rotator already owns the file.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { fileObservability } from 'agentfootprint/observe';
60
+ *
61
+ * const telemetry = agent.enable.observability({
62
+ * strategy: fileObservability({
63
+ * path: '/var/log/agentfootprint/events.ndjson',
64
+ * maxBytes: 64 * 1024 * 1024, // safety ceiling; logrotate owns retention
65
+ * }),
66
+ * });
67
+ *
68
+ * // … run …
69
+ * await agent.shutdown(); // flushes + stops everything enabled
70
+ * ```
71
+ */
72
+ import { lazyRequire } from '../../lib/lazyRequire.js';
73
+ import { rateLimitedConsoleSink } from './deliveryErrors.js';
74
+ // ─── Public factory ──────────────────────────────────────────────────
75
+ /**
76
+ * NDJSON-to-a-local-file observability strategy. See
77
+ * {@link FileObservabilityOptions} for the per-option contract, and this
78
+ * module's docstring for the rotation policy and what is NOT bounded.
79
+ */
80
+ export function fileObservability(opts) {
81
+ const strategyName = 'file';
82
+ if (!opts.path || typeof opts.path !== 'string' || opts.path.trim() === '') {
83
+ throw new TypeError(`[${strategyName}Observability] \`path\` is required — the file this sink writes. ` +
84
+ `Pass one, e.g. '/var/log/agentfootprint/events.ndjson'. There is no default: ` +
85
+ `where a run's telemetry lands on your disk is not a decision this library makes.`);
86
+ }
87
+ if (opts.maxBytes !== undefined && !(opts.maxBytes > 0)) {
88
+ throw new TypeError(`[${strategyName}Observability] \`maxBytes\` must be a positive number of bytes ` +
89
+ `(got ${String(opts.maxBytes)}). Omit it for no rotation at all, which is the ` +
90
+ `right choice when logrotate or a log shipper already owns '${opts.path}'.`);
91
+ }
92
+ const path = opts.path;
93
+ const rotatedPath = `${path}.1`;
94
+ const maxBufferEvents = opts.maxBufferEvents ?? 100;
95
+ const maxBufferBytes = opts.maxBufferBytes ?? 65_536;
96
+ const flushIntervalMs = opts.flushIntervalMs ?? 1000;
97
+ const fs = opts._fs ?? createNodeFileSink(strategyName);
98
+ // Claim the file NOW. Two things this buys, both worth the syscall:
99
+ // 1. an unwritable path (no directory, no permission, a directory where a
100
+ // file should be, a read-only mount) is refused at construction, where
101
+ // the caller can still read the message;
102
+ // 2. `statSync` below gives the rotation counter a true starting size, so a
103
+ // restart appends to an existing file rather than believing it is empty.
104
+ let bytesOnDisk = 0;
105
+ try {
106
+ const dir = parentDir(path);
107
+ if (dir)
108
+ fs.mkdirSync(dir, { recursive: true });
109
+ fs.appendFileSync(path, '');
110
+ bytesOnDisk = fs.statSync(path).size;
111
+ }
112
+ catch (err) {
113
+ throw new Error(`[${strategyName}Observability] cannot write '${path}': ${errorMessage(err)}. ` +
114
+ `The parent directory is created for you; a failure here means the path is a ` +
115
+ `directory, the mount is read-only, or the process user lacks permission. ` +
116
+ `Fix the path or the permission — this sink refuses to start rather than ` +
117
+ `silently drop every event.`);
118
+ }
119
+ // Buffered lines — drained by `flush()` / size trigger / timer.
120
+ const buffer = [];
121
+ let bufferBytes = 0;
122
+ let lastFlushPromise = Promise.resolve();
123
+ let timer;
124
+ let stopped = false;
125
+ // The fallback when the consumer wires nothing. Rate-limited; a
126
+ // consumer-supplied sink is not.
127
+ const consoleSink = rateLimitedConsoleSink(strategyName);
128
+ function scheduleTimedFlush() {
129
+ if (timer || flushIntervalMs <= 0 || stopped)
130
+ return;
131
+ timer = setTimeout(() => {
132
+ timer = undefined;
133
+ void doFlush();
134
+ }, flushIntervalMs);
135
+ // Never hold the process open for telemetry. `unref` exists on Node's
136
+ // Timeout and not on the browser's number — feature-detected, so a
137
+ // non-Node runtime with an injected `_fs` still works.
138
+ timer.unref?.();
139
+ }
140
+ /** Route a delivery failure through whatever `_onError` IS RIGHT NOW —
141
+ * reading it at call time, so a consumer who assigns `_onError` after
142
+ * construction still receives them (the 8.11.0 fix). */
143
+ function reportFailure(err) {
144
+ strategy._onError?.(err);
145
+ }
146
+ /** ONE generation. A file already at or over the ceiling is renamed to
147
+ * `<path>.1` (replacing any previous `.1`) and the counter resets. A batch
148
+ * bigger than the whole ceiling is still written — truncating a run's
149
+ * telemetry to fit a dial nobody set for that purpose would be worse. */
150
+ async function rotateIfNeeded(incomingBytes) {
151
+ const ceiling = opts.maxBytes;
152
+ if (ceiling === undefined)
153
+ return;
154
+ if (bytesOnDisk === 0)
155
+ return;
156
+ if (bytesOnDisk + incomingBytes <= ceiling)
157
+ return;
158
+ await fs.rename(path, rotatedPath);
159
+ bytesOnDisk = 0;
160
+ }
161
+ async function doFlush() {
162
+ // `stopped` is deliberately NOT a guard here — same stance as the AWS
163
+ // adapters since 8.11.1. `stop()` stops this strategy ACCEPTING events; it
164
+ // does not authorise throwing away events already accepted, and a `flush()`
165
+ // that cannot make progress after a `stop()` is how shutdown spins forever.
166
+ if (buffer.length === 0)
167
+ return;
168
+ // Snapshot + clear so events emitted during the in-flight write accumulate
169
+ // into the next batch.
170
+ const batch = buffer.splice(0);
171
+ const batchBytes = bufferBytes;
172
+ bufferBytes = 0;
173
+ const data = `${batch.join('\n')}\n`;
174
+ try {
175
+ await rotateIfNeeded(batchBytes);
176
+ await fs.appendFile(path, data);
177
+ bytesOnDisk += byteLength(data);
178
+ }
179
+ catch (err) {
180
+ reportFailure(new Error(`${batch.length} event(s) dropped writing to '${path}': ${errorMessage(err)}`));
181
+ }
182
+ }
183
+ function enqueue(event) {
184
+ if (stopped)
185
+ return;
186
+ // The hot path must not throw (port law). `JSON.stringify` can — a cycle, a
187
+ // BigInt, a throwing getter — and an event that cannot be serialized is a
188
+ // reportable fact, not a reason to break the agent loop.
189
+ let line;
190
+ try {
191
+ line = JSON.stringify(event);
192
+ }
193
+ catch (err) {
194
+ reportFailure(new Error(`event '${event?.type ?? 'unknown'}' could not be serialized: ` + errorMessage(err)));
195
+ return;
196
+ }
197
+ // `JSON.stringify` returns undefined for an undefined input — nothing to
198
+ // write, and a bare "undefined" line would corrupt the NDJSON stream.
199
+ if (line === undefined)
200
+ return;
201
+ buffer.push(line);
202
+ bufferBytes += byteLength(line) + 1; // + the newline this line will carry
203
+ if (buffer.length >= maxBufferEvents || bufferBytes >= maxBufferBytes) {
204
+ // Size trigger. Chain onto the last write rather than racing it — the
205
+ // file's line order IS the dispatch order, and that is the only thing an
206
+ // offline reader can rely on.
207
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
208
+ }
209
+ else {
210
+ scheduleTimedFlush();
211
+ }
212
+ }
213
+ const strategy = {
214
+ name: strategyName,
215
+ capabilities: { events: true, logs: true },
216
+ ...(opts.eventTypes && { relevantEventTypes: opts.eventTypes }),
217
+ exportEvent: enqueue,
218
+ /**
219
+ * Write what is buffered. Called for you on shutdown (8.12.0) — by the
220
+ * handle `enable.observability()` returns, by `agent.shutdown()`, and by a
221
+ * `standingAgent` closing. Safe at any time, including after `stop()`.
222
+ */
223
+ async flush() {
224
+ // BOUNDED BY CONSTRUCTION — every pass must remove at least one buffered
225
+ // line; a pass that removes none ends the drain instead of retrying. The
226
+ // shape the CloudWatch adapter arrived at in 8.11.1, for the same reason:
227
+ // a drain that cannot finish must return, never spin.
228
+ for (;;) {
229
+ const pending = buffer.length;
230
+ lastFlushPromise = lastFlushPromise.then(doFlush, doFlush);
231
+ await lastFlushPromise;
232
+ if (buffer.length === 0)
233
+ return;
234
+ if (buffer.length >= pending)
235
+ return;
236
+ }
237
+ },
238
+ /** Clear the timer and stop accepting events. Terminal: there is no
239
+ * restart. What it does NOT do is discard the buffer — `flush()` after
240
+ * `stop()` still writes it. */
241
+ stop() {
242
+ stopped = true;
243
+ if (timer) {
244
+ clearTimeout(timer);
245
+ timer = undefined;
246
+ }
247
+ },
248
+ /** Where failures go. Two callers: the dispatch layer (when `exportEvent`
249
+ * itself throws) and this adapter's own write path. */
250
+ _onError(err, event) {
251
+ (opts.onError ?? consoleSink)(err, event);
252
+ },
253
+ };
254
+ return strategy;
255
+ }
256
+ // ─── node:fs binding (lazy) ──────────────────────────────────────────
257
+ /**
258
+ * Bind {@link FileSinkFs} to `node:fs`. Lazily required so that importing
259
+ * `agentfootprint/observe` in a browser bundle never resolves `node:fs` — only
260
+ * CALLING this factory does, and a runtime without it is refused by name.
261
+ */
262
+ function createNodeFileSink(strategyName) {
263
+ let mod;
264
+ try {
265
+ mod = lazyRequire('node:fs');
266
+ }
267
+ catch {
268
+ throw new Error(`[${strategyName}Observability] needs \`node:fs\`, and this runtime has none. ` +
269
+ `It is a Node-only sink by definition — a browser has no local file to append to. ` +
270
+ `In a browser, ship events over your own transport, or pass \`_fs\` to write ` +
271
+ `somewhere you control.`);
272
+ }
273
+ return {
274
+ mkdirSync: (dir, options) => void mod.mkdirSync(dir, options),
275
+ appendFileSync: (file, data) => mod.appendFileSync(file, data),
276
+ statSync: (file) => mod.statSync(file),
277
+ appendFile: (file, data) => mod.promises.appendFile(file, data),
278
+ rename: (from, to) => mod.promises.rename(from, to),
279
+ };
280
+ }
281
+ // ─── Small helpers ───────────────────────────────────────────────────
282
+ function errorMessage(err) {
283
+ return err instanceof Error ? err.message : String(err);
284
+ }
285
+ /** UTF-8 byte length. `Buffer` where there is one, `TextEncoder` otherwise —
286
+ * the byte count decides rotation, so a multi-byte prompt must not be counted
287
+ * as its character length. */
288
+ function byteLength(s) {
289
+ const B = globalThis.Buffer;
290
+ if (B)
291
+ return B.byteLength(s, 'utf8');
292
+ return new TextEncoder().encode(s).length;
293
+ }
294
+ /** The directory part of a path, POSIX or Windows separators, or `''` for a
295
+ * bare filename (nothing to create). Done by hand rather than through
296
+ * `node:path` so the module has exactly one Node import, behind one seam. */
297
+ function parentDir(file) {
298
+ const idx = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\'));
299
+ if (idx <= 0)
300
+ return '';
301
+ return file.slice(0, idx);
302
+ }
303
+ //# sourceMappingURL=file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file.js","sourceRoot":"","sources":["../../../../src/adapters/observability/file.ts"],"names":[],"mappings":"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"}
@@ -584,3 +584,90 @@ export interface PricingTable {
584
584
  /** USD per ONE token for the given model+kind. */
585
585
  pricePerToken(model: string, kind: TokenKind): number;
586
586
  }
587
+ /**
588
+ * A service that runs code in an isolated session — a managed code
589
+ * interpreter, a container pool, a subprocess.
590
+ *
591
+ * **The shape is Start → Execute ×N → Stop**, because that is what every real
592
+ * one is, and the middle is the part a framework has to make possible. Paying
593
+ * session start-up on every call is the honest cheap version; holding the
594
+ * session in a module-level map is the fast version that hands one live sandbox
595
+ * to whoever calls next. `ctx.onTeardown` + {@link ToolExecutionContext.runId}
596
+ * are what let a tool do neither.
597
+ *
598
+ * ── Why this port exists at all: "summarize prose, compute data" ────────────
599
+ * A tool that returns 40MB of rows does not need a bigger context window; it
600
+ * needs to not put the rows in one. The motivating failure is a real production
601
+ * request of 879,073 tokens — a tool result pasted straight into the prompt.
602
+ * With a code runner, the model writes the aggregation, the RUNNER holds the
603
+ * data, and what comes back is the answer. Prose gets summarized; data gets
604
+ * computed. `CodeResult.truncated` exists so the second half of that promise
605
+ * cannot quietly break.
606
+ *
607
+ * Implement it for your own backend; ship it to `codeRunnerTool({ runner })`.
608
+ */
609
+ export interface CodeRunner {
610
+ /** Stable id — reported on every `agentfootprint.tools.session_*` event so a
611
+ * row names its backend, not just its tool. */
612
+ readonly id: string;
613
+ /**
614
+ * Open a session.
615
+ *
616
+ * `key` is the ISOLATION key the caller derived (see `toolSessionKey`). An
617
+ * adapter may use it to name the remote session; it must never widen it.
618
+ */
619
+ start(req: {
620
+ readonly key: string;
621
+ readonly language?: string;
622
+ readonly signal?: AbortSignal;
623
+ }): Promise<CodeSession>;
624
+ }
625
+ /** One live session. `stop()` is idempotent and tolerates "already gone". */
626
+ export interface CodeSession {
627
+ /** The backend's own id for this session, when it has one. */
628
+ readonly id: string;
629
+ execute(req: {
630
+ readonly code: string;
631
+ readonly language?: string;
632
+ readonly timeoutMs?: number;
633
+ readonly signal?: AbortSignal;
634
+ }): Promise<CodeResult>;
635
+ /**
636
+ * Release the session.
637
+ *
638
+ * Must tolerate a session the far side already reaped — an idle timeout is
639
+ * the reality on every managed backend, and a `Stop` on a dead session is a
640
+ * no-op, not an error.
641
+ */
642
+ stop(): Promise<void>;
643
+ }
644
+ /** What one execution produced. */
645
+ export interface CodeResult {
646
+ /** Did the code run to completion without an error exit? */
647
+ readonly ok: boolean;
648
+ readonly stdout: string;
649
+ readonly stderr: string;
650
+ readonly exitCode?: number;
651
+ /** Files the run produced, described rather than inlined — the whole point is
652
+ * that big data does not enter the window. */
653
+ readonly artifacts?: readonly {
654
+ readonly name: string;
655
+ readonly bytes: number;
656
+ readonly uri?: string;
657
+ }[];
658
+ /**
659
+ * Present IFF output was cut, and then it says by how much.
660
+ *
661
+ * Load-bearing, not politeness. A runner exists so big data is computed
662
+ * outside the context window instead of pasted into it; a runner that
663
+ * quietly slices its own output to fit is the same bug wearing a different
664
+ * hat, and the model would go on to reason over a truncated table it was
665
+ * never told was truncated. An unstated slice is a silent success.
666
+ */
667
+ readonly truncated?: {
668
+ readonly stdout?: boolean;
669
+ readonly stderr?: boolean;
670
+ /** The pre-truncation length, in characters, of whichever stream was cut. */
671
+ readonly ofChars?: number;
672
+ };
673
+ }
@@ -286,6 +286,9 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
286
286
  * default is derived from a runId, and storing that would pin a whole
287
287
  * conversation to the id of the one run that started it. */
288
288
  private lastRunIdentity?;
289
+ /** How long ONE tool teardown may take before the runner stops waiting.
290
+ * See `AgentOptions.toolTeardownTimeoutMs`. */
291
+ private readonly toolTeardownTimeoutMs;
289
292
  /** The run in flight, by id — the whole of the one-turn-at-a-time guard.
290
293
  * Set before the executor is built and cleared in `finally`, so a run that
291
294
  * throws does not leave the agent permanently refusing. */
@@ -611,6 +614,29 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
611
614
  */
612
615
  private installCheckpointTracker;
613
616
  resume(checkpoint: FlowchartCheckpoint, input?: unknown, options?: AgentRunOptions): Promise<AgentOutput | RunnerPauseOutcome>;
617
+ /**
618
+ * Fire `'run'`-scoped tool teardown — IF this run really ended.
619
+ *
620
+ * **Not on `finally`, and that is the whole point.** `finally` runs on every
621
+ * exit including a pause, and a pause exits TWO ways: a returned
622
+ * `RunnerPauseOutcome` and a thrown `PauseSignal`. A check-in on a
623
+ * code-interpreter call pauses the run so a person can approve the code —
624
+ * tearing the sandbox down there destroys the exact state the resume needs,
625
+ * and it fails QUIETLY, as a resumed run that "just re-ran everything".
626
+ * Both shapes are discriminated here and both are skipped.
627
+ *
628
+ * An error IS a terminal: the run is over, nobody is coming back, and a
629
+ * sandbox held by a run that crashed is the clearest kind of leak. Only a
630
+ * pause survives.
631
+ *
632
+ * Fired for the TURN, not for `currentRunContext.runId` — `resume()` mints a
633
+ * fresh run id, so a pause and its resume are one turn across two runs, and
634
+ * filtering on the id would leave everything a paused turn opened alive
635
+ * forever. See `ToolSessionTier.fireRun`.
636
+ *
637
+ * @param outcome what `run()`/`resume()` is about to return, or about to throw.
638
+ */
639
+ private endRunToolSessions;
614
640
  /**
615
641
  * The conversation this agent's LAST completed run leaves behind, packed as
616
642
  * the same `AgentRunCheckpoint` that `resumeOnError(...)` accepts. Store it,
@@ -761,6 +787,45 @@ export declare class Agent extends RunnerBase<AgentInput, AgentOutput> {
761
787
  * An open pick ACTIVATES but never moves the cursor — see the tool-calls gate.
762
788
  * Computed once per chart build; the injection list is fixed at construction.
763
789
  */
790
+ /**
791
+ * The identity facts this run hands `tool.execute` (9.7.0).
792
+ *
793
+ * Read through an ACCESSOR from the chart (see `ToolCallsHandlerDeps.currentRun`)
794
+ * because the chart is built once and this changes every run.
795
+ *
796
+ * `identity` is `lastRunIdentity` — what the CALLER passed — and deliberately
797
+ * NOT `scope.runIdentity`, which is always populated and defaults to
798
+ * `{ conversationId: '<runId>' }`. Handing a tool a synthesized conversation
799
+ * as "the identity" would let it key an isolated session on a fiction, and
800
+ * would make "absent" unrepresentable at exactly the layer that most needs to
801
+ * see it.
802
+ */
803
+ private toolRunFacts;
804
+ /**
805
+ * The teardown tier, built on FIRST registration.
806
+ *
807
+ * An agent whose tools never hold a session never allocates one, and its
808
+ * terminals stay a single `undefined` check.
809
+ */
810
+ private toolSessions;
811
+ /**
812
+ * Turn one TEARDOWN report into a typed `agentfootprint.tools.session_*` event.
813
+ *
814
+ * Only the two closing events come through here. A start and a reuse happen
815
+ * inside `tool.execute`, where the dispatch loop still holds the scope, so
816
+ * those ride the ordinary emit channel and carry the stage they really
817
+ * happened in. These two fire after the run's last stage committed, and this
818
+ * is the one place that has to answer "from where?" without a stage to point
819
+ * at.
820
+ *
821
+ * **Built with `buildEventMeta`, never `minimalMeta()`.** `minimalMeta()`
822
+ * hardcodes `runId: 'consumer-scope'`, and a teardown event stamped that way
823
+ * cannot be joined to the run that OPENED the session — the exact
824
+ * unjoinability 9.4.0 spent a release fixing for credential events. So the
825
+ * meta comes from `currentRunContext`, with a STATED pseudo-stage, the same
826
+ * move as the `'<stageId>#paused'` stamp at the pause boundary.
827
+ */
828
+ private emitToolSessionReport;
764
829
  private openSkillIds;
765
830
  /**
766
831
  * The per-iteration `read_skill` offer builder — or `undefined` to leave the tool