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.
- package/README.md +15 -0
- package/dist/{agent-DHQAsdj6.d.ts → agent-Yu8uhpy-.d.ts} +213 -3
- package/dist/agent-Yu8uhpy-.d.ts.map +1 -0
- package/dist/chat.d.ts +49 -6
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{errors-Byb0F8B9.js → errors-CDwtPIMX.js} +4 -2
- package/dist/{errors-Byb0F8B9.js.map → errors-CDwtPIMX.js.map} +1 -1
- package/dist/{index-CHSaLab5.d.ts → index-DklfxeYy.d.ts} +8 -2
- package/dist/index-DklfxeYy.d.ts.map +1 -0
- package/dist/{index-CrqFoaQA.d.ts → index-j9tY28ah.d.ts} +474 -8
- package/dist/index-j9tY28ah.d.ts.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1528 -53
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-ERgZUxgg.js → interpolate-CmtjEyRJ.js} +155 -18
- package/dist/interpolate-CmtjEyRJ.js.map +1 -0
- package/dist/{login-8c5C0FYq.js → login-DxyAERe1.js} +3 -3
- package/dist/{login-8c5C0FYq.js.map → login-DxyAERe1.js.map} +1 -1
- package/dist/{mcp-DhmmJfxK.js → mcp-CNUbvbsy.js} +2 -2
- package/dist/{mcp-DhmmJfxK.js.map → mcp-CNUbvbsy.js.map} +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-D0xT979U.js → messages-fTR19Ga6.js} +2 -2
- package/dist/{messages-D0xT979U.js.map → messages-fTR19Ga6.js.map} +1 -1
- package/dist/{presets-Ck4VusTo.js → presets-D9IbaI40.js} +2 -2
- package/dist/{presets-Ck4VusTo.js.map → presets-D9IbaI40.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-x3LZByR5.js → providers-CEzRFYtS.js} +3 -3
- package/dist/{providers-x3LZByR5.js.map → providers-CEzRFYtS.js.map} +1 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.js +1 -1
- package/dist/{session-BHZwxmfr.js → session-kwsNnOmt.js} +2 -2
- package/dist/{session-BHZwxmfr.js.map → session-kwsNnOmt.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/skills.d.ts +2 -2
- package/dist/skills.js +1 -1
- package/dist/{tools-PQH1Ge4M.js → tools-BK2vG9UX.js} +246 -44
- package/dist/tools-BK2vG9UX.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-ByB2MSCB.d.ts → transcript-anchors-DnaBcJej.d.ts} +52 -8
- package/dist/transcript-anchors-DnaBcJej.d.ts.map +1 -0
- package/dist/tui.d.ts +4 -2
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +651 -42
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-Bqs4YbbH.js → turn-operations-OzKEOXul.js} +240 -52
- package/dist/turn-operations-OzKEOXul.js.map +1 -0
- package/dist/types-IcokUOyC.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js +1 -1
- package/docs/ARCHITECTURE.md +16 -3
- package/docs/CHAT.md +1 -1
- package/docs/SKILL.md +24 -14
- package/docs/TUI.md +24 -0
- package/package.json +3 -3
- package/dist/agent-DHQAsdj6.d.ts.map +0 -1
- package/dist/index-CHSaLab5.d.ts.map +0 -1
- package/dist/index-CrqFoaQA.d.ts.map +0 -1
- package/dist/interpolate-ERgZUxgg.js.map +0 -1
- package/dist/tools-PQH1Ge4M.js.map +0 -1
- package/dist/transcript-anchors-ByB2MSCB.d.ts.map +0 -1
- package/dist/turn-operations-Bqs4YbbH.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,1037 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
11
|
-
import { i as anthropic, n as openai, r as cerebras, t as openrouter } from "./providers-
|
|
12
|
-
import { a as createFileMapStore, i as createMemoryStore, n as loadSession, r as createRemoteStore, t as createSession } from "./session-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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), {
|
|
83
|
-
|
|
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("
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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), {
|
|
100
|
-
|
|
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("
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|