@yagni-app/code 1.0.6 → 1.0.7

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 (85) hide show
  1. package/README.md +84 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/condensedTools.d.ts +12 -1
  10. package/dist/extension/condensedTools.js +17 -9
  11. package/dist/extension/index.d.ts +13 -0
  12. package/dist/extension/index.js +230 -43
  13. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  14. package/dist/extension/mcp/cliConfig.js +1 -1
  15. package/dist/extension/mcp/config.d.ts +24 -2
  16. package/dist/extension/mcp/config.js +75 -3
  17. package/dist/extension/mcp/manager.d.ts +3 -1
  18. package/dist/extension/mcp/manager.js +2 -2
  19. package/dist/extension/mcp/panel.d.ts +0 -1
  20. package/dist/extension/mcp/panel.js +13 -3
  21. package/dist/extension/mcp/startup.js +8 -6
  22. package/dist/extension/permission/gate.d.ts +97 -2
  23. package/dist/extension/permission/gate.js +375 -26
  24. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  25. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  26. package/dist/extension/permissionRules/engine.d.ts +50 -0
  27. package/dist/extension/permissionRules/engine.js +238 -0
  28. package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
  29. package/dist/extension/permissionRules/loadConfig.js +90 -0
  30. package/dist/extension/permissionRules/parser.d.ts +38 -0
  31. package/dist/extension/permissionRules/parser.js +136 -0
  32. package/dist/extension/permissionRules/pathRules.d.ts +58 -0
  33. package/dist/extension/permissionRules/pathRules.js +120 -0
  34. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  35. package/dist/extension/permissionRules/shellRules.js +221 -0
  36. package/dist/extension/pipeline/invocation.d.ts +3 -6
  37. package/dist/extension/pipeline/invocation.js +3 -6
  38. package/dist/extension/pipeline/runner.d.ts +0 -1
  39. package/dist/extension/pipeline/runner.js +6 -14
  40. package/dist/extension/plugins/inventory.d.ts +88 -0
  41. package/dist/extension/plugins/inventory.js +144 -0
  42. package/dist/extension/plugins/panel.d.ts +45 -0
  43. package/dist/extension/plugins/panel.js +293 -0
  44. package/dist/extension/sandbox/bash.d.ts +99 -0
  45. package/dist/extension/sandbox/bash.js +190 -0
  46. package/dist/extension/sandbox/config.d.ts +114 -0
  47. package/dist/extension/sandbox/config.js +366 -0
  48. package/dist/extension/sandbox/manager.d.ts +98 -0
  49. package/dist/extension/sandbox/manager.js +216 -0
  50. package/dist/extension/sandbox/panel.d.ts +111 -0
  51. package/dist/extension/sandbox/panel.js +342 -0
  52. package/dist/extension/sandbox/session.d.ts +85 -0
  53. package/dist/extension/sandbox/session.js +775 -0
  54. package/dist/extension/telemetry/attrs.d.ts +96 -0
  55. package/dist/extension/telemetry/attrs.js +149 -0
  56. package/dist/extension/telemetry/config.d.ts +99 -0
  57. package/dist/extension/telemetry/config.js +193 -0
  58. package/dist/extension/telemetry/index.d.ts +7 -0
  59. package/dist/extension/telemetry/index.js +7 -0
  60. package/dist/extension/telemetry/probe.d.ts +29 -0
  61. package/dist/extension/telemetry/probe.js +122 -0
  62. package/dist/extension/telemetry/register.d.ts +40 -0
  63. package/dist/extension/telemetry/register.js +192 -0
  64. package/dist/extension/telemetry/sdk.d.ts +63 -0
  65. package/dist/extension/telemetry/sdk.js +207 -0
  66. package/dist/extension/telemetry/tracker.d.ts +131 -0
  67. package/dist/extension/telemetry/tracker.js +551 -0
  68. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  69. package/dist/extension/vendor/ignore.d.ts +86 -0
  70. package/dist/extension/vendor/ignore.js +788 -0
  71. package/dist/goHeadless.d.ts +1 -1
  72. package/dist/goHeadless.js +2 -2
  73. package/dist/launch.d.ts +4 -3
  74. package/dist/launch.js +7 -4
  75. package/dist/mcpCommand.d.ts +10 -1
  76. package/dist/mcpCommand.js +42 -10
  77. package/dist/otel.d.ts +67 -90
  78. package/dist/otel.js +152 -195
  79. package/dist/paths.d.ts +13 -0
  80. package/dist/paths.js +18 -0
  81. package/dist/pluginCommand.d.ts +43 -0
  82. package/dist/pluginCommand.js +499 -0
  83. package/dist/pluginStore.d.ts +170 -0
  84. package/dist/pluginStore.js +554 -0
  85. package/package.json +19 -3
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Session telemetry tracker: turns pi's lifecycle into spans, metrics, and
3
+ * log events. Pure with respect to pi — `register.ts` feeds it events, tests
4
+ * feed it directly — and it only ever talks to the OTel API (tracer, meter,
5
+ * logger), never to exporters.
6
+ *
7
+ * Shape (Claude Code parity, prefix `yagni_code`):
8
+ *
9
+ * spans yagni_code.interaction ─┬─ yagni_code.turn ─┬─ yagni_code.llm_request
10
+ * │ └─ yagni_code.tool
11
+ * metrics session.count, token.usage, cost.usage, lines_of_code.count,
12
+ * commit.count, pull_request.count, code_edit_tool.decision,
13
+ * active_time.total
14
+ * events user_prompt, assistant_response, api_request, api_error,
15
+ * tool_result, tool_decision, permission_mode_changed
16
+ *
17
+ * Metadata only. Prompt text, model output, tool arguments, and tool results
18
+ * never become attributes — only their lengths.
19
+ */
20
+ import { type Attributes, type Meter, type Tracer } from "@opentelemetry/api";
21
+ import { type Logger } from "@opentelemetry/api-logs";
22
+ import { type Decision, type DecisionSource } from "./attrs.js";
23
+ import type { TelemetryConfig } from "./config.js";
24
+ /** pi's assistant-message usage block (loose: providers differ in what they fill). */
25
+ export interface UsageLike {
26
+ input?: number;
27
+ output?: number;
28
+ cacheRead?: number;
29
+ cacheWrite?: number;
30
+ cost?: number | {
31
+ total?: number;
32
+ };
33
+ }
34
+ export interface AssistantMessageLike {
35
+ model?: string;
36
+ responseModel?: string;
37
+ responseId?: string;
38
+ usage?: UsageLike;
39
+ stopReason?: string;
40
+ errorMessage?: string;
41
+ content?: unknown;
42
+ }
43
+ export interface ToolDecisionInput {
44
+ toolName: string;
45
+ toolCallId?: string;
46
+ decision: Decision;
47
+ source: DecisionSource;
48
+ /** For edit/write tools: the target path, so `language` can be derived. */
49
+ filePath?: string;
50
+ }
51
+ export interface TrackerDeps {
52
+ tracer: Tracer;
53
+ meter: Meter;
54
+ logger: Logger;
55
+ config: TelemetryConfig;
56
+ now?: () => number;
57
+ }
58
+ /** Idle cutoff for user active time: gaps longer than this are not "active". */
59
+ export declare const USER_ACTIVE_IDLE_CUTOFF_MS: number;
60
+ export declare class SessionTelemetry {
61
+ private readonly tracer;
62
+ private readonly meter;
63
+ private readonly logger;
64
+ private readonly config;
65
+ private readonly now;
66
+ private sessionId;
67
+ private sessionCounted;
68
+ private eventSequence;
69
+ private interactionSequence;
70
+ private interaction;
71
+ private turn;
72
+ private llm;
73
+ private readonly tools;
74
+ /** Decision recorded per tool call before it executes (feeds tool_result). */
75
+ private readonly decisions;
76
+ /** When the agent last went idle — the start of the user's active window. */
77
+ private idleSince;
78
+ private readonly counters;
79
+ constructor(deps: TrackerDeps);
80
+ /** Attributes every metric and event carries (Claude Code's standard set). */
81
+ standardAttributes(): Attributes;
82
+ /** Attributes every span carries: session identity in both vocabularies. */
83
+ private spanAttributes;
84
+ private counter;
85
+ private add;
86
+ emitEvent(name: string, attrs: Attributes): void;
87
+ sessionStart(sessionId: string | undefined): void;
88
+ getSessionId(): string | undefined;
89
+ interactionStart(prompt: string | undefined): void;
90
+ interactionEnd(): void;
91
+ turnStart(turnIndex: number | undefined): void;
92
+ turnEnd(): void;
93
+ private parentCtx;
94
+ llmStart(model: string | undefined): void;
95
+ llmResponseHeaders(status: number | undefined, headers: Record<string, string> | undefined): void;
96
+ /**
97
+ * The assistant message ended: attach usage, emit api_request / api_error
98
+ * plus the token and cost counters, and close the LLM span. Opens a span
99
+ * first when pi delivered the message without a before_provider_request
100
+ * (e.g. a replayed or synthetic message) so nothing is lost.
101
+ */
102
+ assistantMessageEnd(message: AssistantMessageLike): void;
103
+ llmEnd(error: Error | undefined): void;
104
+ toolDecision(input: ToolDecisionInput): void;
105
+ toolStart(toolCallId: string, toolName: string, args: unknown): void;
106
+ toolEnd(toolCallId: string, outcome: {
107
+ isError: boolean;
108
+ resultBytes: number;
109
+ errorType?: string;
110
+ }): void;
111
+ /** Lines added/removed by an edit or write, from pi's tool_result details. */
112
+ linesOfCode(added: number, removed: number): void;
113
+ /** A successful bash command: count commits and PR creations. */
114
+ bashSucceeded(command: string | undefined): void;
115
+ permissionModeChanged(fromMode: string, toMode: string): void;
116
+ /** Close everything still open so no span is lost at exit. */
117
+ close(): void;
118
+ }
119
+ /** Cap on the exported error excerpt (characters, one line). */
120
+ export declare const ERROR_EXCERPT_CHARS = 120;
121
+ /** First line of a provider error, capped, never the full body. */
122
+ export declare function errorExcerpt(message: string | undefined): string;
123
+ export declare function byteLength(v: unknown): number;
124
+ /** Count +/- lines in a unified patch, ignoring file headers. */
125
+ export declare function countPatchLines(patch: string | undefined): {
126
+ added: number;
127
+ removed: number;
128
+ };
129
+ /** Line count of freshly written content (a write tool call). */
130
+ export declare function countContentLines(content: unknown): number;
131
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Session telemetry tracker: turns pi's lifecycle into spans, metrics, and
3
+ * log events. Pure with respect to pi — `register.ts` feeds it events, tests
4
+ * feed it directly — and it only ever talks to the OTel API (tracer, meter,
5
+ * logger), never to exporters.
6
+ *
7
+ * Shape (Claude Code parity, prefix `yagni_code`):
8
+ *
9
+ * spans yagni_code.interaction ─┬─ yagni_code.turn ─┬─ yagni_code.llm_request
10
+ * │ └─ yagni_code.tool
11
+ * metrics session.count, token.usage, cost.usage, lines_of_code.count,
12
+ * commit.count, pull_request.count, code_edit_tool.decision,
13
+ * active_time.total
14
+ * events user_prompt, assistant_response, api_request, api_error,
15
+ * tool_result, tool_decision, permission_mode_changed
16
+ *
17
+ * Metadata only. Prompt text, model output, tool arguments, and tool results
18
+ * never become attributes — only their lengths.
19
+ */
20
+ import { randomUUID } from "node:crypto";
21
+ import { context as otelContext, SpanStatusCode, trace, } from "@opentelemetry/api";
22
+ import { SeverityNumber } from "@opentelemetry/api-logs";
23
+ import { ATTR_APP_ENTRYPOINT, ATTR_APP_VERSION, ATTR_ERROR_TYPE, ATTR_GEN_AI_AGENT_NAME, ATTR_GEN_AI_CACHE_CREATION_TOKENS_LEGACY, ATTR_GEN_AI_CACHE_READ_TOKENS, ATTR_GEN_AI_CACHE_READ_TOKENS_LEGACY, ATTR_GEN_AI_CACHE_WRITE_TOKENS, ATTR_GEN_AI_CONVERSATION_ID, ATTR_GEN_AI_COST_ESTIMATED_TOTAL, ATTR_GEN_AI_FINISH_REASONS, ATTR_GEN_AI_INPUT_TOKENS, ATTR_GEN_AI_OPERATION_NAME, ATTR_GEN_AI_OUTPUT_TOKENS, ATTR_GEN_AI_TOTAL_TOKENS, ATTR_GEN_AI_PROVIDER_NAME, ATTR_GEN_AI_REQUEST_MODEL, ATTR_GEN_AI_RESPONSE_ID, ATTR_GEN_AI_RESPONSE_MODEL, ATTR_GEN_AI_SYSTEM, ATTR_GEN_AI_TOOL_CALL_ID, ATTR_GEN_AI_TOOL_NAME, ATTR_GEN_AI_TOOL_TYPE, ATTR_HTTP_STATUS_CODE, ATTR_ORGANIZATION_ID, ATTR_SESSION_ID, ATTR_TERMINAL_TYPE, ATTR_USER_EMAIL, EVENT_API_ERROR, EVENT_API_REQUEST, EVENT_ASSISTANT_RESPONSE, EVENT_PERMISSION_MODE_CHANGED, EVENT_TOOL_DECISION, EVENT_TOOL_RESULT, EVENT_USER_PROMPT, GEN_AI_PROVIDER, languageFromPath, METRIC_ACTIVE_TIME, METRIC_CODE_EDIT_DECISION, METRIC_COMMIT_COUNT, METRIC_COST_USAGE, METRIC_LINES_OF_CODE, METRIC_PULL_REQUEST_COUNT, METRIC_SESSION_COUNT, METRIC_TOKEN_USAGE, PREFIX, SPAN_INTERACTION, SPAN_LLM_REQUEST, SPAN_TOOL, SPAN_TURN, } from "./attrs.js";
24
+ /** Idle cutoff for user active time: gaps longer than this are not "active". */
25
+ export const USER_ACTIVE_IDLE_CUTOFF_MS = 5 * 60 * 1000;
26
+ const EDIT_TOOLS = new Set(["edit", "write", "multi_edit", "notebook_edit"]);
27
+ const GIT_COMMIT_RE = /(?:^|[;&|]\s*)git\s+(?:-C\s+\S+\s+)?(?:[a-z-]+=\S+\s+)*commit\b/;
28
+ const GH_PR_CREATE_RE = /(?:^|[;&|]\s*)gh\s+pr\s+create\b/;
29
+ export class SessionTelemetry {
30
+ tracer;
31
+ meter;
32
+ logger;
33
+ config;
34
+ now;
35
+ sessionId;
36
+ sessionCounted = false;
37
+ eventSequence = 0;
38
+ interactionSequence = 0;
39
+ interaction = null;
40
+ turn = null;
41
+ llm = null;
42
+ tools = new Map();
43
+ /** Decision recorded per tool call before it executes (feeds tool_result). */
44
+ decisions = new Map();
45
+ /** When the agent last went idle — the start of the user's active window. */
46
+ idleSince;
47
+ counters = {};
48
+ constructor(deps) {
49
+ this.tracer = deps.tracer;
50
+ this.meter = deps.meter;
51
+ this.logger = deps.logger;
52
+ this.config = deps.config;
53
+ this.now = deps.now ?? (() => Date.now());
54
+ }
55
+ // ── Standard attributes ─────────────────────────────────────────────────
56
+ /** Attributes every metric and event carries (Claude Code's standard set). */
57
+ standardAttributes() {
58
+ const id = this.config.identity;
59
+ const attrs = {};
60
+ if (this.config.includeSessionId && this.sessionId)
61
+ attrs[ATTR_SESSION_ID] = this.sessionId;
62
+ if (this.config.includeVersion && id.appVersion)
63
+ attrs[ATTR_APP_VERSION] = id.appVersion;
64
+ if (this.config.includeEntrypoint)
65
+ attrs[ATTR_APP_ENTRYPOINT] = id.entrypoint;
66
+ if (id.organizationId)
67
+ attrs[ATTR_ORGANIZATION_ID] = id.organizationId;
68
+ if (this.config.includeAccountId && id.userEmail)
69
+ attrs[ATTR_USER_EMAIL] = id.userEmail;
70
+ if (id.terminalType)
71
+ attrs[ATTR_TERMINAL_TYPE] = id.terminalType;
72
+ if (this.config.includeResourceAttributes)
73
+ Object.assign(attrs, this.config.resourceAttributes);
74
+ return attrs;
75
+ }
76
+ /** Attributes every span carries: session identity in both vocabularies. */
77
+ spanAttributes() {
78
+ const attrs = {
79
+ [ATTR_GEN_AI_PROVIDER_NAME]: GEN_AI_PROVIDER,
80
+ [ATTR_GEN_AI_SYSTEM]: GEN_AI_PROVIDER,
81
+ [ATTR_GEN_AI_AGENT_NAME]: this.config.serviceName,
82
+ };
83
+ if (this.sessionId) {
84
+ attrs[ATTR_SESSION_ID] = this.sessionId;
85
+ attrs[ATTR_GEN_AI_CONVERSATION_ID] = this.sessionId;
86
+ }
87
+ const id = this.config.identity;
88
+ if (id.organizationId)
89
+ attrs[ATTR_ORGANIZATION_ID] = id.organizationId;
90
+ // Same gate as the metrics/events attribute set: an admin who turned
91
+ // account identity off gets it off on spans too.
92
+ if (this.config.includeAccountId && id.userEmail)
93
+ attrs[ATTR_USER_EMAIL] = id.userEmail;
94
+ if (id.appVersion)
95
+ attrs[ATTR_APP_VERSION] = id.appVersion;
96
+ return attrs;
97
+ }
98
+ // ── Instruments ─────────────────────────────────────────────────────────
99
+ counter(name, unit, description) {
100
+ let c = this.counters[name];
101
+ if (!c) {
102
+ c = this.meter.createCounter(name, { unit, description });
103
+ this.counters[name] = c;
104
+ }
105
+ return c;
106
+ }
107
+ add(name, unit, description, value, attrs = {}) {
108
+ if (!Number.isFinite(value) || value === 0)
109
+ return;
110
+ try {
111
+ this.counter(name, unit, description).add(value, { ...this.standardAttributes(), ...attrs });
112
+ }
113
+ catch {
114
+ // Metrics are best-effort.
115
+ }
116
+ }
117
+ // ── Events ──────────────────────────────────────────────────────────────
118
+ emitEvent(name, attrs) {
119
+ this.eventSequence += 1;
120
+ const record = {
121
+ ...this.standardAttributes(),
122
+ "event.name": name,
123
+ "event.timestamp": new Date(this.now()).toISOString(),
124
+ "event.sequence": this.eventSequence,
125
+ ...(this.interaction ? { "prompt.id": this.interaction.promptId } : {}),
126
+ ...attrs,
127
+ };
128
+ try {
129
+ this.logger.emit({
130
+ eventName: `${PREFIX}.${name}`,
131
+ severityNumber: SeverityNumber.INFO,
132
+ severityText: "INFO",
133
+ body: `${PREFIX}.${name}`,
134
+ attributes: record,
135
+ });
136
+ }
137
+ catch {
138
+ // Logs are best-effort.
139
+ }
140
+ }
141
+ // ── Session ─────────────────────────────────────────────────────────────
142
+ sessionStart(sessionId) {
143
+ if (sessionId)
144
+ this.sessionId = sessionId;
145
+ if (!this.sessionCounted) {
146
+ this.sessionCounted = true;
147
+ this.add(METRIC_SESSION_COUNT, "1", "Count of CLI sessions started", 1);
148
+ }
149
+ this.idleSince = this.now();
150
+ }
151
+ getSessionId() {
152
+ return this.sessionId;
153
+ }
154
+ // ── Interaction (one user prompt) ───────────────────────────────────────
155
+ interactionStart(prompt) {
156
+ if (this.interaction)
157
+ this.interactionEnd();
158
+ const now = this.now();
159
+ // User active time: the window since the agent last went idle, unless the
160
+ // user walked away (idle cutoff).
161
+ if (this.idleSince !== undefined) {
162
+ const gap = now - this.idleSince;
163
+ if (gap > 0 && gap <= USER_ACTIVE_IDLE_CUTOFF_MS) {
164
+ this.add(METRIC_ACTIVE_TIME, "s", "Total active time in seconds", gap / 1000, { type: "user" });
165
+ }
166
+ this.idleSince = undefined;
167
+ }
168
+ this.interactionSequence += 1;
169
+ const promptId = randomUUID();
170
+ const promptLength = typeof prompt === "string" ? prompt.length : 0;
171
+ // `invoke_agent` is the semconv operation for the root of an agent run.
172
+ // Without it Datadog files the trace as a generic workflow, flags an
173
+ // instrumentation issue on the span, and leaves it out of per-agent views.
174
+ const span = this.tracer.startSpan(SPAN_INTERACTION, {
175
+ attributes: {
176
+ ...this.spanAttributes(),
177
+ [ATTR_GEN_AI_OPERATION_NAME]: "invoke_agent",
178
+ user_prompt_length: promptLength,
179
+ "interaction.sequence": this.interactionSequence,
180
+ "prompt.id": promptId,
181
+ },
182
+ });
183
+ const ctx = trace.setSpan(otelContext.active(), span);
184
+ this.interaction = { span, ctx, startedAt: now, promptId };
185
+ this.emitEvent(EVENT_USER_PROMPT, { prompt_length: promptLength });
186
+ }
187
+ interactionEnd() {
188
+ if (!this.interaction)
189
+ return;
190
+ const now = this.now();
191
+ // Close stragglers: pi guarantees agent_end after its children, but an
192
+ // abort can leave a turn or tool open.
193
+ if (this.llm)
194
+ this.llmEnd(undefined);
195
+ for (const [id] of this.tools)
196
+ this.toolEnd(id, { isError: false, resultBytes: 0 });
197
+ if (this.turn)
198
+ this.turnEnd();
199
+ const elapsed = now - this.interaction.startedAt;
200
+ this.interaction.span.setAttribute("interaction.duration_ms", Math.max(0, Math.round(elapsed)));
201
+ this.interaction.span.end();
202
+ this.interaction = null;
203
+ this.decisions.clear();
204
+ this.add(METRIC_ACTIVE_TIME, "s", "Total active time in seconds", Math.max(0, elapsed) / 1000, { type: "cli" });
205
+ this.idleSince = now;
206
+ }
207
+ // ── Turn ────────────────────────────────────────────────────────────────
208
+ turnStart(turnIndex) {
209
+ if (!this.interaction)
210
+ return;
211
+ if (this.turn)
212
+ this.turnEnd();
213
+ const span = this.tracer.startSpan(SPAN_TURN, { attributes: { ...this.spanAttributes(), ...(typeof turnIndex === "number" ? { "turn.index": turnIndex } : {}) } }, this.interaction.ctx);
214
+ this.turn = { span, ctx: trace.setSpan(this.interaction.ctx, span), startedAt: this.now() };
215
+ }
216
+ turnEnd() {
217
+ if (!this.turn)
218
+ return;
219
+ this.turn.span.end();
220
+ this.turn = null;
221
+ }
222
+ // ── LLM request ─────────────────────────────────────────────────────────
223
+ parentCtx() {
224
+ return this.turn?.ctx ?? this.interaction?.ctx ?? otelContext.active();
225
+ }
226
+ llmStart(model) {
227
+ if (this.llm)
228
+ this.llmEnd(undefined);
229
+ const parent = this.parentCtx();
230
+ const clientRequestId = randomUUID();
231
+ const attrs = {
232
+ ...this.spanAttributes(),
233
+ [ATTR_GEN_AI_OPERATION_NAME]: "chat",
234
+ client_request_id: clientRequestId,
235
+ "llm_request.context": this.interaction ? "interaction" : "standalone",
236
+ };
237
+ if (model) {
238
+ attrs[ATTR_GEN_AI_REQUEST_MODEL] = model;
239
+ attrs.model = model;
240
+ }
241
+ const span = this.tracer.startSpan(SPAN_LLM_REQUEST, { attributes: attrs }, parent);
242
+ this.llm = {
243
+ span,
244
+ ctx: trace.setSpan(parent, span),
245
+ startedAt: this.now(),
246
+ clientRequestId,
247
+ ...(model ? { requestModel: model } : {}),
248
+ };
249
+ }
250
+ llmResponseHeaders(status, headers) {
251
+ if (!this.llm)
252
+ return;
253
+ if (typeof status === "number") {
254
+ this.llm.status = status;
255
+ this.llm.span.setAttribute(ATTR_HTTP_STATUS_CODE, status);
256
+ }
257
+ const h = headers ?? {};
258
+ const requestId = h["x-request-id"] ?? h["request-id"] ?? h["anthropic-request-id"] ?? h["openai-response-id"];
259
+ if (typeof requestId === "string" && requestId) {
260
+ this.llm.requestId = requestId;
261
+ this.llm.span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, requestId);
262
+ this.llm.span.setAttribute("request_id", requestId);
263
+ }
264
+ }
265
+ /**
266
+ * The assistant message ended: attach usage, emit api_request / api_error
267
+ * plus the token and cost counters, and close the LLM span. Opens a span
268
+ * first when pi delivered the message without a before_provider_request
269
+ * (e.g. a replayed or synthetic message) so nothing is lost.
270
+ */
271
+ assistantMessageEnd(message) {
272
+ if (!this.llm)
273
+ this.llmStart(message.model);
274
+ const slot = this.llm;
275
+ const now = this.now();
276
+ const durationMs = Math.max(0, Math.round(now - slot.startedAt));
277
+ const model = message.responseModel ?? message.model ?? slot.requestModel ?? "unknown";
278
+ const usage = message.usage ?? {};
279
+ const input = num(usage.input);
280
+ const output = num(usage.output);
281
+ const cacheRead = num(usage.cacheRead);
282
+ const cacheCreation = num(usage.cacheWrite);
283
+ // Claude Code always reports cost_usd (0 when it has nothing), so the flat
284
+ // attribute and the counter keep that shape. The vendor cost attribute is
285
+ // different: a 0 there renders as "$0" in cost views, so it is only set
286
+ // when pi actually priced the request.
287
+ const costKnown = typeof usage.cost === "number" || typeof usage.cost?.total === "number";
288
+ const cost = typeof usage.cost === "number" ? usage.cost : num(usage.cost?.total);
289
+ const stop = message.stopReason ?? "stop";
290
+ const isError = stop === "error";
291
+ const requestId = message.responseId ?? slot.requestId;
292
+ // pi's `input` excludes cached tokens (Anthropic semantics). The semconv
293
+ // `gen_ai.usage.input_tokens` includes them, with the cache counters as
294
+ // subsets; see attrs.ts. The flat Claude Code attributes below stay disjoint.
295
+ const inclusiveInput = input + cacheRead + cacheCreation;
296
+ const span = slot.span;
297
+ if (message.model)
298
+ span.setAttribute(ATTR_GEN_AI_REQUEST_MODEL, message.model);
299
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_MODEL, model);
300
+ span.setAttribute("model", model);
301
+ span.setAttribute(ATTR_GEN_AI_INPUT_TOKENS, inclusiveInput);
302
+ span.setAttribute(ATTR_GEN_AI_OUTPUT_TOKENS, output);
303
+ span.setAttribute(ATTR_GEN_AI_TOTAL_TOKENS, inclusiveInput + output);
304
+ span.setAttribute(ATTR_GEN_AI_CACHE_READ_TOKENS, cacheRead);
305
+ span.setAttribute(ATTR_GEN_AI_CACHE_WRITE_TOKENS, cacheCreation);
306
+ span.setAttribute(ATTR_GEN_AI_CACHE_READ_TOKENS_LEGACY, cacheRead);
307
+ span.setAttribute(ATTR_GEN_AI_CACHE_CREATION_TOKENS_LEGACY, cacheCreation);
308
+ // Datadog ingests this attribute in nanodollars (its `ml_obs.span.llm.total.cost`
309
+ // unit); sending dollars renders a $0.012 call as 1.2e-9 cents. Verified against
310
+ // a live span on 2026-09-02. `cost_usd` below stays in dollars.
311
+ if (costKnown)
312
+ span.setAttribute(ATTR_GEN_AI_COST_ESTIMATED_TOTAL, Math.round(cost * 1_000_000_000));
313
+ span.setAttribute("input_tokens", input);
314
+ span.setAttribute("output_tokens", output);
315
+ span.setAttribute("cache_read_tokens", cacheRead);
316
+ span.setAttribute("cache_creation_tokens", cacheCreation);
317
+ span.setAttribute("cost_usd", cost);
318
+ span.setAttribute("duration_ms", durationMs);
319
+ span.setAttribute("stop_reason", stop);
320
+ span.setAttribute(ATTR_GEN_AI_FINISH_REASONS, [stop]);
321
+ span.setAttribute("success", isError ? "false" : "true");
322
+ span.setAttribute("response.has_tool_call", hasToolCall(message.content) ? "true" : "false");
323
+ if (requestId) {
324
+ span.setAttribute(ATTR_GEN_AI_RESPONSE_ID, requestId);
325
+ span.setAttribute("request_id", requestId);
326
+ }
327
+ const base = {
328
+ model,
329
+ duration_ms: durationMs,
330
+ client_request_id: slot.clientRequestId,
331
+ ...(requestId ? { request_id: requestId } : {}),
332
+ };
333
+ if (isError) {
334
+ // A short, single-line excerpt only: a provider error body can echo
335
+ // request content, and the contract is metadata-only. The status code
336
+ // and the leading words are what a dashboard needs.
337
+ const error = errorExcerpt(message.errorMessage);
338
+ span.setAttribute(ATTR_ERROR_TYPE, "api_error");
339
+ span.setAttribute("error", error);
340
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error });
341
+ this.emitEvent(EVENT_API_ERROR, {
342
+ ...base,
343
+ error,
344
+ attempt: 1,
345
+ ...(typeof slot.status === "number" ? { status_code: String(slot.status) } : {}),
346
+ });
347
+ }
348
+ else {
349
+ this.emitEvent(EVENT_API_REQUEST, {
350
+ ...base,
351
+ cost_usd: cost,
352
+ cost_usd_micros: Math.round(cost * 1_000_000),
353
+ input_tokens: input,
354
+ output_tokens: output,
355
+ cache_read_tokens: cacheRead,
356
+ cache_creation_tokens: cacheCreation,
357
+ });
358
+ this.emitEvent(EVENT_ASSISTANT_RESPONSE, {
359
+ model,
360
+ response_length: responseLength(message.content),
361
+ ...(requestId ? { request_id: requestId } : {}),
362
+ });
363
+ }
364
+ const tokenAttrs = (type) => ({ type, model });
365
+ this.add(METRIC_TOKEN_USAGE, "{token}", "Number of tokens used", input, tokenAttrs("input"));
366
+ this.add(METRIC_TOKEN_USAGE, "{token}", "Number of tokens used", output, tokenAttrs("output"));
367
+ this.add(METRIC_TOKEN_USAGE, "{token}", "Number of tokens used", cacheRead, tokenAttrs("cacheRead"));
368
+ this.add(METRIC_TOKEN_USAGE, "{token}", "Number of tokens used", cacheCreation, tokenAttrs("cacheCreation"));
369
+ this.add(METRIC_COST_USAGE, "USD", "Cost of the session", cost, { model });
370
+ this.llmEnd(undefined);
371
+ }
372
+ llmEnd(error) {
373
+ if (!this.llm)
374
+ return;
375
+ if (error) {
376
+ this.llm.span.setAttribute(ATTR_ERROR_TYPE, error.name || "Error");
377
+ this.llm.span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
378
+ }
379
+ this.llm.span.end();
380
+ this.llm = null;
381
+ }
382
+ // ── Tools ───────────────────────────────────────────────────────────────
383
+ toolDecision(input) {
384
+ const toolSource = input.toolName.startsWith("mcp__") ? "mcp" : "builtin";
385
+ // Only accepted calls go on to execute (and read this back in toolEnd);
386
+ // a rejected call never reaches tool_execution_end, so storing it would
387
+ // only leak.
388
+ if (input.toolCallId && input.decision === "accept") {
389
+ this.decisions.set(input.toolCallId, { decision: input.decision, source: input.source });
390
+ }
391
+ this.emitEvent(EVENT_TOOL_DECISION, {
392
+ tool_name: input.toolName,
393
+ ...(input.toolCallId ? { tool_use_id: input.toolCallId } : {}),
394
+ decision: input.decision,
395
+ source: input.source,
396
+ tool_source: toolSource,
397
+ });
398
+ if (EDIT_TOOLS.has(input.toolName)) {
399
+ const language = languageFromPath(input.filePath);
400
+ this.add(METRIC_CODE_EDIT_DECISION, "1", "Count of code editing tool permission decisions", 1, {
401
+ tool_name: input.toolName,
402
+ decision: input.decision,
403
+ source: input.source,
404
+ ...(language ? { language } : {}),
405
+ });
406
+ }
407
+ }
408
+ toolStart(toolCallId, toolName, args) {
409
+ if (this.tools.has(toolCallId))
410
+ return;
411
+ const parent = this.parentCtx();
412
+ const inputBytes = byteLength(args);
413
+ const attrs = {
414
+ ...this.spanAttributes(),
415
+ tool_name: toolName,
416
+ tool_use_id: toolCallId,
417
+ [ATTR_GEN_AI_TOOL_NAME]: toolName,
418
+ [ATTR_GEN_AI_TOOL_CALL_ID]: toolCallId,
419
+ [ATTR_GEN_AI_TOOL_TYPE]: toolName.startsWith("mcp__") ? "extension" : "function",
420
+ [ATTR_GEN_AI_OPERATION_NAME]: "execute_tool",
421
+ tool_input_size_bytes: inputBytes,
422
+ };
423
+ const span = this.tracer.startSpan(SPAN_TOOL, { attributes: attrs }, parent);
424
+ this.tools.set(toolCallId, { span, ctx: trace.setSpan(parent, span), startedAt: this.now(), name: toolName, inputBytes });
425
+ }
426
+ toolEnd(toolCallId, outcome) {
427
+ const slot = this.tools.get(toolCallId);
428
+ if (!slot)
429
+ return;
430
+ const durationMs = Math.max(0, Math.round(this.now() - slot.startedAt));
431
+ slot.span.setAttribute("duration_ms", durationMs);
432
+ slot.span.setAttribute("success", outcome.isError ? "false" : "true");
433
+ slot.span.setAttribute("tool_result_size_bytes", outcome.resultBytes);
434
+ if (outcome.isError) {
435
+ slot.span.setAttribute(ATTR_ERROR_TYPE, outcome.errorType ?? "tool_error");
436
+ slot.span.setStatus({ code: SpanStatusCode.ERROR });
437
+ }
438
+ slot.span.end();
439
+ this.tools.delete(toolCallId);
440
+ const decision = this.decisions.get(toolCallId);
441
+ this.decisions.delete(toolCallId);
442
+ this.emitEvent(EVENT_TOOL_RESULT, {
443
+ tool_name: slot.name,
444
+ tool_use_id: toolCallId,
445
+ success: outcome.isError ? "false" : "true",
446
+ duration_ms: durationMs,
447
+ tool_input_size_bytes: slot.inputBytes,
448
+ tool_result_size_bytes: outcome.resultBytes,
449
+ decision_type: "accept",
450
+ decision_source: decision?.source ?? "config",
451
+ ...(outcome.isError ? { error_type: outcome.errorType ?? "tool_error" } : {}),
452
+ });
453
+ }
454
+ /** Lines added/removed by an edit or write, from pi's tool_result details. */
455
+ linesOfCode(added, removed) {
456
+ this.add(METRIC_LINES_OF_CODE, "1", "Count of lines of code modified", added, { type: "added" });
457
+ this.add(METRIC_LINES_OF_CODE, "1", "Count of lines of code modified", removed, { type: "removed" });
458
+ }
459
+ /** A successful bash command: count commits and PR creations. */
460
+ bashSucceeded(command) {
461
+ if (typeof command !== "string")
462
+ return;
463
+ if (GIT_COMMIT_RE.test(command))
464
+ this.add(METRIC_COMMIT_COUNT, "1", "Count of git commits created", 1);
465
+ if (GH_PR_CREATE_RE.test(command))
466
+ this.add(METRIC_PULL_REQUEST_COUNT, "1", "Count of pull requests created", 1);
467
+ }
468
+ // ── Mode ────────────────────────────────────────────────────────────────
469
+ permissionModeChanged(fromMode, toMode) {
470
+ this.emitEvent(EVENT_PERMISSION_MODE_CHANGED, { from_mode: fromMode, to_mode: toMode });
471
+ }
472
+ // ── Shutdown ────────────────────────────────────────────────────────────
473
+ /** Close everything still open so no span is lost at exit. */
474
+ close() {
475
+ this.interactionEnd();
476
+ if (this.llm)
477
+ this.llmEnd(undefined);
478
+ for (const [id] of this.tools)
479
+ this.toolEnd(id, { isError: false, resultBytes: 0 });
480
+ if (this.turn)
481
+ this.turnEnd();
482
+ }
483
+ }
484
+ /** Cap on the exported error excerpt (characters, one line). */
485
+ export const ERROR_EXCERPT_CHARS = 120;
486
+ /** First line of a provider error, capped, never the full body. */
487
+ export function errorExcerpt(message) {
488
+ const firstLine = (message ?? "").split(/\r?\n/, 1)[0]?.trim() ?? "";
489
+ if (!firstLine)
490
+ return "LLM request failed";
491
+ return firstLine.length > ERROR_EXCERPT_CHARS ? `${firstLine.slice(0, ERROR_EXCERPT_CHARS)}…` : firstLine;
492
+ }
493
+ function num(v) {
494
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
495
+ }
496
+ function hasToolCall(content) {
497
+ if (!Array.isArray(content))
498
+ return false;
499
+ return content.some((p) => p && typeof p === "object" && p.type === "toolCall");
500
+ }
501
+ function responseLength(content) {
502
+ if (typeof content === "string")
503
+ return content.length;
504
+ if (!Array.isArray(content))
505
+ return 0;
506
+ let n = 0;
507
+ for (const p of content) {
508
+ if (p && typeof p === "object" && p.type === "text") {
509
+ const t = p.text;
510
+ if (typeof t === "string")
511
+ n += t.length;
512
+ }
513
+ }
514
+ return n;
515
+ }
516
+ export function byteLength(v) {
517
+ if (v === undefined || v === null)
518
+ return 0;
519
+ if (typeof v === "string")
520
+ return Buffer.byteLength(v, "utf8");
521
+ try {
522
+ return Buffer.byteLength(JSON.stringify(v) ?? "", "utf8");
523
+ }
524
+ catch {
525
+ return 0;
526
+ }
527
+ }
528
+ /** Count +/- lines in a unified patch, ignoring file headers. */
529
+ export function countPatchLines(patch) {
530
+ let added = 0;
531
+ let removed = 0;
532
+ if (typeof patch !== "string")
533
+ return { added, removed };
534
+ for (const line of patch.split("\n")) {
535
+ if (line.startsWith("+++") || line.startsWith("---"))
536
+ continue;
537
+ if (line.startsWith("+"))
538
+ added += 1;
539
+ else if (line.startsWith("-"))
540
+ removed += 1;
541
+ }
542
+ return { added, removed };
543
+ }
544
+ /** Line count of freshly written content (a write tool call). */
545
+ export function countContentLines(content) {
546
+ if (typeof content !== "string" || content.length === 0)
547
+ return 0;
548
+ const n = content.split("\n").length;
549
+ return content.endsWith("\n") ? n - 1 : n;
550
+ }
551
+ //# sourceMappingURL=tracker.js.map