zidane 5.4.1 → 5.4.3

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 (68) hide show
  1. package/README.md +15 -0
  2. package/dist/{agent-DHQAsdj6.d.ts → agent-Yu8uhpy-.d.ts} +213 -3
  3. package/dist/agent-Yu8uhpy-.d.ts.map +1 -0
  4. package/dist/chat.d.ts +49 -6
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{errors-Byb0F8B9.js → errors-CDwtPIMX.js} +4 -2
  8. package/dist/{errors-Byb0F8B9.js.map → errors-CDwtPIMX.js.map} +1 -1
  9. package/dist/{index-CHSaLab5.d.ts → index-DklfxeYy.d.ts} +8 -2
  10. package/dist/index-DklfxeYy.d.ts.map +1 -0
  11. package/dist/{index-CrqFoaQA.d.ts → index-j9tY28ah.d.ts} +474 -8
  12. package/dist/index-j9tY28ah.d.ts.map +1 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +1528 -53
  15. package/dist/index.js.map +1 -1
  16. package/dist/{interpolate-ERgZUxgg.js → interpolate-CmtjEyRJ.js} +155 -18
  17. package/dist/interpolate-CmtjEyRJ.js.map +1 -0
  18. package/dist/{login-8c5C0FYq.js → login-DxyAERe1.js} +3 -3
  19. package/dist/{login-8c5C0FYq.js.map → login-DxyAERe1.js.map} +1 -1
  20. package/dist/{mcp-DhmmJfxK.js → mcp-CNUbvbsy.js} +2 -2
  21. package/dist/{mcp-DhmmJfxK.js.map → mcp-CNUbvbsy.js.map} +1 -1
  22. package/dist/mcp.d.ts +1 -1
  23. package/dist/mcp.js +1 -1
  24. package/dist/{messages-D0xT979U.js → messages-fTR19Ga6.js} +2 -2
  25. package/dist/{messages-D0xT979U.js.map → messages-fTR19Ga6.js.map} +1 -1
  26. package/dist/{presets-Ck4VusTo.js → presets-D9IbaI40.js} +2 -2
  27. package/dist/{presets-Ck4VusTo.js.map → presets-D9IbaI40.js.map} +1 -1
  28. package/dist/presets.d.ts +2 -2
  29. package/dist/presets.js +1 -1
  30. package/dist/{providers-x3LZByR5.js → providers-CEzRFYtS.js} +3 -3
  31. package/dist/{providers-x3LZByR5.js.map → providers-CEzRFYtS.js.map} +1 -1
  32. package/dist/providers.d.ts +1 -1
  33. package/dist/providers.js +2 -2
  34. package/dist/session/sqlite.d.ts +1 -1
  35. package/dist/session/sqlite.js +1 -1
  36. package/dist/{session-BHZwxmfr.js → session-kwsNnOmt.js} +2 -2
  37. package/dist/{session-BHZwxmfr.js.map → session-kwsNnOmt.js.map} +1 -1
  38. package/dist/session.d.ts +1 -1
  39. package/dist/session.js +2 -2
  40. package/dist/skills.d.ts +2 -2
  41. package/dist/skills.js +1 -1
  42. package/dist/{tools-PQH1Ge4M.js → tools-BK2vG9UX.js} +246 -44
  43. package/dist/tools-BK2vG9UX.js.map +1 -0
  44. package/dist/tools.d.ts +2 -2
  45. package/dist/tools.js +1 -1
  46. package/dist/{transcript-anchors-ByB2MSCB.d.ts → transcript-anchors-DnaBcJej.d.ts} +52 -8
  47. package/dist/transcript-anchors-DnaBcJej.d.ts.map +1 -0
  48. package/dist/tui.d.ts +4 -2
  49. package/dist/tui.d.ts.map +1 -1
  50. package/dist/tui.js +651 -42
  51. package/dist/tui.js.map +1 -1
  52. package/dist/{turn-operations-Bqs4YbbH.js → turn-operations-OzKEOXul.js} +240 -52
  53. package/dist/turn-operations-OzKEOXul.js.map +1 -0
  54. package/dist/types-IcokUOyC.js.map +1 -1
  55. package/dist/types.d.ts +2 -2
  56. package/dist/types.js +1 -1
  57. package/docs/ARCHITECTURE.md +16 -3
  58. package/docs/CHAT.md +1 -1
  59. package/docs/SKILL.md +24 -14
  60. package/docs/TUI.md +24 -0
  61. package/package.json +3 -3
  62. package/dist/agent-DHQAsdj6.d.ts.map +0 -1
  63. package/dist/index-CHSaLab5.d.ts.map +0 -1
  64. package/dist/index-CrqFoaQA.d.ts.map +0 -1
  65. package/dist/interpolate-ERgZUxgg.js.map +0 -1
  66. package/dist/tools-PQH1Ge4M.js.map +0 -1
  67. package/dist/transcript-anchors-ByB2MSCB.d.ts.map +0 -1
  68. package/dist/turn-operations-Bqs4YbbH.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,18 +1,1037 @@
1
- import { A as readStateKey, C as PERSISTENCE_PREVIEW_BYTES, D as resolvePersistDir, E as maybePersistToolResult, O as getReadState, S as PERSISTED_STUB_PREFIX, T as cleanupPersistedSession, _ as createSkillsReadTool, a as multiEdit, b as TOOL_USE_SKIPPED_MESSAGE, c as grep, g as createSkillsRunScriptTool, h as createSkillsUseTool, j as resolveReadStateMap, l as glob, n as createSpawnTool, p as createAgent, s as createInteractionTool, u as edit, v as INTERRUPT_MESSAGE_FOR_TOOL_USE, w as buildPersistedStub, x as validateToolArgs, y as SHELL_CASCADE_CANCEL_MESSAGE } from "./tools-PQH1Ge4M.js";
1
+ import { C as PERSISTED_STUB_PREFIX, D as maybePersistToolResult, E as cleanupPersistedSession, M as resolveReadStateMap, O as resolvePersistDir, S as validateToolArgs, T as buildPersistedStub, _ as createSkillsReadTool, a as multiEdit, b as TOOL_USE_CANCELLED_MESSAGE, c as grep, g as createSkillsRunScriptTool, h as createSkillsUseTool, j as readStateKey, k as getReadState, l as glob, n as createSpawnTool, p as createAgent, s as createInteractionTool, u as edit, v as INTERRUPT_MESSAGE_FOR_TOOL_USE, w as PERSISTENCE_PREVIEW_BYTES, x as TOOL_USE_SKIPPED_MESSAGE, y as SHELL_CASCADE_CANCEL_MESSAGE } from "./tools-BK2vG9UX.js";
2
2
  import { n as createProcessContext, t as createSandboxContext } from "./contexts-BwiHIr2w.js";
3
- import { a as AgentToolPairingError, c as matchesContextExceeded, i as AgentToolNotAllowedError, l as toTypedError, n as AgentContextExceededError, o as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-Byb0F8B9.js";
3
+ import { a as AgentToolPairingError, c as matchesContextExceeded, i as AgentToolNotAllowedError, l as toTypedError, n as AgentContextExceededError, o as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, r as AgentProviderError, s as errorMessage, t as AgentAbortedError } from "./errors-CDwtPIMX.js";
4
4
  import { n as toolResultToText, t as toolOutputByteLength } from "./types-IcokUOyC.js";
5
- import { a as detectTurnInterruption, b as sanitizeToolSpecs, c as fromAnthropic, d as toOpenAI, f as OpenAICompatHttpError, g as openaiCompat, h as mapOAIFinishReason, i as autoDetectAndConvert, l as fromOpenAI, m as classifyOpenAICompatError, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, r as TOOL_USE_INTERRUPTED_MARKER, s as filterUnresolvedToolUses, t as ORPHANED_TOOL_RESULT_MARKER, u as toAnthropic, y as sanitizeToolSchema } from "./messages-D0xT979U.js";
6
- import { c as createMemoryMcpCredentialStore, i as resultToString, l as hasAuthorizationHeader, n as normalizeMcpBlocks, r as normalizeMcpServers, s as McpOAuthProvider, t as connectMcpServers } from "./mcp-DhmmJfxK.js";
7
- import { _ as validateResourcePath, a as discoverSkills, b as createSkillActivationState, f as IMPLICITLY_ALLOWED_SKILL_TOOLS, g as parseAllowedToolPattern, h as matchesAllowedTool, i as writeSkillsToDisk, l as parseSkillFile, m as isToolAllowedByUnion, n as resolveSkills, p as installAllowedToolsGate, r as writeSkillToDisk, t as interpolateShellCommands, u as buildCatalog, v as validateSkillForWrite, y as validateSkillName } from "./interpolate-ERgZUxgg.js";
8
- import { C as summaryToTurn, E as CompactPromptTooLongError, S as stripImagesFromTurns, T as CompactInvalidInputError, _ as buildTailCompactPrompt, a as selectFilesFromSession, b as anchorPreviewFor, c as BYTES_PER_TOKEN, d as BASE_INSTRUCTIONS, f as NO_TOOLS_PREAMBLE, g as buildFullCompactPrompt, h as buildFromCompactPrompt, i as selectFilesFromReadState, l as estimateTokens, m as buildCompactPrompt, n as startOAuthCallback, o as selectRecentFiles, p as TRAILER, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer, u as utf8ByteLength, v as buildUpToCompactPrompt, w as truncateHeadForPtlRetry, x as sliceForCompaction, y as ANCHOR_PREVIEW_MAX_CHARS } from "./login-8c5C0FYq.js";
5
+ import { a as detectTurnInterruption, b as sanitizeToolSpecs, c as fromAnthropic, d as toOpenAI, f as OpenAICompatHttpError, g as openaiCompat, h as mapOAIFinishReason, i as autoDetectAndConvert, l as fromOpenAI, m as classifyOpenAICompatError, n as SYNTHETIC_TOOL_RESULT_PLACEHOLDER, o as ensureToolResultPairing, r as TOOL_USE_INTERRUPTED_MARKER, s as filterUnresolvedToolUses, t as ORPHANED_TOOL_RESULT_MARKER, u as toAnthropic, y as sanitizeToolSchema } from "./messages-fTR19Ga6.js";
6
+ import { c as createMemoryMcpCredentialStore, i as resultToString, l as hasAuthorizationHeader, n as normalizeMcpBlocks, r as normalizeMcpServers, s as McpOAuthProvider, t as connectMcpServers } from "./mcp-CNUbvbsy.js";
7
+ import { _ as validateResourcePath, a as discoverSkills, b as createSkillActivationState, f as IMPLICITLY_ALLOWED_SKILL_TOOLS, g as parseAllowedToolPattern, h as matchesAllowedTool, i as writeSkillsToDisk, l as parseSkillFile, m as isToolAllowedByUnion, n as resolveSkills, p as installAllowedToolsGate, r as writeSkillToDisk, t as interpolateShellCommands, u as buildCatalog, v as validateSkillForWrite, y as validateSkillName } from "./interpolate-CmtjEyRJ.js";
8
+ import { C as summaryToTurn, E as CompactPromptTooLongError, S as stripImagesFromTurns, T as CompactInvalidInputError, _ as buildTailCompactPrompt, a as selectFilesFromSession, b as anchorPreviewFor, c as BYTES_PER_TOKEN, d as BASE_INSTRUCTIONS, f as NO_TOOLS_PREAMBLE, g as buildFullCompactPrompt, h as buildFromCompactPrompt, i as selectFilesFromReadState, l as estimateTokens, m as buildCompactPrompt, n as startOAuthCallback, o as selectRecentFiles, p as TRAILER, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer, u as utf8ByteLength, v as buildUpToCompactPrompt, w as truncateHeadForPtlRetry, x as sliceForCompaction, y as ANCHOR_PREVIEW_MAX_CHARS } from "./login-DxyAERe1.js";
9
9
  import { r as statsByModel, t as flattenTurns } from "./stats-DgOvY7wd.js";
10
- import { i as basic_default, n as definePreset, r as basicTools } from "./presets-Ck4VusTo.js";
11
- import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-x3LZByR5.js";
12
- import { a as createFileMapStore, i as createMemoryStore, n as loadSession, r as createRemoteStore, t as createSession } from "./session-BHZwxmfr.js";
10
+ import { i as basic_default, n as definePreset, r as basicTools } from "./presets-D9IbaI40.js";
11
+ import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-CEzRFYtS.js";
12
+ import { a as createFileMapStore, i as createMemoryStore, n as loadSession, r as createRemoteStore, t as createSession } from "./session-kwsNnOmt.js";
13
13
  import { defineSkill } from "./skills.js";
14
+ //#region src/logger.ts
15
+ /**
16
+ * Build a Logger from a sink. Stateless and cheap; create one per agent
17
+ * (or per app) and use `.with()` to attach correlation ids per-call.
18
+ */
19
+ function createLogger(sink, baseAttributes = {}) {
20
+ function emit(level, message, attrs) {
21
+ try {
22
+ sink.emit({
23
+ level,
24
+ timestamp: Date.now(),
25
+ message,
26
+ attrs: attrs ? {
27
+ ...baseAttributes,
28
+ ...attrs
29
+ } : { ...baseAttributes }
30
+ });
31
+ } catch {}
32
+ }
33
+ return {
34
+ debug: (m, a) => emit("debug", m, a),
35
+ info: (m, a) => emit("info", m, a),
36
+ warn: (m, a) => emit("warn", m, a),
37
+ error: (m, a) => emit("error", m, a),
38
+ with: (extra) => createLogger(sink, {
39
+ ...baseAttributes,
40
+ ...extra
41
+ }),
42
+ baseAttributes
43
+ };
44
+ }
45
+ const LEVEL_ORDER = {
46
+ debug: 0,
47
+ info: 1,
48
+ warn: 2,
49
+ error: 3
50
+ };
51
+ /**
52
+ * Human-readable terminal sink. Renders each record as
53
+ * `<ISO timestamp> <LEVEL> <message> <attrs as kv pairs>`.
54
+ *
55
+ * Honors `process.stderr` by default so log lines don't interleave with
56
+ * the agent's stdout-bound output (chat responses, JSON results).
57
+ */
58
+ function consoleSink(options = {}) {
59
+ const min = LEVEL_ORDER[options.minLevel ?? "info"];
60
+ const stream = options.stream ?? process.stderr;
61
+ return { emit(record) {
62
+ if (LEVEL_ORDER[record.level] < min) return;
63
+ const ts = new Date(record.timestamp).toISOString();
64
+ const kv = Object.entries(record.attrs).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
65
+ stream.write(`${ts} ${record.level.toUpperCase().padEnd(5)} ${record.message}${kv ? ` ${kv}` : ""}\n`);
66
+ } };
67
+ }
68
+ /**
69
+ * One-JSON-object-per-line sink. Suitable for piping into log aggregators
70
+ * (Datadog Agent, Fluent Bit, Loki, Vector) that expect JSONL.
71
+ */
72
+ function jsonSink(options = {}) {
73
+ const min = LEVEL_ORDER[options.minLevel ?? "info"];
74
+ const stream = options.stream ?? process.stderr;
75
+ return { emit(record) {
76
+ if (LEVEL_ORDER[record.level] < min) return;
77
+ try {
78
+ stream.write(`${JSON.stringify(record)}\n`);
79
+ } catch {}
80
+ } };
81
+ }
82
+ /**
83
+ * Install a bundle of hook handlers that emit a structured line per
84
+ * relevant lifecycle event, automatically attaching correlation ids
85
+ * (`runId`, `turnId`, `callId`, `childId`, `depth`, `agentName`).
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * const logger = createLogger(consoleSink({ minLevel: 'debug' }), { service: 'tui' })
90
+ * const lh = createLoggingHooks({ logger })
91
+ * const uninstall = lh.install(agent.hooks)
92
+ * try { await agent.run({ prompt }) }
93
+ * finally { uninstall() }
94
+ * ```
95
+ */
96
+ function createLoggingHooks(options) {
97
+ const root = options.logger;
98
+ const includeLifecycle = options.includeLifecycle ?? true;
99
+ const minLevel = LEVEL_ORDER[options.level ?? "info"];
100
+ /**
101
+ * Wrap a Logger so emissions below `minLevel` are dropped before they
102
+ * hit the sink. Lets us gate harness chatter without forcing every
103
+ * `LogSink` to re-implement level filtering. Preserves `.with()`
104
+ * composition (children inherit the filter).
105
+ */
106
+ function gateLevel(logger) {
107
+ const skip = (level) => LEVEL_ORDER[level] < minLevel;
108
+ const wrap = (l) => ({
109
+ debug: (m, a) => {
110
+ if (!skip("debug")) l.debug(m, a);
111
+ },
112
+ info: (m, a) => {
113
+ if (!skip("info")) l.info(m, a);
114
+ },
115
+ warn: (m, a) => {
116
+ if (!skip("warn")) l.warn(m, a);
117
+ },
118
+ error: (m, a) => {
119
+ if (!skip("error")) l.error(m, a);
120
+ },
121
+ with: (extra) => wrap(l.with(extra)),
122
+ baseAttributes: l.baseAttributes
123
+ });
124
+ return wrap(logger);
125
+ }
126
+ return { install(hooks) {
127
+ const unregisters = [];
128
+ const gatedRoot = gateLevel(root);
129
+ let runLogger = gatedRoot;
130
+ let turnLogger = gatedRoot;
131
+ unregisters.push(hooks.hook("agent:start", (ctx) => {
132
+ runLogger = gatedRoot.with({
133
+ runId: ctx.runId,
134
+ ...ctx.parentRunId ? { parentRunId: ctx.parentRunId } : {},
135
+ depth: ctx.depth,
136
+ ...ctx.agentName ? { agentName: ctx.agentName } : {}
137
+ });
138
+ turnLogger = runLogger;
139
+ if (includeLifecycle) runLogger.debug("agent run started");
140
+ }));
141
+ unregisters.push(hooks.hook("agent:done", (stats) => {
142
+ runLogger.info("agent run completed", {
143
+ turns: stats.turns,
144
+ totalIn: stats.totalIn,
145
+ totalOut: stats.totalOut,
146
+ ...typeof stats.cost === "number" ? { cost: stats.cost } : {},
147
+ elapsedMs: stats.elapsed,
148
+ ...typeof stats.timeTillFirstTokenMs === "number" ? { ttftMs: stats.timeTillFirstTokenMs } : {}
149
+ });
150
+ }));
151
+ unregisters.push(hooks.hook("agent:abort", () => {
152
+ runLogger.warn("agent run aborted");
153
+ }));
154
+ unregisters.push(hooks.hook("turn:before", (ctx) => {
155
+ turnLogger = runLogger.with({
156
+ turnId: ctx.turnId,
157
+ turn: ctx.turn
158
+ });
159
+ if (includeLifecycle) turnLogger.debug("turn started");
160
+ }));
161
+ unregisters.push(hooks.hook("turn:after", (ctx) => {
162
+ turnLogger.debug("turn ended", {
163
+ inputTokens: ctx.usage.input,
164
+ outputTokens: ctx.usage.output,
165
+ ...ctx.usage.finishReason ? { finishReason: ctx.usage.finishReason } : {},
166
+ ...ctx.usage.modelId ? { modelId: ctx.usage.modelId } : {},
167
+ ...typeof ctx.usage.timeToFirstTokenMs === "number" ? { ttftMs: ctx.usage.timeToFirstTokenMs } : {}
168
+ });
169
+ }));
170
+ unregisters.push(hooks.hook("stream:error", (ctx) => {
171
+ turnLogger.error("stream error", {
172
+ message: ctx.err instanceof Error ? ctx.err.message : String(ctx.err),
173
+ ...ctx.statusCode !== void 0 ? { statusCode: ctx.statusCode } : {},
174
+ ...ctx.requestId !== void 0 ? { requestId: ctx.requestId } : {}
175
+ });
176
+ }));
177
+ unregisters.push(hooks.hook("tool:before", (ctx) => {
178
+ if (!includeLifecycle) return;
179
+ turnLogger.debug("tool started", {
180
+ toolName: ctx.name,
181
+ displayName: ctx.displayName,
182
+ callId: ctx.callId
183
+ });
184
+ }));
185
+ unregisters.push(hooks.hook("tool:after", (ctx) => {
186
+ if (!includeLifecycle) return;
187
+ turnLogger.debug("tool ended", {
188
+ toolName: ctx.name,
189
+ callId: ctx.callId,
190
+ outputBytes: ctx.outputBytes
191
+ });
192
+ }));
193
+ unregisters.push(hooks.hook("tool:error", (ctx) => {
194
+ turnLogger.error("tool error", {
195
+ toolName: ctx.name,
196
+ callId: ctx.callId,
197
+ message: ctx.error.message
198
+ });
199
+ }));
200
+ unregisters.push(hooks.hook("tool:dispatched", (ctx) => {
201
+ const isAnomaly = ctx.outcome === "gate-block" || ctx.outcome === "unknown" || ctx.outcome === "invalid-input";
202
+ if (!isAnomaly && !includeLifecycle) return;
203
+ turnLogger[isAnomaly ? "warn" : "debug"]("tool dispatched", {
204
+ toolName: ctx.name,
205
+ callId: ctx.callId,
206
+ outcome: ctx.outcome,
207
+ ...ctx.reason ? { reason: ctx.reason } : {}
208
+ });
209
+ }));
210
+ unregisters.push(hooks.hook("validation:reject", (ctx) => {
211
+ turnLogger.warn("tool input rejected", {
212
+ toolName: ctx.name,
213
+ callId: ctx.callId,
214
+ reason: ctx.reason
215
+ });
216
+ }));
217
+ unregisters.push(hooks.hook("budget:exceeded", (ctx) => {
218
+ turnLogger.warn("byte budget exceeded", {
219
+ bytes: ctx.bytes,
220
+ budget: ctx.budget
221
+ });
222
+ }));
223
+ unregisters.push(hooks.hook("tool-budget:exceeded", (ctx) => {
224
+ turnLogger.warn("tool budget exceeded", {
225
+ toolName: ctx.tool,
226
+ count: ctx.count,
227
+ max: ctx.max,
228
+ mode: ctx.mode
229
+ });
230
+ }));
231
+ unregisters.push(hooks.hook("mcp:bootstrap:end", (ctx) => {
232
+ if (ctx.ok) {
233
+ if (includeLifecycle) runLogger.debug("mcp bootstrap ok", {
234
+ server: ctx.name,
235
+ transport: ctx.transport,
236
+ durationMs: ctx.durationMs,
237
+ toolCount: ctx.toolCount,
238
+ ...ctx.lazy ? { lazy: true } : {},
239
+ ...ctx.cached ? { cached: true } : {}
240
+ });
241
+ } else runLogger.warn("mcp bootstrap failed", {
242
+ server: ctx.name,
243
+ transport: ctx.transport,
244
+ durationMs: ctx.durationMs,
245
+ message: ctx.error.message
246
+ });
247
+ }));
248
+ unregisters.push(hooks.hook("mcp:error", (ctx) => {
249
+ runLogger.error("mcp error", {
250
+ server: ctx.name,
251
+ message: ctx.error.message
252
+ });
253
+ }));
254
+ unregisters.push(hooks.hook("mcp:auth:required", (ctx) => {
255
+ runLogger.warn("mcp auth required", {
256
+ server: ctx.name,
257
+ transport: ctx.transport,
258
+ reason: ctx.reason
259
+ });
260
+ }));
261
+ unregisters.push(hooks.hook("mcp:tool:error", (ctx) => {
262
+ turnLogger.error("mcp tool error", {
263
+ server: ctx.server,
264
+ tool: ctx.displayName,
265
+ callId: ctx.callId,
266
+ message: ctx.error.message
267
+ });
268
+ }));
269
+ unregisters.push(hooks.hook("spawn:before", (ctx) => {
270
+ if (!includeLifecycle) return;
271
+ runLogger.debug("spawn started", {
272
+ childId: ctx.id,
273
+ depth: ctx.depth
274
+ });
275
+ }));
276
+ unregisters.push(hooks.hook("spawn:complete", (ctx) => {
277
+ runLogger.info("spawn completed", {
278
+ childId: ctx.id,
279
+ ...ctx.depth ? { depth: ctx.depth } : {},
280
+ status: ctx.status ?? "completed",
281
+ turns: ctx.stats.turns,
282
+ totalIn: ctx.stats.totalIn,
283
+ totalOut: ctx.stats.totalOut,
284
+ ...typeof ctx.stats.cost === "number" ? { cost: ctx.stats.cost } : {}
285
+ });
286
+ }));
287
+ unregisters.push(hooks.hook("spawn:error", (ctx) => {
288
+ runLogger.error("spawn error", {
289
+ childId: ctx.id,
290
+ ...ctx.depth ? { depth: ctx.depth } : {},
291
+ message: ctx.error.message
292
+ });
293
+ }));
294
+ let disposed = false;
295
+ return function uninstall() {
296
+ if (disposed) return;
297
+ disposed = true;
298
+ for (const un of unregisters) try {
299
+ un();
300
+ } catch {}
301
+ };
302
+ } };
303
+ }
304
+ //#endregion
305
+ //#region src/metrics.ts
306
+ function prefixed$1(prefix, name) {
307
+ return prefix ? `${prefix}${name}` : name;
308
+ }
309
+ /**
310
+ * Drop `undefined` entries before handing attributes to a metrics
311
+ * backend. OTel's `@opentelemetry/api` rejects `undefined` attribute
312
+ * values (and Prometheus / StatsD adapters silently mis-label them); the
313
+ * helper's own API surface allows `undefined` for ergonomic call sites
314
+ * (`{...(cond ? { foo } : {})}`-style spreads), so we strip here at the
315
+ * boundary.
316
+ */
317
+ function mergeAttrs(base, extra) {
318
+ const out = {};
319
+ if (base) {
320
+ for (const [k, v] of Object.entries(base)) if (v !== void 0) out[k] = v;
321
+ }
322
+ for (const [k, v] of Object.entries(extra)) if (v !== void 0) out[k] = v;
323
+ return out;
324
+ }
325
+ /**
326
+ * Build a set of metrics hook handlers that can be installed on an agent.
327
+ *
328
+ * @example OpenTelemetry
329
+ * ```ts
330
+ * import { metrics } from '@opentelemetry/api'
331
+ * const meter = metrics.getMeter('zidane')
332
+ * const m = createMetricsHooks({ meter, baseAttributes: { service: 'tui' } })
333
+ * const uninstall = m.install(agent.hooks)
334
+ * try { await agent.run({ prompt }) }
335
+ * finally { uninstall() }
336
+ * ```
337
+ */
338
+ function createMetricsHooks(options) {
339
+ const ns = options.namespace;
340
+ const base = options.baseAttributes;
341
+ const onError = options.onError ?? (() => {});
342
+ const make = {
343
+ counter: (name, opts) => safeFactory(() => options.meter.createCounter(prefixed$1(ns, name), opts), name, onError),
344
+ histogram: (name, opts) => safeFactory(() => options.meter.createHistogram(prefixed$1(ns, name), opts), name, onError),
345
+ upDown: (name, opts) => safeFactory(() => options.meter.createUpDownCounter(prefixed$1(ns, name), opts), name, onError)
346
+ };
347
+ const turnDuration = make.histogram("gen_ai.client.operation.duration", {
348
+ unit: "ms",
349
+ description: "Per-turn LLM operation duration."
350
+ });
351
+ const tokenUsage = make.histogram("gen_ai.client.token.usage", {
352
+ unit: "tokens",
353
+ description: "Per-turn token usage (tagged by gen_ai.token.type)."
354
+ });
355
+ const ttft = make.histogram("gen_ai.client.time_to_first_token", {
356
+ unit: "ms",
357
+ description: "Per-turn time to first token."
358
+ });
359
+ const toolDuration = make.histogram("gen_ai.tool.duration", {
360
+ unit: "ms",
361
+ description: "Native tool execution wall clock."
362
+ });
363
+ const toolOutput = make.histogram("gen_ai.tool.output_bytes", {
364
+ unit: "By",
365
+ description: "Native tool output bytes."
366
+ });
367
+ const mcpToolDuration = make.histogram("gen_ai.mcp.tool.duration", {
368
+ unit: "ms",
369
+ description: "MCP tool execution wall clock."
370
+ });
371
+ const mcpBootstrapDuration = make.histogram("gen_ai.mcp.bootstrap.duration", {
372
+ unit: "ms",
373
+ description: "MCP server bootstrap duration."
374
+ });
375
+ const runsStarted = make.counter("gen_ai.agent.runs", { description: "Runs started." });
376
+ const runsCompleted = make.counter("gen_ai.agent.runs.completed", { description: "Runs completed." });
377
+ const aborts = make.counter("gen_ai.agent.aborts", { description: "Run abort events." });
378
+ const toolCalls = make.counter("gen_ai.tool.calls", { description: "Tool dispatches (tagged by outcome)." });
379
+ const toolErrors = make.counter("gen_ai.tool.errors", { description: "Native tool errors." });
380
+ const mcpToolErrors = make.counter("gen_ai.mcp.tool.errors", { description: "MCP tool errors." });
381
+ const streamErrors = make.counter("gen_ai.stream.errors", { description: "Provider stream errors." });
382
+ const validationRejects = make.counter("gen_ai.validation.rejects", { description: "Tool input validation rejects." });
383
+ const validationCoercions = make.counter("gen_ai.validation.coercions", { description: "Tool input auto-coercions." });
384
+ const gateBlocks = make.counter("gen_ai.gate.blocks", { description: "Tool-gate refusals." });
385
+ const budgetExceeded = make.counter("gen_ai.budget.exceeded", { description: "Per-turn byte budget exceedances." });
386
+ const toolBudgetExceeded = make.counter("gen_ai.tool_budget.exceeded", { description: "Per-tool dispatch budget exceedances." });
387
+ const pairingRepairs = make.counter("gen_ai.pairing.repairs", { description: "Tool-pairing repair counts." });
388
+ const oauthRefreshes = make.counter("gen_ai.oauth.refreshes", { description: "Provider OAuth credential refreshes." });
389
+ const mcpErrors = make.counter("gen_ai.mcp.errors", { description: "MCP server errors." });
390
+ const mcpAuthRequired = make.counter("gen_ai.mcp.auth.required", { description: "MCP bootstrap blocked on missing auth." });
391
+ const costMeter = make.counter("gen_ai.cost_usd", {
392
+ unit: "USD",
393
+ description: "Cumulative cost across turns."
394
+ });
395
+ const runsActive = make.upDown("gen_ai.agent.runs.active", { description: "In-flight agent runs." });
396
+ return { install(hooks) {
397
+ const turnStart = /* @__PURE__ */ new Map();
398
+ const toolStart = /* @__PURE__ */ new Map();
399
+ const mcpStart = /* @__PURE__ */ new Map();
400
+ const unregisters = [];
401
+ const record = (instrument, value, attrs, kind) => {
402
+ if (!instrument) return;
403
+ try {
404
+ instrument.record(value, mergeAttrs(base, attrs));
405
+ } catch (err) {
406
+ try {
407
+ onError(kind, err);
408
+ } catch {}
409
+ }
410
+ };
411
+ const add = (instrument, value, attrs, kind) => {
412
+ if (!instrument) return;
413
+ try {
414
+ instrument.add(value, mergeAttrs(base, attrs));
415
+ } catch (err) {
416
+ try {
417
+ onError(kind, err);
418
+ } catch {}
419
+ }
420
+ };
421
+ unregisters.push(hooks.hook("agent:start", (ctx) => {
422
+ const attrs = {
423
+ "gen_ai.agent.depth": ctx.depth,
424
+ ...ctx.agentName ? { "gen_ai.agent.name": ctx.agentName } : {}
425
+ };
426
+ add(runsStarted, 1, attrs, "runs");
427
+ add(runsActive, 1, attrs, "runs.active");
428
+ }));
429
+ unregisters.push(hooks.hook("agent:done", () => {
430
+ add(runsCompleted, 1, {}, "runs.completed");
431
+ add(runsActive, -1, {}, "runs.active");
432
+ }));
433
+ unregisters.push(hooks.hook("agent:abort", () => {
434
+ add(aborts, 1, {}, "aborts");
435
+ }));
436
+ unregisters.push(hooks.hook("turn:before", (ctx) => {
437
+ turnStart.set(ctx.turnId, Date.now());
438
+ }));
439
+ unregisters.push(hooks.hook("turn:after", (ctx) => {
440
+ const started = turnStart.get(ctx.turnId);
441
+ turnStart.delete(ctx.turnId);
442
+ const tagsBase = {
443
+ "gen_ai.operation.name": "chat",
444
+ ...ctx.usage.modelId ? { "gen_ai.response.model": ctx.usage.modelId } : {},
445
+ ...ctx.usage.finishReason ? { "gen_ai.response.finish_reason": ctx.usage.finishReason } : {}
446
+ };
447
+ if (typeof started === "number") record(turnDuration, Date.now() - started, tagsBase, "turn.duration");
448
+ if (typeof ctx.usage.timeToFirstTokenMs === "number") record(ttft, ctx.usage.timeToFirstTokenMs, tagsBase, "ttft");
449
+ record(tokenUsage, ctx.usage.input, {
450
+ ...tagsBase,
451
+ "gen_ai.token.type": "input"
452
+ }, "token.input");
453
+ record(tokenUsage, ctx.usage.output, {
454
+ ...tagsBase,
455
+ "gen_ai.token.type": "output"
456
+ }, "token.output");
457
+ if (typeof ctx.usage.cacheRead === "number" && ctx.usage.cacheRead > 0) record(tokenUsage, ctx.usage.cacheRead, {
458
+ ...tagsBase,
459
+ "gen_ai.token.type": "cache_read"
460
+ }, "token.cache_read");
461
+ if (typeof ctx.usage.cacheCreation === "number" && ctx.usage.cacheCreation > 0) record(tokenUsage, ctx.usage.cacheCreation, {
462
+ ...tagsBase,
463
+ "gen_ai.token.type": "cache_creation"
464
+ }, "token.cache_creation");
465
+ if (typeof ctx.usage.cost === "number" && ctx.usage.cost > 0) add(costMeter, ctx.usage.cost, tagsBase, "cost");
466
+ }));
467
+ unregisters.push(hooks.hook("stream:error", (ctx) => {
468
+ add(streamErrors, 1, {
469
+ "http.response.status_code": ctx.statusCode,
470
+ "error.type": ctx.err instanceof Error ? ctx.err.name : "unknown"
471
+ }, "stream.error");
472
+ }));
473
+ unregisters.push(hooks.hook("tool:before", (ctx) => {
474
+ toolStart.set(ctx.callId, Date.now());
475
+ }));
476
+ unregisters.push(hooks.hook("tool:after", (ctx) => {
477
+ const started = toolStart.get(ctx.callId);
478
+ toolStart.delete(ctx.callId);
479
+ const tags = { "gen_ai.tool.name": ctx.name };
480
+ if (typeof started === "number") record(toolDuration, Date.now() - started, tags, "tool.duration");
481
+ record(toolOutput, ctx.outputBytes, tags, "tool.output_bytes");
482
+ }));
483
+ unregisters.push(hooks.hook("tool:error", (ctx) => {
484
+ toolStart.delete(ctx.callId);
485
+ add(toolErrors, 1, {
486
+ "gen_ai.tool.name": ctx.name,
487
+ "error.type": ctx.error.name
488
+ }, "tool.errors");
489
+ }));
490
+ unregisters.push(hooks.hook("tool:dispatched", (ctx) => {
491
+ add(toolCalls, 1, {
492
+ "gen_ai.tool.name": ctx.name,
493
+ "outcome": ctx.outcome
494
+ }, "tool.calls");
495
+ if (ctx.outcome === "gate-block") add(gateBlocks, 1, {
496
+ "gen_ai.tool.name": ctx.name,
497
+ ...ctx.reason ? { reason: ctx.reason } : {}
498
+ }, "gate.blocks");
499
+ }));
500
+ unregisters.push(hooks.hook("validation:reject", (ctx) => {
501
+ add(validationRejects, 1, { "gen_ai.tool.name": ctx.name }, "validation.rejects");
502
+ }));
503
+ unregisters.push(hooks.hook("validation:coerce", (ctx) => {
504
+ add(validationCoercions, 1, {
505
+ "gen_ai.tool.name": ctx.name,
506
+ "coercions": ctx.coercions.length
507
+ }, "validation.coercions");
508
+ }));
509
+ unregisters.push(hooks.hook("budget:exceeded", (ctx) => {
510
+ add(budgetExceeded, 1, {
511
+ bytes: ctx.bytes,
512
+ budget: ctx.budget
513
+ }, "budget.exceeded");
514
+ }));
515
+ unregisters.push(hooks.hook("tool-budget:exceeded", (ctx) => {
516
+ add(toolBudgetExceeded, 1, {
517
+ "gen_ai.tool.name": ctx.tool,
518
+ "mode": ctx.mode,
519
+ "count": ctx.count,
520
+ "max": ctx.max
521
+ }, "tool_budget.exceeded");
522
+ }));
523
+ unregisters.push(hooks.hook("pairing:repair", (ctx) => {
524
+ add(pairingRepairs, 1, { mode: ctx.mode }, "pairing.repairs");
525
+ }));
526
+ unregisters.push(hooks.hook("oauth:refresh", (ctx) => {
527
+ add(oauthRefreshes, 1, {
528
+ provider: ctx.provider,
529
+ source: ctx.source
530
+ }, "oauth.refreshes");
531
+ }));
532
+ unregisters.push(hooks.hook("mcp:tool:before", (ctx) => {
533
+ mcpStart.set(ctx.callId, Date.now());
534
+ }));
535
+ unregisters.push(hooks.hook("mcp:tool:after", (ctx) => {
536
+ const started = mcpStart.get(ctx.callId);
537
+ mcpStart.delete(ctx.callId);
538
+ const tags = {
539
+ "gen_ai.mcp.server": ctx.server,
540
+ "gen_ai.tool.name": ctx.displayName
541
+ };
542
+ if (typeof started === "number") record(mcpToolDuration, Date.now() - started, tags, "mcp.tool.duration");
543
+ }));
544
+ unregisters.push(hooks.hook("mcp:tool:error", (ctx) => {
545
+ mcpStart.delete(ctx.callId);
546
+ add(mcpToolErrors, 1, {
547
+ "gen_ai.mcp.server": ctx.server,
548
+ "gen_ai.tool.name": ctx.displayName,
549
+ "error.type": ctx.error.name
550
+ }, "mcp.tool.errors");
551
+ }));
552
+ unregisters.push(hooks.hook("mcp:bootstrap:end", (ctx) => {
553
+ record(mcpBootstrapDuration, ctx.durationMs, {
554
+ "gen_ai.mcp.server": ctx.name,
555
+ "ok": ctx.ok
556
+ }, "mcp.bootstrap.duration");
557
+ }));
558
+ unregisters.push(hooks.hook("mcp:error", (ctx) => {
559
+ add(mcpErrors, 1, {
560
+ "gen_ai.mcp.server": ctx.name,
561
+ "error.type": ctx.error.name
562
+ }, "mcp.errors");
563
+ }));
564
+ unregisters.push(hooks.hook("mcp:auth:required", (ctx) => {
565
+ add(mcpAuthRequired, 1, {
566
+ "gen_ai.mcp.server": ctx.name,
567
+ "transport": ctx.transport,
568
+ "reason": ctx.reason
569
+ }, "mcp.auth.required");
570
+ }));
571
+ let disposed = false;
572
+ return function uninstall() {
573
+ if (disposed) return;
574
+ disposed = true;
575
+ for (const un of unregisters) try {
576
+ un();
577
+ } catch {}
578
+ turnStart.clear();
579
+ toolStart.clear();
580
+ mcpStart.clear();
581
+ };
582
+ } };
583
+ }
584
+ /**
585
+ * Defensive factory wrapper — returns undefined when meter creation
586
+ * throws. Lets a half-broken meter (one unsupported instrument) still
587
+ * give partial metrics rather than collapsing the whole install.
588
+ */
589
+ function safeFactory(factory, name, onError) {
590
+ try {
591
+ return factory();
592
+ } catch (err) {
593
+ try {
594
+ onError(`createInstrument:${name}`, err);
595
+ } catch {}
596
+ return;
597
+ }
598
+ }
599
+ //#endregion
600
+ //#region src/run-summary.ts
601
+ /**
602
+ * Build a run-summary collector. State is created fresh inside each
603
+ * `install()` call, so a single collector instance can be installed
604
+ * across multiple agents without attribution cross-talk. `latest()`
605
+ * returns the most-recent summary across **any** install — install
606
+ * per-agent collectors if you need separate post-run snapshots.
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * const collector = createRunSummaryCollector({
611
+ * onSummary: s => console.log(JSON.stringify(s)),
612
+ * })
613
+ * const uninstall = collector.install(agent.hooks)
614
+ * try { await agent.run({ prompt }) }
615
+ * finally { uninstall() }
616
+ * ```
617
+ */
618
+ function createRunSummaryCollector(options = {}) {
619
+ let last;
620
+ return {
621
+ latest: () => last,
622
+ install(hooks) {
623
+ let runId;
624
+ let parentRunId;
625
+ let depth = 0;
626
+ let agentName;
627
+ let startedAt = Date.now();
628
+ let aborted = false;
629
+ const errors = [];
630
+ const blocks = [];
631
+ const validationRejects = [];
632
+ const budgetEvents = [];
633
+ const pairingRepairs = {};
634
+ const children = [];
635
+ function resetForNewRun() {
636
+ aborted = false;
637
+ errors.length = 0;
638
+ blocks.length = 0;
639
+ validationRejects.length = 0;
640
+ budgetEvents.length = 0;
641
+ for (const k of Object.keys(pairingRepairs)) delete pairingRepairs[k];
642
+ children.length = 0;
643
+ }
644
+ const unregisters = [];
645
+ unregisters.push(hooks.hook("agent:start", (ctx) => {
646
+ resetForNewRun();
647
+ runId = ctx.runId;
648
+ parentRunId = ctx.parentRunId;
649
+ depth = ctx.depth;
650
+ agentName = ctx.agentName;
651
+ startedAt = ctx.startedAt;
652
+ }));
653
+ unregisters.push(hooks.hook("agent:abort", () => {
654
+ aborted = true;
655
+ }));
656
+ unregisters.push(hooks.hook("stream:error", (ctx) => {
657
+ const msg = ctx.err instanceof Error ? ctx.err.message : String(ctx.err);
658
+ const errorType = ctx.err instanceof Error ? ctx.err.name : "unknown";
659
+ errors.push({
660
+ kind: "stream",
661
+ message: msg,
662
+ errorType,
663
+ turnId: ctx.turnId,
664
+ ...ctx.statusCode !== void 0 ? { statusCode: ctx.statusCode } : {},
665
+ ...ctx.requestId !== void 0 ? { requestId: ctx.requestId } : {}
666
+ });
667
+ }));
668
+ unregisters.push(hooks.hook("tool:error", (ctx) => {
669
+ errors.push({
670
+ kind: "tool",
671
+ message: ctx.error.message,
672
+ errorType: ctx.error.name,
673
+ turnId: ctx.turnId,
674
+ callId: ctx.callId,
675
+ toolName: ctx.name
676
+ });
677
+ }));
678
+ unregisters.push(hooks.hook("mcp:tool:error", (ctx) => {
679
+ errors.push({
680
+ kind: "mcp-tool",
681
+ message: ctx.error.message,
682
+ errorType: ctx.error.name,
683
+ turnId: ctx.turnId,
684
+ callId: ctx.callId,
685
+ server: ctx.server,
686
+ toolName: ctx.displayName
687
+ });
688
+ }));
689
+ unregisters.push(hooks.hook("mcp:error", (ctx) => {
690
+ errors.push({
691
+ kind: "mcp",
692
+ message: ctx.error.message,
693
+ errorType: ctx.error.name,
694
+ server: ctx.name
695
+ });
696
+ }));
697
+ unregisters.push(hooks.hook("spawn:error", (ctx) => {
698
+ errors.push({
699
+ kind: "spawn",
700
+ message: ctx.error.message,
701
+ errorType: ctx.error.name,
702
+ childId: ctx.id
703
+ });
704
+ }));
705
+ unregisters.push(hooks.hook("tool:dispatched", (ctx) => {
706
+ if (ctx.outcome === "gate-block" || ctx.outcome === "unknown" || ctx.outcome === "invalid-input") blocks.push({
707
+ callId: ctx.callId,
708
+ toolName: ctx.name,
709
+ outcome: ctx.outcome,
710
+ ...ctx.reason ? { reason: ctx.reason } : {}
711
+ });
712
+ }));
713
+ unregisters.push(hooks.hook("validation:reject", (ctx) => {
714
+ validationRejects.push({
715
+ callId: ctx.callId,
716
+ toolName: ctx.name,
717
+ reason: ctx.reason
718
+ });
719
+ }));
720
+ unregisters.push(hooks.hook("budget:exceeded", (ctx) => {
721
+ budgetEvents.push({
722
+ kind: "bytes",
723
+ observed: ctx.bytes,
724
+ limit: ctx.budget,
725
+ turnId: ctx.turnId
726
+ });
727
+ }));
728
+ unregisters.push(hooks.hook("tool-budget:exceeded", (ctx) => {
729
+ budgetEvents.push({
730
+ kind: "tool-count",
731
+ toolName: ctx.tool,
732
+ mode: ctx.mode,
733
+ observed: ctx.count,
734
+ limit: ctx.max,
735
+ turnId: ctx.turnId
736
+ });
737
+ }));
738
+ unregisters.push(hooks.hook("pairing:repair", (ctx) => {
739
+ pairingRepairs[ctx.mode] = (pairingRepairs[ctx.mode] ?? 0) + 1;
740
+ }));
741
+ unregisters.push(hooks.hook("agent:done", (stats) => {
742
+ const endedAt = Date.now();
743
+ const byModel = [];
744
+ for (const [modelId, usage] of statsByModel(stats)) byModel.push({
745
+ modelId,
746
+ input: usage.input,
747
+ output: usage.output,
748
+ cacheRead: usage.cacheRead,
749
+ cacheCreation: usage.cacheCreation,
750
+ cost: usage.cost,
751
+ turns: usage.turns
752
+ });
753
+ for (const c of stats.children ?? []) children.push(minimalSummaryFromStats(c.stats, {
754
+ depth: c.depth ?? depth + 1,
755
+ status: c.status === "aborted" ? "aborted" : "completed"
756
+ }));
757
+ const summary = {
758
+ ...runId ? { runId } : {},
759
+ ...parentRunId ? { parentRunId } : {},
760
+ depth,
761
+ ...agentName ? { agentName } : {},
762
+ startedAt,
763
+ endedAt,
764
+ durationMs: endedAt - startedAt,
765
+ status: aborted ? "aborted" : "completed",
766
+ turns: stats.turns,
767
+ totals: buildTotals(stats),
768
+ byModel,
769
+ errors: errors.slice(),
770
+ blocks: blocks.slice(),
771
+ validationRejects: validationRejects.slice(),
772
+ budgetEvents: budgetEvents.slice(),
773
+ pairingRepairs: { ...pairingRepairs },
774
+ ...children.length > 0 ? { children: children.slice() } : {}
775
+ };
776
+ last = summary;
777
+ try {
778
+ options.onSummary?.(summary);
779
+ } catch {}
780
+ }));
781
+ let disposed = false;
782
+ return function uninstall() {
783
+ if (disposed) return;
784
+ disposed = true;
785
+ for (const un of unregisters) try {
786
+ un();
787
+ } catch {}
788
+ };
789
+ }
790
+ };
791
+ }
792
+ function buildTotals(stats) {
793
+ return {
794
+ input: stats.totalIn,
795
+ output: stats.totalOut,
796
+ cacheRead: stats.totalCacheRead,
797
+ cacheCreation: stats.totalCacheCreation,
798
+ ...typeof stats.cost === "number" ? { cost: stats.cost } : {},
799
+ ...typeof stats.timeTillFirstTokenMs === "number" ? { ttftMs: stats.timeTillFirstTokenMs } : {}
800
+ };
801
+ }
802
+ function minimalSummaryFromStats(stats, meta) {
803
+ const byModel = [];
804
+ for (const [modelId, usage] of statsByModel(stats)) byModel.push({
805
+ modelId,
806
+ input: usage.input,
807
+ output: usage.output,
808
+ cacheRead: usage.cacheRead,
809
+ cacheCreation: usage.cacheCreation,
810
+ cost: usage.cost,
811
+ turns: usage.turns
812
+ });
813
+ const children = [];
814
+ for (const c of stats.children ?? []) children.push(minimalSummaryFromStats(c.stats, {
815
+ depth: c.depth ?? meta.depth + 1,
816
+ status: c.status === "aborted" ? "aborted" : "completed"
817
+ }));
818
+ return {
819
+ depth: meta.depth,
820
+ startedAt: 0,
821
+ endedAt: 0,
822
+ durationMs: stats.elapsed,
823
+ status: meta.status,
824
+ turns: stats.turns,
825
+ totals: buildTotals(stats),
826
+ byModel,
827
+ errors: [],
828
+ blocks: [],
829
+ validationRejects: [],
830
+ budgetEvents: [],
831
+ pairingRepairs: {},
832
+ ...children.length > 0 ? { children } : {}
833
+ };
834
+ }
835
+ //#endregion
14
836
  //#region src/tracing.ts
15
837
  /**
838
+ * Stable keys we emit unconditionally — present in BOTH Sentry and OTel
839
+ * conventions with identical semantics, so no `conventions` branching is
840
+ * needed at the call site.
841
+ */
842
+ const GEN_AI = {
843
+ system: "gen_ai.system",
844
+ operationName: "gen_ai.operation.name",
845
+ requestModel: "gen_ai.request.model",
846
+ responseModel: "gen_ai.response.model",
847
+ responseFinishReasons: "gen_ai.response.finish_reasons",
848
+ responseId: "gen_ai.response.id",
849
+ responseStreaming: "gen_ai.response.streaming",
850
+ responseTokensPerSecond: "gen_ai.response.tokens_per_second",
851
+ responseTimeToFirstTokenSeconds: "gen_ai.response.time_to_first_token",
852
+ responseTimeToFirstTokenMs: "gen_ai.client.time_to_first_token",
853
+ usageInputTokens: "gen_ai.usage.input_tokens",
854
+ usageOutputTokens: "gen_ai.usage.output_tokens",
855
+ usageTotalTokens: "gen_ai.usage.total_tokens",
856
+ usageInputTokensCached: "gen_ai.usage.input_tokens.cached",
857
+ usageInputTokensCacheWrite: "gen_ai.usage.input_tokens.cache_write",
858
+ usageOutputTokensReasoning: "gen_ai.usage.output_tokens.reasoning",
859
+ usageCacheReadInputTokens: "gen_ai.usage.cache_read_input_tokens",
860
+ usageCacheCreationInputTokens: "gen_ai.usage.cache_creation_input_tokens",
861
+ usageReasoningTokens: "gen_ai.usage.reasoning_tokens",
862
+ costTotalTokens: "gen_ai.cost.total_tokens",
863
+ costInputTokens: "gen_ai.cost.input_tokens",
864
+ costOutputTokens: "gen_ai.cost.output_tokens",
865
+ usageCostUsd: "gen_ai.usage.cost_usd",
866
+ toolName: "gen_ai.tool.name",
867
+ toolDescription: "gen_ai.tool.description",
868
+ toolType: "gen_ai.tool.type",
869
+ toolCallId: "gen_ai.tool.call.id",
870
+ toolCallArguments: "gen_ai.tool.call.arguments",
871
+ toolCallResult: "gen_ai.tool.call.result",
872
+ toolMessage: "gen_ai.tool.message",
873
+ toolInputDeprecated: "gen_ai.tool.input",
874
+ toolOutputDeprecated: "gen_ai.tool.output",
875
+ requestMaxTokens: "gen_ai.request.max_tokens",
876
+ requestTemperature: "gen_ai.request.temperature",
877
+ requestTopP: "gen_ai.request.top_p",
878
+ requestTopK: "gen_ai.request.top_k",
879
+ requestSeed: "gen_ai.request.seed",
880
+ requestFrequencyPenalty: "gen_ai.request.frequency_penalty",
881
+ requestPresencePenalty: "gen_ai.request.presence_penalty",
882
+ inputMessages: "gen_ai.input.messages",
883
+ outputMessages: "gen_ai.output.messages",
884
+ systemInstructions: "gen_ai.system_instructions",
885
+ toolDefinitions: "gen_ai.tool.definitions",
886
+ agentName: "gen_ai.agent.name",
887
+ agentRunId: "gen_ai.agent.run.id",
888
+ agentParentRunId: "gen_ai.agent.parent_run.id",
889
+ agentDepth: "gen_ai.agent.depth",
890
+ pipelineName: "gen_ai.pipeline.name",
891
+ turn: "gen_ai.agent.turn",
892
+ mcpServer: "gen_ai.mcp.server",
893
+ mcpToolCount: "gen_ai.mcp.tool_count"
894
+ };
895
+ /** Compose namespace prefix once. */
896
+ function prefixed(namespace, name) {
897
+ return namespace ? `${namespace}/${name}` : name;
898
+ }
899
+ /** Drop undefined entries — tracers vary on how they render `undefined`. */
900
+ function compact(attrs) {
901
+ const out = {};
902
+ for (const [k, v] of Object.entries(attrs)) if (v !== void 0) out[k] = v;
903
+ return out;
904
+ }
905
+ function blockToParts(block) {
906
+ switch (block.type) {
907
+ case "text": return [{
908
+ type: "text",
909
+ content: typeof block.text === "string" ? block.text : ""
910
+ }];
911
+ case "image": return [{
912
+ type: "text",
913
+ content: "[Blob substitute]"
914
+ }];
915
+ case "tool_call": return [{
916
+ type: "tool_call",
917
+ ...typeof block.name === "string" ? { name: block.name } : {},
918
+ ...typeof block.id === "string" ? { call_id: block.id } : {},
919
+ arguments: safeJson(block.input)
920
+ }];
921
+ case "tool_result": {
922
+ const output = block.output;
923
+ const text = typeof output === "string" ? output : Array.isArray(output) ? output.map((b) => b.type === "image" ? "[Blob substitute]" : b.text ?? "").join("\n") : "";
924
+ return [{
925
+ type: "tool_result",
926
+ ...typeof block.callId === "string" ? { call_id: block.callId } : {},
927
+ content: text
928
+ }];
929
+ }
930
+ case "thinking": return [{
931
+ type: "reasoning",
932
+ content: typeof block.text === "string" ? block.text : ""
933
+ }];
934
+ default: return [];
935
+ }
936
+ }
937
+ function messageToSentry(msg) {
938
+ const parts = [];
939
+ for (const block of msg.content) parts.push(...blockToParts(block));
940
+ return {
941
+ role: msg.role,
942
+ parts
943
+ };
944
+ }
945
+ function safeJson(value) {
946
+ try {
947
+ return JSON.stringify(value ?? null);
948
+ } catch {
949
+ return "\"[unserializable]\"";
950
+ }
951
+ }
952
+ /**
953
+ * Stringify an array of `SessionMessage`s into Sentry's `gen_ai.input.messages`
954
+ * format. Each entry's role + content is mapped via `messageToSentry`.
955
+ * Returns `undefined` when the input is empty or unserializable.
956
+ */
957
+ function stringifyMessages(messages) {
958
+ if (messages.length === 0) return void 0;
959
+ try {
960
+ const sentry = messages.map((m) => messageToSentry({
961
+ role: m.role,
962
+ content: m.content
963
+ }));
964
+ return JSON.stringify(sentry);
965
+ } catch {
966
+ return;
967
+ }
968
+ }
969
+ /**
970
+ * Stringify a `StreamOptions.tools` array into Sentry's
971
+ * `gen_ai.tool.definitions` format: `[{name, description?, parameters?}, …]`.
972
+ *
973
+ * The input is `unknown[]` because `StreamOptions.tools` carries the
974
+ * provider-formatted shape (Anthropic / OpenAI / OpenAI-compat all
975
+ * differ). We probe the three names that overlap (`name`, `description`,
976
+ * `inputSchema` / `parameters`) and degrade gracefully otherwise.
977
+ */
978
+ function stringifyToolDefs(tools) {
979
+ if (!tools || tools.length === 0) return void 0;
980
+ try {
981
+ return JSON.stringify(tools.map((raw) => {
982
+ if (!raw || typeof raw !== "object") return { name: "anonymous" };
983
+ const t = raw;
984
+ const name = typeof t.name === "string" ? t.name : "anonymous";
985
+ const description = typeof t.description === "string" ? t.description : void 0;
986
+ const parameters = t.parameters ?? t.input_schema ?? t.inputSchema;
987
+ return {
988
+ name,
989
+ ...description ? { description } : {},
990
+ ...parameters !== void 0 ? { parameters } : {}
991
+ };
992
+ }));
993
+ } catch {
994
+ return;
995
+ }
996
+ }
997
+ /**
998
+ * Build the usage attribute set per the configured conventions. Sentry
999
+ * expects `input_tokens` to include cached, with `input_tokens.cached`
1000
+ * + `input_tokens.cache_write` subkeys; OTel splits cache into
1001
+ * separate top-level keys. `'both'` emits everything.
1002
+ */
1003
+ function buildUsageAttrs(usage, conventions) {
1004
+ const cacheRead = usage.cacheRead ?? 0;
1005
+ const cacheWrite = usage.cacheCreation ?? 0;
1006
+ const inputInclCache = usage.input + cacheRead + cacheWrite;
1007
+ const totalTokens = inputInclCache + usage.output;
1008
+ const wantSentry = conventions === "sentry" || conventions === "both";
1009
+ const wantOtel = conventions === "otel" || conventions === "both";
1010
+ const out = {
1011
+ [GEN_AI.responseModel]: usage.modelId,
1012
+ [GEN_AI.responseFinishReasons]: usage.finishReason ? [usage.finishReason] : void 0,
1013
+ [GEN_AI.usageOutputTokens]: usage.output,
1014
+ [GEN_AI.usageTotalTokens]: totalTokens
1015
+ };
1016
+ if (wantSentry) {
1017
+ out[GEN_AI.usageInputTokens] = inputInclCache;
1018
+ if (cacheRead > 0) out[GEN_AI.usageInputTokensCached] = cacheRead;
1019
+ if (cacheWrite > 0) out[GEN_AI.usageInputTokensCacheWrite] = cacheWrite;
1020
+ if (typeof usage.thinking === "number" && usage.thinking > 0) out[GEN_AI.usageOutputTokensReasoning] = usage.thinking;
1021
+ if (typeof usage.cost === "number") out[GEN_AI.costTotalTokens] = usage.cost;
1022
+ if (typeof usage.timeToFirstTokenMs === "number") out[GEN_AI.responseTimeToFirstTokenSeconds] = usage.timeToFirstTokenMs / 1e3;
1023
+ }
1024
+ if (wantOtel) {
1025
+ if (!wantSentry) out[GEN_AI.usageInputTokens] = usage.input;
1026
+ if (cacheRead > 0) out[GEN_AI.usageCacheReadInputTokens] = cacheRead;
1027
+ if (cacheWrite > 0) out[GEN_AI.usageCacheCreationInputTokens] = cacheWrite;
1028
+ if (typeof usage.thinking === "number" && usage.thinking > 0) out[GEN_AI.usageReasoningTokens] = usage.thinking;
1029
+ if (typeof usage.cost === "number") out[GEN_AI.usageCostUsd] = usage.cost;
1030
+ if (typeof usage.timeToFirstTokenMs === "number") out[GEN_AI.responseTimeToFirstTokenMs] = usage.timeToFirstTokenMs;
1031
+ }
1032
+ return out;
1033
+ }
1034
+ /**
16
1035
  * Build a set of tracing hook handlers that can be installed on an agent.
17
1036
  *
18
1037
  * @example Sentry
@@ -24,85 +1043,530 @@ import { defineSkill } from "./skills.js";
24
1043
  * try { await agent.run({ prompt }) }
25
1044
  * finally { uninstall() }
26
1045
  * ```
1046
+ *
1047
+ * @example OpenTelemetry with parent-context propagation
1048
+ * ```ts
1049
+ * import { trace, context, propagation } from '@opentelemetry/api'
1050
+ *
1051
+ * const tracer = trace.getTracer('zidane')
1052
+ * const tracing = createTracingHooks({
1053
+ * startSpan: (name, attrs, parentCtx) => {
1054
+ * const ctx = parentCtx ? propagation.extract(context.active(), parentCtx) : context.active()
1055
+ * return tracer.startSpan(name, { attributes: attrs }, ctx)
1056
+ * },
1057
+ * getActiveTraceContext: () => {
1058
+ * const carrier: Record<string, string> = {}
1059
+ * propagation.inject(context.active(), carrier)
1060
+ * return carrier
1061
+ * },
1062
+ * })
1063
+ * ```
27
1064
  */
28
1065
  function createTracingHooks(options) {
29
- const prefix = options.namespace ? `${options.namespace}/` : "";
1066
+ const namespace = options.namespace;
1067
+ const legacy = options.legacyAttributes ?? true;
1068
+ const conventions = options.conventions ?? "sentry";
1069
+ const wantSentry = conventions === "sentry" || conventions === "both";
1070
+ const captureContent = options.captureMessageContent ?? true;
1071
+ const onError = options.onError ?? (() => {});
30
1072
  return { install(hooks) {
1073
+ const runSpans = /* @__PURE__ */ new Map();
31
1074
  const turnSpans = /* @__PURE__ */ new Map();
32
1075
  const toolSpans = /* @__PURE__ */ new Map();
33
1076
  const mcpSpans = /* @__PURE__ */ new Map();
34
- function endSpan(map, key) {
1077
+ const spawnSpans = /* @__PURE__ */ new Map();
1078
+ const bootstrapSpans = /* @__PURE__ */ new Map();
1079
+ let activeSystem;
1080
+ const turnTrack = /* @__PURE__ */ new Map();
1081
+ let activeRunSpan;
1082
+ function safeStartSpan(name, attrs, parentContext) {
1083
+ try {
1084
+ return options.startSpan(prefixed(namespace, name), compact(attrs), parentContext);
1085
+ } catch (err) {
1086
+ try {
1087
+ onError("startSpan", err);
1088
+ } catch {}
1089
+ return;
1090
+ }
1091
+ }
1092
+ function safeSetAttrs(span, attrs) {
1093
+ if (!span?.setAttributes) return;
1094
+ try {
1095
+ span.setAttributes(compact(attrs));
1096
+ } catch (err) {
1097
+ try {
1098
+ onError("setAttributes", err);
1099
+ } catch {}
1100
+ }
1101
+ }
1102
+ function safeEnd(map, key) {
35
1103
  const span = map.get(key);
36
- if (span) {
1104
+ if (!span) return;
1105
+ try {
1106
+ span.end();
1107
+ } catch (err) {
37
1108
  try {
38
- span.end();
1109
+ onError("end", err);
39
1110
  } catch {}
40
- map.delete(key);
41
1111
  }
1112
+ map.delete(key);
42
1113
  }
43
1114
  function endAll(map) {
44
1115
  for (const [, span] of map) try {
45
1116
  span.end();
46
- } catch {}
1117
+ } catch (err) {
1118
+ try {
1119
+ onError("end", err);
1120
+ } catch {}
1121
+ }
47
1122
  map.clear();
48
1123
  }
49
- const unregisters = [];
50
- unregisters.push(hooks.hook("turn:before", (ctx) => {
51
- const span = options.startSpan(`${prefix}turn:${ctx.turn}`, { turnId: ctx.turnId });
52
- turnSpans.set(ctx.turnId, span);
53
- }));
54
- function safeSetAttrs(span, attrs) {
1124
+ function safeAddEvent(span, name, attrs) {
55
1125
  if (!span) return;
1126
+ if (typeof span.addEvent === "function") {
1127
+ try {
1128
+ span.addEvent(name, compact(attrs));
1129
+ } catch (err) {
1130
+ try {
1131
+ onError("addEvent", err);
1132
+ } catch {}
1133
+ }
1134
+ return;
1135
+ }
1136
+ safeSetAttrs(span, Object.fromEntries(Object.entries(compact(attrs)).map(([k, v]) => [`event.${name}.${k}`, v])));
1137
+ }
1138
+ function redact(kind, value, meta) {
1139
+ if (typeof options.redact !== "function") return value;
56
1140
  try {
57
- span.setAttributes?.(attrs);
58
- } catch {}
1141
+ return options.redact(kind, value, meta);
1142
+ } catch (err) {
1143
+ try {
1144
+ onError("redact", err);
1145
+ } catch {}
1146
+ return value;
1147
+ }
59
1148
  }
1149
+ const unregisters = [];
1150
+ unregisters.push(hooks.hook("agent:start", (ctx) => {
1151
+ activeSystem = ctx.providerName;
1152
+ const span = safeStartSpan(`invoke_agent ${ctx.agentName && ctx.agentName.length > 0 ? ctx.agentName : "agent"}`, {
1153
+ [GEN_AI.operationName]: "invoke_agent",
1154
+ [GEN_AI.system]: activeSystem,
1155
+ [GEN_AI.agentName]: ctx.agentName,
1156
+ [GEN_AI.agentRunId]: ctx.runId,
1157
+ [GEN_AI.agentParentRunId]: ctx.parentRunId,
1158
+ [GEN_AI.agentDepth]: ctx.depth,
1159
+ "sentry.op": "gen_ai.invoke_agent",
1160
+ ...legacy ? {
1161
+ runId: ctx.runId,
1162
+ parentRunId: ctx.parentRunId,
1163
+ agentName: ctx.agentName,
1164
+ depth: ctx.depth
1165
+ } : {}
1166
+ }, ctx.tracingContext);
1167
+ if (span) {
1168
+ runSpans.set(ctx.runId, span);
1169
+ activeRunSpan = span;
1170
+ }
1171
+ }));
1172
+ unregisters.push(hooks.hook("agent:done", (stats) => {
1173
+ const usageAttrs = buildUsageAttrs({
1174
+ input: stats.totalIn,
1175
+ output: stats.totalOut,
1176
+ cacheRead: stats.totalCacheRead,
1177
+ cacheCreation: stats.totalCacheCreation,
1178
+ cost: stats.cost,
1179
+ timeToFirstTokenMs: stats.timeTillFirstTokenMs
1180
+ }, conventions);
1181
+ for (const [, span] of runSpans) safeSetAttrs(span, {
1182
+ ...usageAttrs,
1183
+ [GEN_AI.turn]: stats.turns,
1184
+ "gen_ai.agent.elapsed_ms": stats.elapsed,
1185
+ ...legacy ? {
1186
+ totalIn: stats.totalIn,
1187
+ totalOut: stats.totalOut,
1188
+ totalCacheRead: stats.totalCacheRead,
1189
+ totalCacheCreation: stats.totalCacheCreation,
1190
+ cost: stats.cost,
1191
+ turns: stats.turns,
1192
+ elapsed: stats.elapsed
1193
+ } : {}
1194
+ });
1195
+ endAll(runSpans);
1196
+ endAll(turnSpans);
1197
+ endAll(toolSpans);
1198
+ endAll(mcpSpans);
1199
+ endAll(spawnSpans);
1200
+ endAll(bootstrapSpans);
1201
+ activeRunSpan = void 0;
1202
+ activeSystem = void 0;
1203
+ turnTrack.clear();
1204
+ }));
1205
+ unregisters.push(hooks.hook("agent:abort", () => {
1206
+ for (const [, span] of runSpans) safeSetAttrs(span, { "gen_ai.agent.status": "aborted" });
1207
+ }));
1208
+ unregisters.push(hooks.hook("turn:before", (ctx) => {
1209
+ const requestedModel = ctx.options.model;
1210
+ const baseAttrs = {
1211
+ [GEN_AI.operationName]: "chat",
1212
+ [GEN_AI.system]: activeSystem,
1213
+ [GEN_AI.requestModel]: requestedModel,
1214
+ [GEN_AI.turn]: ctx.turn,
1215
+ [GEN_AI.responseStreaming]: true,
1216
+ [GEN_AI.requestMaxTokens]: ctx.options.maxTokens,
1217
+ "sentry.op": "gen_ai.chat",
1218
+ "gen_ai.agent.turn_id": ctx.turnId,
1219
+ ...legacy ? {
1220
+ turnId: ctx.turnId,
1221
+ turn: ctx.turn
1222
+ } : {}
1223
+ };
1224
+ if (captureContent) {
1225
+ const systemText = ctx.options.system;
1226
+ if (systemText) baseAttrs[GEN_AI.systemInstructions] = redact("system", systemText, {
1227
+ turnId: ctx.turnId,
1228
+ turn: ctx.turn
1229
+ });
1230
+ const inputMsgs = stringifyMessages(ctx.options.messages);
1231
+ if (inputMsgs) baseAttrs[GEN_AI.inputMessages] = redact("prompt", inputMsgs, {
1232
+ turnId: ctx.turnId,
1233
+ turn: ctx.turn
1234
+ });
1235
+ const toolDefs = stringifyToolDefs(ctx.options.tools);
1236
+ if (toolDefs) baseAttrs[GEN_AI.toolDefinitions] = toolDefs;
1237
+ }
1238
+ const span = safeStartSpan(`chat${requestedModel ? ` ${requestedModel}` : ""}`, baseAttrs);
1239
+ if (span) {
1240
+ turnSpans.set(ctx.turnId, span);
1241
+ turnTrack.set(ctx.turnId, { startedAt: Date.now() });
1242
+ }
1243
+ }));
1244
+ unregisters.push(hooks.hook("stream:start", (ctx) => {
1245
+ safeAddEvent(turnSpans.get(ctx.turnId), "gen_ai.stream.start", { startedAt: ctx.startedAt });
1246
+ }));
60
1247
  unregisters.push(hooks.hook("turn:after", (ctx) => {
61
- safeSetAttrs(turnSpans.get(ctx.turnId), {
62
- inputTokens: ctx.usage.input,
63
- outputTokens: ctx.usage.output,
64
- ...ctx.usage.finishReason ? { finishReason: ctx.usage.finishReason } : {},
65
- ...ctx.usage.modelId ? { modelId: ctx.usage.modelId } : {}
1248
+ const span = turnSpans.get(ctx.turnId);
1249
+ const tracked = turnTrack.get(ctx.turnId);
1250
+ turnTrack.delete(ctx.turnId);
1251
+ let tokensPerSecond;
1252
+ if (tracked && ctx.usage.output > 0) {
1253
+ const elapsedMs = Date.now() - tracked.startedAt;
1254
+ if (elapsedMs > 0) tokensPerSecond = ctx.usage.output / elapsedMs * 1e3;
1255
+ }
1256
+ const usageAttrs = buildUsageAttrs(ctx.usage, conventions);
1257
+ const cumInputTokens = wantSentry ? ctx.cumulativeUsage.input + ctx.cumulativeUsage.cacheRead + ctx.cumulativeUsage.cacheCreation : ctx.cumulativeUsage.input;
1258
+ const attrs = {
1259
+ ...usageAttrs,
1260
+ [GEN_AI.responseTokensPerSecond]: tokensPerSecond,
1261
+ "gen_ai.agent.cumulative.input_tokens": cumInputTokens,
1262
+ "gen_ai.agent.cumulative.output_tokens": ctx.cumulativeUsage.output,
1263
+ "gen_ai.agent.cumulative.total_tokens": cumInputTokens + ctx.cumulativeUsage.output,
1264
+ "gen_ai.agent.cumulative.cache_read_input_tokens": ctx.cumulativeUsage.cacheRead,
1265
+ "gen_ai.agent.cumulative.cache_creation_input_tokens": ctx.cumulativeUsage.cacheCreation,
1266
+ "gen_ai.agent.cumulative.cost_usd": ctx.cumulativeUsage.cost,
1267
+ "gen_ai.agent.cumulative.turns": ctx.cumulativeUsage.turns,
1268
+ ...legacy ? {
1269
+ inputTokens: ctx.usage.input,
1270
+ outputTokens: ctx.usage.output,
1271
+ finishReason: ctx.usage.finishReason,
1272
+ modelId: ctx.usage.modelId
1273
+ } : {}
1274
+ };
1275
+ if (captureContent && ctx.message && ctx.message.role !== "system") {
1276
+ const outputMsgs = stringifyMessages([{
1277
+ role: ctx.message.role,
1278
+ content: ctx.message.content
1279
+ }]);
1280
+ if (outputMsgs) attrs[GEN_AI.outputMessages] = redact("stream-text", outputMsgs, {
1281
+ turnId: ctx.turnId,
1282
+ turn: ctx.turn
1283
+ });
1284
+ }
1285
+ safeSetAttrs(span, attrs);
1286
+ safeEnd(turnSpans, ctx.turnId);
1287
+ }));
1288
+ unregisters.push(hooks.hook("stream:error", (ctx) => {
1289
+ const span = turnSpans.get(ctx.turnId);
1290
+ const message = ctx.err instanceof Error ? ctx.err.message : String(ctx.err);
1291
+ safeSetAttrs(span, {
1292
+ "error.type": ctx.err instanceof Error ? ctx.err.name : "unknown",
1293
+ "error.message": message,
1294
+ "http.response.status_code": ctx.statusCode,
1295
+ "gen_ai.request.id": ctx.requestId
1296
+ });
1297
+ safeAddEvent(span, "gen_ai.stream.error", {
1298
+ "error.type": ctx.err instanceof Error ? ctx.err.name : "unknown",
1299
+ "error.message": message,
1300
+ "http.response.status_code": ctx.statusCode,
1301
+ "gen_ai.request.id": ctx.requestId
66
1302
  });
67
- endSpan(turnSpans, ctx.turnId);
68
1303
  }));
69
1304
  unregisters.push(hooks.hook("tool:before", (ctx) => {
70
- const span = options.startSpan(`${prefix}tool:${ctx.displayName}`, {
71
- toolName: ctx.name,
72
- displayName: ctx.displayName,
73
- turnId: ctx.turnId,
74
- callId: ctx.callId
1305
+ const span = safeStartSpan(`execute_tool ${ctx.displayName}`, {
1306
+ [GEN_AI.operationName]: "execute_tool",
1307
+ [GEN_AI.system]: activeSystem,
1308
+ [GEN_AI.toolName]: ctx.name,
1309
+ [GEN_AI.toolType]: "function",
1310
+ [GEN_AI.toolCallId]: ctx.callId,
1311
+ "sentry.op": "gen_ai.execute_tool",
1312
+ "gen_ai.tool.display_name": ctx.displayName,
1313
+ "gen_ai.agent.turn_id": ctx.turnId,
1314
+ "gen_ai.agent.depth": ctx.depth,
1315
+ "gen_ai.agent.run.id": ctx.runId,
1316
+ "gen_ai.agent.parent_run.id": ctx.parentRunId,
1317
+ ...legacy ? {
1318
+ toolName: ctx.name,
1319
+ displayName: ctx.displayName,
1320
+ turnId: ctx.turnId,
1321
+ callId: ctx.callId
1322
+ } : {}
75
1323
  });
76
- toolSpans.set(ctx.callId, span);
1324
+ if (span) {
1325
+ toolSpans.set(ctx.callId, span);
1326
+ if (captureContent) try {
1327
+ const redacted = redact("tool-input", JSON.stringify(ctx.input), {
1328
+ toolName: ctx.name,
1329
+ displayName: ctx.displayName,
1330
+ callId: ctx.callId,
1331
+ turnId: ctx.turnId
1332
+ });
1333
+ const attrs = {};
1334
+ if (wantSentry) attrs[GEN_AI.toolCallArguments] = redacted;
1335
+ if (conventions === "otel" || conventions === "both") attrs[GEN_AI.toolInputDeprecated] = redacted;
1336
+ safeSetAttrs(span, attrs);
1337
+ } catch {}
1338
+ }
77
1339
  }));
78
1340
  unregisters.push(hooks.hook("tool:after", (ctx) => {
79
- endSpan(toolSpans, ctx.callId);
1341
+ const span = toolSpans.get(ctx.callId);
1342
+ safeSetAttrs(span, {
1343
+ "gen_ai.tool.output_bytes": ctx.outputBytes,
1344
+ "gen_ai.tool.coercions": ctx.coercions?.length ?? 0
1345
+ });
1346
+ if (captureContent && typeof ctx.result === "string") {
1347
+ const redacted = redact("tool-result", ctx.result, {
1348
+ toolName: ctx.name,
1349
+ callId: ctx.callId
1350
+ });
1351
+ const attrs = {};
1352
+ if (wantSentry) attrs[GEN_AI.toolCallResult] = redacted;
1353
+ if (conventions === "otel" || conventions === "both") attrs[GEN_AI.toolOutputDeprecated] = redacted;
1354
+ safeSetAttrs(span, attrs);
1355
+ }
1356
+ safeEnd(toolSpans, ctx.callId);
80
1357
  }));
81
1358
  unregisters.push(hooks.hook("tool:error", (ctx) => {
82
- safeSetAttrs(toolSpans.get(ctx.callId), { error: ctx.error.message });
83
- endSpan(toolSpans, ctx.callId);
1359
+ safeSetAttrs(toolSpans.get(ctx.callId), {
1360
+ "error.type": ctx.error.name,
1361
+ "error.message": ctx.error.message
1362
+ });
1363
+ safeEnd(toolSpans, ctx.callId);
84
1364
  }));
85
- unregisters.push(hooks.hook("mcp:tool:before", (ctx) => {
86
- const span = options.startSpan(`${prefix}mcp:${ctx.server}:${ctx.tool}`, {
87
- server: ctx.server,
88
- tool: ctx.tool,
89
- displayName: ctx.displayName,
1365
+ unregisters.push(hooks.hook("tool:dispatched", (ctx) => {
1366
+ if (ctx.outcome === "gate-block") safeAddEvent(activeRunSpan, "gen_ai.gate.block", {
1367
+ [GEN_AI.toolName]: ctx.name,
1368
+ [GEN_AI.toolCallId]: ctx.callId,
1369
+ reason: ctx.reason
1370
+ });
1371
+ else if (ctx.outcome === "gate-substitute") safeAddEvent(activeRunSpan, "gen_ai.gate.substitute", {
1372
+ [GEN_AI.toolName]: ctx.name,
1373
+ [GEN_AI.toolCallId]: ctx.callId
1374
+ });
1375
+ else if (ctx.outcome === "unknown") safeAddEvent(activeRunSpan, "gen_ai.tool.unknown", {
1376
+ [GEN_AI.toolName]: ctx.name,
1377
+ [GEN_AI.toolCallId]: ctx.callId
1378
+ });
1379
+ else if (ctx.outcome === "invalid-input") safeAddEvent(activeRunSpan, "gen_ai.tool.invalid_input", {
1380
+ [GEN_AI.toolName]: ctx.name,
1381
+ [GEN_AI.toolCallId]: ctx.callId
1382
+ });
1383
+ }));
1384
+ unregisters.push(hooks.hook("validation:reject", (ctx) => {
1385
+ safeAddEvent(activeRunSpan, "gen_ai.validation.reject", {
1386
+ [GEN_AI.toolName]: ctx.name,
1387
+ [GEN_AI.toolCallId]: ctx.callId,
1388
+ reason: ctx.reason
1389
+ });
1390
+ }));
1391
+ unregisters.push(hooks.hook("validation:coerce", (ctx) => {
1392
+ safeAddEvent(activeRunSpan, "gen_ai.validation.coerce", {
1393
+ [GEN_AI.toolName]: ctx.name,
1394
+ [GEN_AI.toolCallId]: ctx.callId,
1395
+ coercions: ctx.coercions
1396
+ });
1397
+ }));
1398
+ unregisters.push(hooks.hook("budget:exceeded", (ctx) => {
1399
+ safeAddEvent(activeRunSpan, "gen_ai.budget.exceeded", {
90
1400
  turnId: ctx.turnId,
91
- callId: ctx.callId
1401
+ bytes: ctx.bytes,
1402
+ budget: ctx.budget
1403
+ });
1404
+ }));
1405
+ unregisters.push(hooks.hook("tool-budget:exceeded", (ctx) => {
1406
+ safeAddEvent(activeRunSpan, "gen_ai.tool_budget.exceeded", {
1407
+ [GEN_AI.toolName]: ctx.tool,
1408
+ turnId: ctx.turnId,
1409
+ count: ctx.count,
1410
+ max: ctx.max,
1411
+ mode: ctx.mode
1412
+ });
1413
+ }));
1414
+ unregisters.push(hooks.hook("pairing:repair", (ctx) => {
1415
+ safeAddEvent(activeRunSpan, "gen_ai.pairing.repair", {
1416
+ mode: ctx.mode,
1417
+ callId: ctx.callId,
1418
+ turnId: ctx.turnId
1419
+ });
1420
+ }));
1421
+ unregisters.push(hooks.hook("oauth:refresh", (ctx) => {
1422
+ safeAddEvent(activeRunSpan, "gen_ai.oauth.refresh", {
1423
+ provider: ctx.provider,
1424
+ providerId: ctx.providerId,
1425
+ source: ctx.source
92
1426
  });
93
- mcpSpans.set(ctx.callId, span);
1427
+ }));
1428
+ unregisters.push(hooks.hook("mcp:tool:before", (ctx) => {
1429
+ const span = safeStartSpan(`execute_tool ${ctx.server}.${ctx.tool}`, {
1430
+ [GEN_AI.operationName]: "execute_tool",
1431
+ [GEN_AI.system]: activeSystem,
1432
+ [GEN_AI.toolName]: ctx.displayName,
1433
+ [GEN_AI.toolType]: "extension",
1434
+ [GEN_AI.toolCallId]: ctx.callId,
1435
+ [GEN_AI.mcpServer]: ctx.server,
1436
+ "sentry.op": "gen_ai.execute_tool",
1437
+ "gen_ai.mcp.tool": ctx.tool,
1438
+ "gen_ai.agent.turn_id": ctx.turnId,
1439
+ "gen_ai.agent.run.id": ctx.runId,
1440
+ ...legacy ? {
1441
+ server: ctx.server,
1442
+ tool: ctx.tool,
1443
+ displayName: ctx.displayName,
1444
+ turnId: ctx.turnId,
1445
+ callId: ctx.callId
1446
+ } : {}
1447
+ });
1448
+ if (span) {
1449
+ mcpSpans.set(ctx.callId, span);
1450
+ if (captureContent) try {
1451
+ const redacted = redact("mcp-tool-input", JSON.stringify(ctx.input), {
1452
+ server: ctx.server,
1453
+ tool: ctx.tool,
1454
+ callId: ctx.callId
1455
+ });
1456
+ const attrs = {};
1457
+ if (wantSentry) attrs[GEN_AI.toolCallArguments] = redacted;
1458
+ if (conventions === "otel" || conventions === "both") attrs[GEN_AI.toolInputDeprecated] = redacted;
1459
+ safeSetAttrs(span, attrs);
1460
+ } catch {}
1461
+ }
94
1462
  }));
95
1463
  unregisters.push(hooks.hook("mcp:tool:after", (ctx) => {
96
- endSpan(mcpSpans, ctx.callId);
1464
+ const span = mcpSpans.get(ctx.callId);
1465
+ safeSetAttrs(span, { "gen_ai.tool.output_bytes": ctx.outputBytes });
1466
+ if (captureContent && typeof ctx.result === "string") {
1467
+ const redacted = redact("mcp-tool-result", ctx.result, {
1468
+ server: ctx.server,
1469
+ tool: ctx.tool,
1470
+ callId: ctx.callId
1471
+ });
1472
+ const attrs = {};
1473
+ if (wantSentry) attrs[GEN_AI.toolCallResult] = redacted;
1474
+ if (conventions === "otel" || conventions === "both") attrs[GEN_AI.toolOutputDeprecated] = redacted;
1475
+ safeSetAttrs(span, attrs);
1476
+ }
1477
+ safeEnd(mcpSpans, ctx.callId);
97
1478
  }));
98
1479
  unregisters.push(hooks.hook("mcp:tool:error", (ctx) => {
99
- safeSetAttrs(mcpSpans.get(ctx.callId), { error: ctx.error.message });
100
- endSpan(mcpSpans, ctx.callId);
1480
+ safeSetAttrs(mcpSpans.get(ctx.callId), {
1481
+ "error.type": ctx.error.name,
1482
+ "error.message": ctx.error.message
1483
+ });
1484
+ safeEnd(mcpSpans, ctx.callId);
101
1485
  }));
102
- unregisters.push(hooks.hook("agent:done", () => {
103
- endAll(turnSpans);
104
- endAll(toolSpans);
105
- endAll(mcpSpans);
1486
+ unregisters.push(hooks.hook("mcp:bootstrap:start", (ctx) => {
1487
+ const span = safeStartSpan(`mcp.bootstrap ${ctx.name}`, {
1488
+ [GEN_AI.operationName]: "mcp.bootstrap",
1489
+ [GEN_AI.system]: activeSystem,
1490
+ [GEN_AI.mcpServer]: ctx.name,
1491
+ "gen_ai.mcp.transport": ctx.transport
1492
+ });
1493
+ if (span) bootstrapSpans.set(ctx.name, span);
1494
+ }));
1495
+ unregisters.push(hooks.hook("mcp:bootstrap:end", (ctx) => {
1496
+ const span = bootstrapSpans.get(ctx.name);
1497
+ const ok = ctx.ok;
1498
+ safeSetAttrs(span, {
1499
+ "gen_ai.mcp.bootstrap.duration_ms": ctx.durationMs,
1500
+ "gen_ai.mcp.bootstrap.ok": ok,
1501
+ ...ok ? {
1502
+ [GEN_AI.mcpToolCount]: ctx.toolCount,
1503
+ "gen_ai.mcp.lazy": ctx.lazy,
1504
+ "gen_ai.mcp.cached": ctx.cached
1505
+ } : {
1506
+ "error.type": ctx.error.name,
1507
+ "error.message": ctx.error.message
1508
+ }
1509
+ });
1510
+ safeEnd(bootstrapSpans, ctx.name);
1511
+ }));
1512
+ unregisters.push(hooks.hook("mcp:auth:required", (ctx) => {
1513
+ safeAddEvent(bootstrapSpans.get(ctx.name) ?? activeRunSpan, "gen_ai.mcp.auth.required", {
1514
+ server: ctx.name,
1515
+ transport: ctx.transport,
1516
+ reason: ctx.reason
1517
+ });
1518
+ }));
1519
+ unregisters.push(hooks.hook("mcp:error", (ctx) => {
1520
+ safeAddEvent(activeRunSpan, "gen_ai.mcp.error", {
1521
+ "server": ctx.name,
1522
+ "error.type": ctx.error.name,
1523
+ "error.message": ctx.error.message
1524
+ });
1525
+ }));
1526
+ unregisters.push(hooks.hook("spawn:before", (ctx) => {
1527
+ const span = safeStartSpan(`handoff to ${ctx.task.length > 60 ? `${ctx.task.slice(0, 57)}…` : ctx.task}`, {
1528
+ [GEN_AI.operationName]: "handoff",
1529
+ [GEN_AI.system]: activeSystem,
1530
+ "sentry.op": "gen_ai.handoff",
1531
+ "gen_ai.agent.spawn.id": ctx.id,
1532
+ "gen_ai.agent.spawn.task": ctx.task,
1533
+ "gen_ai.agent.depth": ctx.depth
1534
+ });
1535
+ if (span) spawnSpans.set(ctx.id, span);
1536
+ if (typeof options.getActiveTraceContext === "function" && ctx.tracingContext) {
1537
+ let carrier;
1538
+ try {
1539
+ carrier = options.getActiveTraceContext();
1540
+ } catch (err) {
1541
+ try {
1542
+ onError("getActiveTraceContext", err);
1543
+ } catch {}
1544
+ }
1545
+ if (carrier) for (const [k, v] of Object.entries(carrier)) ctx.tracingContext[k] = v;
1546
+ }
1547
+ }));
1548
+ unregisters.push(hooks.hook("spawn:complete", (ctx) => {
1549
+ const span = spawnSpans.get(ctx.id);
1550
+ const childUsage = buildUsageAttrs({
1551
+ input: ctx.stats.totalIn,
1552
+ output: ctx.stats.totalOut,
1553
+ cacheRead: ctx.stats.totalCacheRead,
1554
+ cacheCreation: ctx.stats.totalCacheCreation,
1555
+ cost: ctx.stats.cost
1556
+ }, conventions);
1557
+ safeSetAttrs(span, {
1558
+ "gen_ai.agent.spawn.status": ctx.status ?? "completed",
1559
+ "gen_ai.agent.spawn.depth": ctx.depth,
1560
+ ...childUsage
1561
+ });
1562
+ safeEnd(spawnSpans, ctx.id);
1563
+ }));
1564
+ unregisters.push(hooks.hook("spawn:error", (ctx) => {
1565
+ safeSetAttrs(spawnSpans.get(ctx.id), {
1566
+ "error.type": ctx.error.name,
1567
+ "error.message": ctx.error.message
1568
+ });
1569
+ safeEnd(spawnSpans, ctx.id);
106
1570
  }));
107
1571
  let disposed = false;
108
1572
  return function uninstall() {
@@ -111,12 +1575,23 @@ function createTracingHooks(options) {
111
1575
  for (const un of unregisters) try {
112
1576
  un();
113
1577
  } catch {}
1578
+ endAll(runSpans);
114
1579
  endAll(turnSpans);
115
1580
  endAll(toolSpans);
116
1581
  endAll(mcpSpans);
1582
+ endAll(spawnSpans);
1583
+ endAll(bootstrapSpans);
1584
+ activeRunSpan = void 0;
117
1585
  };
118
1586
  } };
119
1587
  }
1588
+ /**
1589
+ * OpenTelemetry Gen AI semantic-convention attribute keys used by the
1590
+ * built-in tracer. Exported so consumers integrating with raw OTel SDKs
1591
+ * (instead of going through `createTracingHooks`) can stay aligned with
1592
+ * the names the harness itself emits.
1593
+ */
1594
+ const GEN_AI_ATTRIBUTES = GEN_AI;
120
1595
  //#endregion
121
1596
  //#region src/zod.ts
122
1597
  /**
@@ -141,6 +1616,6 @@ function zodToJsonSchema(jsonSchema) {
141
1616
  return rest;
142
1617
  }
143
1618
  //#endregion
144
- export { ANCHOR_PREVIEW_MAX_CHARS, AgentAbortedError, AgentContextExceededError, AgentProviderError, AgentToolNotAllowedError, AgentToolPairingError, BASE_INSTRUCTIONS, BYTES_PER_TOKEN, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, CompactInvalidInputError, CompactPromptTooLongError, IMPLICITLY_ALLOWED_SKILL_TOOLS, INTERRUPT_MESSAGE_FOR_TOOL_USE, McpOAuthProvider, NO_TOOLS_PREAMBLE, ORPHANED_TOOL_RESULT_MARKER, OpenAICompatHttpError, PERSISTED_STUB_PREFIX, PERSISTENCE_PREVIEW_BYTES, SHELL_CASCADE_CANCEL_MESSAGE, SYNTHETIC_TOOL_RESULT_PLACEHOLDER, TOOL_USE_INTERRUPTED_MARKER, TOOL_USE_SKIPPED_MESSAGE, TRAILER, anchorPreviewFor, anthropic, autoDetectAndConvert, basic_default as basic, basicTools, buildCatalog, buildCompactPrompt, buildFromCompactPrompt, buildFullCompactPrompt, buildPersistedStub, buildPostCompactAttachments, buildTailCompactPrompt, buildUpToCompactPrompt, cerebras, classifyOpenAICompatError, cleanupPersistedSession, compactConversation, connectMcpServers, createAgent, createFileMapStore, createInteractionTool, createMemoryMcpCredentialStore, createMemoryStore, createProcessContext, createRemoteStore, createSandboxContext, createSession, createSkillActivationState, createSkillsReadTool, createSkillsRunScriptTool, createSkillsUseTool, createSpawnTool, createTracingHooks, definePreset, defineSkill, detectTurnInterruption, discoverSkills, edit, ensureToolResultPairing, errorMessage, estimateTokens, filterUnresolvedToolUses, flattenTurns, fromAnthropic, fromOpenAI, getReadState, glob, grep, hasAuthorizationHeader, installAllowedToolsGate, interpolateShellCommands, isToolAllowedByUnion, loadSession, loginMcpServer, mapOAIFinishReason, matchesAllowedTool, matchesContextExceeded, maybePersistToolResult, multiEdit, normalizeMcpBlocks, normalizeMcpServers, openai, openaiCompat, openrouter, parseAllowedToolPattern, parseSkillFile, readStateKey, resolvePersistDir, resolveReadStateMap, resolveSkills, resultToString, sanitizeToolSchema, sanitizeToolSpecs, selectFilesFromReadState, selectFilesFromSession, selectRecentFiles, sliceForCompaction, startOAuthCallback, statsByModel, stripImagesFromTurns, summaryToTurn, toAnthropic, toOpenAI, toTypedError, toolOutputByteLength, toolResultToText, truncateHeadForPtlRetry, utf8ByteLength, validateResourcePath, validateSkillForWrite, validateSkillName, validateToolArgs, writeSkillToDisk, writeSkillsToDisk, zodToJsonSchema };
1619
+ export { ANCHOR_PREVIEW_MAX_CHARS, AgentAbortedError, AgentContextExceededError, AgentProviderError, AgentToolNotAllowedError, AgentToolPairingError, BASE_INSTRUCTIONS, BYTES_PER_TOKEN, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, CompactInvalidInputError, CompactPromptTooLongError, GEN_AI_ATTRIBUTES, IMPLICITLY_ALLOWED_SKILL_TOOLS, INTERRUPT_MESSAGE_FOR_TOOL_USE, McpOAuthProvider, NO_TOOLS_PREAMBLE, ORPHANED_TOOL_RESULT_MARKER, OpenAICompatHttpError, PERSISTED_STUB_PREFIX, PERSISTENCE_PREVIEW_BYTES, SHELL_CASCADE_CANCEL_MESSAGE, SYNTHETIC_TOOL_RESULT_PLACEHOLDER, TOOL_USE_CANCELLED_MESSAGE, TOOL_USE_INTERRUPTED_MARKER, TOOL_USE_SKIPPED_MESSAGE, TRAILER, anchorPreviewFor, anthropic, autoDetectAndConvert, basic_default as basic, basicTools, buildCatalog, buildCompactPrompt, buildFromCompactPrompt, buildFullCompactPrompt, buildPersistedStub, buildPostCompactAttachments, buildTailCompactPrompt, buildUpToCompactPrompt, cerebras, classifyOpenAICompatError, cleanupPersistedSession, compactConversation, connectMcpServers, consoleSink, createAgent, createFileMapStore, createInteractionTool, createLogger, createLoggingHooks, createMemoryMcpCredentialStore, createMemoryStore, createMetricsHooks, createProcessContext, createRemoteStore, createRunSummaryCollector, createSandboxContext, createSession, createSkillActivationState, createSkillsReadTool, createSkillsRunScriptTool, createSkillsUseTool, createSpawnTool, createTracingHooks, definePreset, defineSkill, detectTurnInterruption, discoverSkills, edit, ensureToolResultPairing, errorMessage, estimateTokens, filterUnresolvedToolUses, flattenTurns, fromAnthropic, fromOpenAI, getReadState, glob, grep, hasAuthorizationHeader, installAllowedToolsGate, interpolateShellCommands, isToolAllowedByUnion, jsonSink, loadSession, loginMcpServer, mapOAIFinishReason, matchesAllowedTool, matchesContextExceeded, maybePersistToolResult, multiEdit, normalizeMcpBlocks, normalizeMcpServers, openai, openaiCompat, openrouter, parseAllowedToolPattern, parseSkillFile, readStateKey, resolvePersistDir, resolveReadStateMap, resolveSkills, resultToString, sanitizeToolSchema, sanitizeToolSpecs, selectFilesFromReadState, selectFilesFromSession, selectRecentFiles, sliceForCompaction, startOAuthCallback, statsByModel, stripImagesFromTurns, summaryToTurn, toAnthropic, toOpenAI, toTypedError, toolOutputByteLength, toolResultToText, truncateHeadForPtlRetry, utf8ByteLength, validateResourcePath, validateSkillForWrite, validateSkillName, validateToolArgs, writeSkillToDisk, writeSkillsToDisk, zodToJsonSchema };
145
1620
 
146
1621
  //# sourceMappingURL=index.js.map