@agimon-ai/doompi-log 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/extension.cjs +2 -0
- package/dist/extension.cjs.map +1 -0
- package/dist/extension.d.cts +33 -0
- package/dist/extension.d.cts.map +1 -0
- package/dist/extension.d.mts +33 -0
- package/dist/extension.d.mts.map +1 -0
- package/dist/extension.mjs +2 -0
- package/dist/extension.mjs.map +1 -0
- package/dist/extensions/pi.cjs +2 -0
- package/dist/extensions/pi.cjs.map +1 -0
- package/dist/extensions/pi.d.cts +4 -0
- package/dist/extensions/pi.d.cts.map +1 -0
- package/dist/extensions/pi.d.mts +5 -0
- package/dist/extensions/pi.d.mts.map +1 -0
- package/dist/extensions/pi.mjs +2 -0
- package/dist/extensions/pi.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1 -0
- package/dist/metrics.cjs +2 -0
- package/dist/metrics.cjs.map +1 -0
- package/dist/metrics.d.cts +94 -0
- package/dist/metrics.d.cts.map +1 -0
- package/dist/metrics.d.mts +94 -0
- package/dist/metrics.d.mts.map +1 -0
- package/dist/metrics.mjs +2 -0
- package/dist/metrics.mjs.map +1 -0
- package/dist/metricsSource.cjs +2 -0
- package/dist/metricsSource.cjs.map +1 -0
- package/dist/metricsSource.d.cts +25 -0
- package/dist/metricsSource.d.cts.map +1 -0
- package/dist/metricsSource.d.mts +25 -0
- package/dist/metricsSource.d.mts.map +1 -0
- package/dist/metricsSource.mjs +2 -0
- package/dist/metricsSource.mjs.map +1 -0
- package/dist/tui/doomOverlay.cjs +2 -0
- package/dist/tui/doomOverlay.cjs.map +1 -0
- package/dist/tui/doomOverlay.d.cts +63 -0
- package/dist/tui/doomOverlay.d.cts.map +1 -0
- package/dist/tui/doomOverlay.d.mts +63 -0
- package/dist/tui/doomOverlay.d.mts.map +1 -0
- package/dist/tui/doomOverlay.mjs +2 -0
- package/dist/tui/doomOverlay.mjs.map +1 -0
- package/dist/tui/metricsOverlay.cjs +2 -0
- package/dist/tui/metricsOverlay.cjs.map +1 -0
- package/dist/tui/metricsOverlay.d.cts +71 -0
- package/dist/tui/metricsOverlay.d.cts.map +1 -0
- package/dist/tui/metricsOverlay.d.mts +71 -0
- package/dist/tui/metricsOverlay.d.mts.map +1 -0
- package/dist/tui/metricsOverlay.mjs +2 -0
- package/dist/tui/metricsOverlay.mjs.map +1 -0
- package/dist/tui/rendering.cjs +2 -0
- package/dist/tui/rendering.cjs.map +1 -0
- package/dist/tui/rendering.mjs +2 -0
- package/dist/tui/rendering.mjs.map +1 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vuong Ngo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @agimon-ai/doompi-log
|
|
2
|
+
|
|
3
|
+
Pi log-sink telemetry extension with session-local metrics and a Log Metrics overlay.
|
|
4
|
+
|
|
5
|
+
Session-local metrics and a log metrics overlay, backed by log-sink telemetry.
|
|
6
|
+
|
|
7
|
+
Part of [Doompi](https://www.npmjs.com/package/@agimon-ai/doompi), a coding agent that loads
|
|
8
|
+
only the skills and tools you name. This package is composed by Doompi and is not usually
|
|
9
|
+
installed on its own.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @agimon-ai/doompi-log
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## How it loads
|
|
18
|
+
|
|
19
|
+
Doompi loads this unconditionally. It is core, not a layer, so it does not appear in
|
|
20
|
+
`.doom/modes.yaml`: making it optional would only create broken configurations.
|
|
21
|
+
|
|
22
|
+
Extension entry: `@agimon-ai/doompi-log/extensions/pi`
|
|
23
|
+
|
|
24
|
+
## Leader
|
|
25
|
+
|
|
26
|
+
`SPC h l` opens log metrics, once the extension is loaded.
|
|
27
|
+
|
|
28
|
+
## Entry points
|
|
29
|
+
|
|
30
|
+
| Import |
|
|
31
|
+
| ------------------------------------------ |
|
|
32
|
+
| `@agimon-ai/doompi-log` |
|
|
33
|
+
| `@agimon-ai/doompi-log/extension` |
|
|
34
|
+
| `@agimon-ai/doompi-log/extensions/pi` |
|
|
35
|
+
| `@agimon-ai/doompi-log/metrics` |
|
|
36
|
+
| `@agimon-ai/doompi-log/metricsSource` |
|
|
37
|
+
| `@agimon-ai/doompi-log/tui/metricsOverlay` |
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));exports.__toESM=s;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./metrics.cjs"),t=require("./metricsSource.cjs"),n=require("./tui/metricsOverlay.cjs");let r=require("@agimon-ai/doompi-telemetry"),i=require("@agimon-ai/doompi-extension-contracts/leader");const a=`log-metrics`,o=new Set([`error`,`aborted`]);function s(e){return e===`1`||e?.toLowerCase()===`true`||e?.toLowerCase()===`yes`}function c(e){let t={"pi.mode":e.mode,"pi.session.id":e.sessionManager.getSessionId()};return e.thinkingLevel&&(t[`pi.thinking_level`]=e.thinkingLevel),e.model&&(t[`gen_ai.provider.name`]=e.model.provider,t[`gen_ai.request.model`]=e.model.id),t}function l(e){if(e.role!==`assistant`)return;let t={"gen_ai.response.model":e.responseModel??e.model,"gen_ai.response.finish_reasons":e.stopReason,"gen_ai.usage.input_tokens":e.usage.input,"gen_ai.usage.output_tokens":e.usage.output,"gen_ai.usage.cache_read_tokens":e.usage.cacheRead,"gen_ai.usage.cache_write_tokens":e.usage.cacheWrite,"gen_ai.usage.total_tokens":e.usage.totalTokens,"gen_ai.usage.cost":e.usage.cost.total};return e.usage.reasoning!==void 0&&(t[`gen_ai.usage.reasoning_tokens`]=e.usage.reasoning),t}function u(e){if(e.role!==`assistant`)return{"error.type":`AgentError`,outcome:`error`};let t=e.stopReason===`aborted`;return{"error.type":`ProviderError`,"error.code":e.stopReason,outcome:t?`aborted`:`error`}}function d(e){return{service:e.serviceName,backend:e.backend,endpoint:e.endpoint??`none`,endpointSource:e.endpointSource,traces:e.traces,redaction:!0,fileFallback:e.fileFallback}}function f(e={}){return function(t){let n=e.env??process.env,i=e.metrics,a=new Map,f,p,m=async(e,t,n)=>{p&&(e===`debug`?await p.recordDebug(t,n):e===`warn`?await p.recordWarning(t,void 0,n):e===`error`?await p.recordError(t,void 0,n):await p.recordEvent(t,n))},h=t=>(p??=(0,r.createDoomTelemetry)({serviceName:e.serviceName??`pi`,packageName:`@agimon-ai/doompi-log`,cwd:t.cwd,env:n,allowFileFallback:e.allowFileFallback,telemetryFactory:e.telemetryFactory,warn:e.onDiagnostic,enableLogs:!0,enableTraces:n.AGENT_OTEL_TRACES===void 0||s(n.AGENT_OTEL_TRACES),onRecord:e=>i?.record(e.event,e.attributes),onStatus:t=>e.onSinkStatus?.(d(t))}),e.onSinkStatus?.(d(p.status())),p);t.on(`session_start`,async(e,t)=>{h(t),await m(`info`,`pi.session.started`,{...c(t),"pi.session.reason":e.reason,"pi.session.previous":e.previousSessionFile!==void 0})}),t.on(`before_agent_start`,async(e,t)=>{h(t),await m(`info`,`pi.user_prompt`,{...c(t),"pi.user_message.length":e.prompt.length,"pi.user_message.image_count":e.images?.length??0})}),t.on(`agent_start`,async(e,t)=>{f=Date.now(),h(t),await m(`info`,`pi.agent.started`,c(t))}),t.on(`turn_start`,async(e,t)=>{h(t),await m(`debug`,`pi.turn.started`,{...c(t),"pi.turn.index":e.turnIndex})}),t.on(`turn_end`,async(e,t)=>{h(t);let n=l(e.message);if(await m(`info`,`pi.turn.finished`,{...c(t),"pi.turn.index":e.turnIndex,"pi.tool_result.count":e.toolResults.length,...n}),e.message.role===`assistant`&&o.has(e.message.stopReason)&&await m(e.message.stopReason===`aborted`?`warn`:`error`,`pi.turn.failed`,{...c(t),"pi.turn.index":e.turnIndex,...u(e.message),...n}),n)for(let r of e.toolResults)await m(`debug`,`pi.tool_token_sample`,{...c(t),...n,"tool.name":r.toolName,token_attribution:`toolCallTurn`})}),t.on(`tool_execution_start`,async(e,t)=>{a.set(e.toolCallId,Date.now()),h(t),await m(`debug`,`pi.tool_call`,{...c(t),"tool.name":e.toolName,"tool.call.id":e.toolCallId})}),t.on(`tool_execution_end`,async(e,t)=>{let n=a.get(e.toolCallId);a.delete(e.toolCallId),h(t),await m(`info`,`pi.tool_result`,{...c(t),success:!e.isError,"tool.name":e.toolName,"tool.call.id":e.toolCallId,"tool.result.error":e.isError,...n===void 0?{}:{"tool.duration_ms":Date.now()-n}})}),t.on(`after_provider_response`,async(e,t)=>{h(t);let n={...c(t),"http.response.status_code":e.status};e.status>=400?await m(`error`,`pi.api_error`,n):await m(`debug`,`pi.api_response`,n)}),t.on(`model_select`,async(e,t)=>{h(t),await m(`info`,`pi.model.selected`,{...c(t),"pi.model.source":e.source,"gen_ai.provider.name":e.model.provider,"gen_ai.request.model":e.model.id})}),t.on(`agent_end`,async(e,t)=>{h(t),await m(`info`,`pi.agent.finished`,{...c(t),"pi.message.count":e.messages.length,...f===void 0?{}:{"pi.agent.duration_ms":Date.now()-f}}),f=void 0,await p?.flush()}),t.on(`agent_settled`,async(e,t)=>{h(t),await m(`debug`,`pi.agent.settled`,c(t))}),t.on(`session_shutdown`,async(e,t)=>{h(t),await m(`info`,`pi.session.finished`,{...c(t),"pi.session.reason":e.reason}),await p?.shutdown()})}}function p(e,t={}){return(0,i.registerDoomLeaderContribution)(e,{source:t.source??`@agimon-ai/doompi-log`,bindings:[{id:`log.metrics`,path:[{key:`h`,label:`help`,order:70},{key:`l`,label:`logs`,detail:`telemetry`}],command:{name:a}}]})}async function m(e,t,r){await e.ui.custom((e,i,a,o)=>new n.LogMetricsOverlayComponent(e,i,t,o,r),{overlay:!0,overlayOptions:{anchor:`top-left`,width:`100%`,maxHeight:`100%`,margin:0}})}function h(n={}){return function(r){let i=n.env??process.env,o=new e.LogMetricsAggregator,c=n.metricsSource??t.createMetricsSource({env:i}),l,u;f({...n,metrics:o,onDiagnostic:e=>{u=e,n.onDiagnostic?n.onDiagnostic(e):i.AGENT_OTEL_DIAGNOSTICS===`stderr`&&process.emitWarning(e)},onSinkStatus:e=>{l=e,n.onSinkStatus?.(e)}})(r),p(r,{source:n.leaderSource}),r.registerCommand(a,{description:`Show session-local log metrics`,handler:async(e,t)=>{await m(t,()=>({disabled:s(i.AGENT_TELEMETRY_DISABLED)||s(i.OTEL_SDK_DISABLED),snapshot:o.snapshot(),sink:l,transport:c.lastTransport(),lastDiagnostic:u}),c.query)}})}}exports.createDoomLogExtension=h,exports.createPiTelemetryExtension=f,exports.openLogMetricsOverlay=m,exports.registerLogMetricsLeaderBinding=p;
|
|
2
|
+
//# sourceMappingURL=extension.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.cjs","names":["createDoomTelemetry","registerDoomLeaderContribution","LogMetricsOverlayComponent","LogMetricsAggregator","createMetricsSource"],"sources":["../src/extension.ts"],"sourcesContent":["/**\n * Pi telemetry wiring for the Doom harness.\n *\n * This extension deliberately emits metadata-only records. Prompt, message,\n * tool payload, file path, diagnostic text, exception, and stack data never\n * cross the shared telemetry boundary.\n */\nimport type { ExtensionAPI, ExtensionContext, TurnEndEvent } from '@earendil-works/pi-coding-agent';\nimport { createDoomTelemetry, type DoomTelemetry, type DoomTelemetryOptions } from '@agimon-ai/doompi-telemetry';\nimport { registerDoomLeaderContribution } from '@agimon-ai/doompi-extension-contracts/leader';\nimport { LogMetricsAggregator, type LogMetricsRecorder } from './metrics.ts';\nimport { createMetricsSource, type MetricsQuery, type MetricsSource } from './metricsSource.ts';\nimport { LogMetricsOverlayComponent, type LogMetricsView, type SinkStatus } from './tui/metricsOverlay.ts';\n\nconst SERVICE_NAME = 'pi';\nconst PACKAGE_NAME = '@agimon-ai/doompi-log';\n/** Opt back into stderr diagnostics when debugging outside a live TUI. */\nconst DIAGNOSTICS_STDERR = 'stderr';\nconst LEADER_SOURCE = '@agimon-ai/doompi-log';\nconst LOG_METRICS_COMMAND = 'log-metrics';\nconst LOG_METRICS_DESCRIPTION = 'Show session-local log metrics';\nconst HELP_GROUP_ORDER = 70;\nconst NO_ENDPOINT = 'none';\nconst API_ERROR_STATUS = 400;\nconst FAILURE_STOP_REASONS = new Set(['error', 'aborted']);\n\ntype TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;\ntype TelemetryAttributes = Record<string, string | number | boolean>;\ntype RecordLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface PiTelemetryExtensionOptions {\n allowFileFallback?: boolean;\n /** Overrides the telemetry service identity. */\n serviceName?: string;\n /** Overrides the leader contribution owner. */\n leaderSource?: string;\n env?: NodeJS.ProcessEnv;\n telemetryFactory?: TelemetryFactory;\n /** Receives every sanitized record, whether or not a sink is live. */\n metrics?: LogMetricsRecorder;\n /** Called once the sink backend is resolved, so the overlay can report it. */\n onSinkStatus?: (status: SinkStatus | undefined) => void;\n /** Overrides the sink-history transport; injected in tests. */\n metricsSource?: MetricsSource;\n /** Receives telemetry lifecycle failures without writing into the TUI. */\n onDiagnostic?: (message: string) => void;\n}\n\nfunction isEnabled(value: string | undefined): boolean {\n return value === '1' || value?.toLowerCase() === 'true' || value?.toLowerCase() === 'yes';\n}\n\nfunction contextAttributes(ctx: ExtensionContext): TelemetryAttributes {\n const attributes: TelemetryAttributes = {\n 'pi.mode': ctx.mode,\n 'pi.session.id': ctx.sessionManager.getSessionId(),\n };\n if (ctx.thinkingLevel) attributes['pi.thinking_level'] = ctx.thinkingLevel;\n if (ctx.model) {\n attributes['gen_ai.provider.name'] = ctx.model.provider;\n attributes['gen_ai.request.model'] = ctx.model.id;\n }\n return attributes;\n}\n\nfunction usageAttributes(message: TurnEndEvent['message']): TelemetryAttributes | undefined {\n if (message.role !== 'assistant') return undefined;\n const attributes: TelemetryAttributes = {\n 'gen_ai.response.model': message.responseModel ?? message.model,\n 'gen_ai.response.finish_reasons': message.stopReason,\n 'gen_ai.usage.input_tokens': message.usage.input,\n 'gen_ai.usage.output_tokens': message.usage.output,\n 'gen_ai.usage.cache_read_tokens': message.usage.cacheRead,\n 'gen_ai.usage.cache_write_tokens': message.usage.cacheWrite,\n 'gen_ai.usage.total_tokens': message.usage.totalTokens,\n 'gen_ai.usage.cost': message.usage.cost.total,\n };\n if (message.usage.reasoning !== undefined) attributes['gen_ai.usage.reasoning_tokens'] = message.usage.reasoning;\n return attributes;\n}\n\nfunction failureAttributes(message: TurnEndEvent['message']): TelemetryAttributes {\n if (message.role !== 'assistant') return { 'error.type': 'AgentError', outcome: 'error' };\n const aborted = message.stopReason === 'aborted';\n return {\n 'error.type': 'ProviderError',\n 'error.code': message.stopReason,\n outcome: aborted ? 'aborted' : 'error',\n };\n}\n\nfunction statusFromTelemetry(status: ReturnType<DoomTelemetry['status']>): SinkStatus {\n return {\n service: status.serviceName,\n backend: status.backend,\n endpoint: status.endpoint ?? NO_ENDPOINT,\n endpointSource: status.endpointSource,\n traces: status.traces,\n redaction: true,\n fileFallback: status.fileFallback,\n };\n}\n\nexport function createPiTelemetryExtension(options: PiTelemetryExtensionOptions = {}) {\n return function registerPiTelemetry(pi: ExtensionAPI): void {\n const env = options.env ?? process.env;\n const metrics = options.metrics;\n const toolStartedAt = new Map<string, number>();\n let agentStartedAt: number | undefined;\n let telemetry: DoomTelemetry | undefined;\n\n const emit = async (level: RecordLevel, name: string, attributes: TelemetryAttributes): Promise<void> => {\n if (!telemetry) return;\n if (level === 'debug') await telemetry.recordDebug(name, attributes);\n else if (level === 'warn') await telemetry.recordWarning(name, undefined, attributes);\n else if (level === 'error') await telemetry.recordError(name, undefined, attributes);\n else await telemetry.recordEvent(name, attributes);\n };\n\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: options.serviceName ?? SERVICE_NAME,\n packageName: PACKAGE_NAME,\n cwd: ctx.cwd,\n env,\n allowFileFallback: options.allowFileFallback,\n telemetryFactory: options.telemetryFactory,\n warn: options.onDiagnostic,\n enableLogs: true,\n enableTraces: env.AGENT_OTEL_TRACES === undefined || isEnabled(env.AGENT_OTEL_TRACES),\n onRecord: (record) => metrics?.record(record.event, record.attributes),\n onStatus: (status) => options.onSinkStatus?.(statusFromTelemetry(status)),\n });\n options.onSinkStatus?.(statusFromTelemetry(telemetry.status()));\n return telemetry;\n };\n\n pi.on('session_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.session.started', {\n ...contextAttributes(ctx),\n 'pi.session.reason': event.reason,\n 'pi.session.previous': event.previousSessionFile !== undefined,\n });\n });\n\n pi.on('before_agent_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.user_prompt', {\n ...contextAttributes(ctx),\n 'pi.user_message.length': event.prompt.length,\n 'pi.user_message.image_count': event.images?.length ?? 0,\n });\n });\n\n pi.on('agent_start', async (_event, ctx) => {\n agentStartedAt = Date.now();\n getTelemetry(ctx);\n await emit('info', 'pi.agent.started', contextAttributes(ctx));\n });\n\n pi.on('turn_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('debug', 'pi.turn.started', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n });\n });\n\n pi.on('turn_end', async (event, ctx) => {\n getTelemetry(ctx);\n const usage = usageAttributes(event.message);\n await emit('info', 'pi.turn.finished', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n 'pi.tool_result.count': event.toolResults.length,\n ...usage,\n });\n if (event.message.role === 'assistant' && FAILURE_STOP_REASONS.has(event.message.stopReason)) {\n await emit(event.message.stopReason === 'aborted' ? 'warn' : 'error', 'pi.turn.failed', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n ...failureAttributes(event.message),\n ...usage,\n });\n }\n if (!usage) return;\n for (const result of event.toolResults) {\n await emit('debug', 'pi.tool_token_sample', {\n ...contextAttributes(ctx),\n ...usage,\n 'tool.name': result.toolName,\n token_attribution: 'toolCallTurn',\n });\n }\n });\n\n pi.on('tool_execution_start', async (event, ctx) => {\n toolStartedAt.set(event.toolCallId, Date.now());\n getTelemetry(ctx);\n await emit('debug', 'pi.tool_call', {\n ...contextAttributes(ctx),\n 'tool.name': event.toolName,\n 'tool.call.id': event.toolCallId,\n });\n });\n\n pi.on('tool_execution_end', async (event, ctx) => {\n const startedAt = toolStartedAt.get(event.toolCallId);\n toolStartedAt.delete(event.toolCallId);\n getTelemetry(ctx);\n await emit('info', 'pi.tool_result', {\n ...contextAttributes(ctx),\n success: !event.isError,\n 'tool.name': event.toolName,\n 'tool.call.id': event.toolCallId,\n 'tool.result.error': event.isError,\n ...(startedAt === undefined ? {} : { 'tool.duration_ms': Date.now() - startedAt }),\n });\n });\n\n pi.on('after_provider_response', async (event, ctx) => {\n getTelemetry(ctx);\n const attributes = {\n ...contextAttributes(ctx),\n 'http.response.status_code': event.status,\n };\n if (event.status >= API_ERROR_STATUS) await emit('error', 'pi.api_error', attributes);\n else await emit('debug', 'pi.api_response', attributes);\n });\n\n pi.on('model_select', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.model.selected', {\n ...contextAttributes(ctx),\n 'pi.model.source': event.source,\n 'gen_ai.provider.name': event.model.provider,\n 'gen_ai.request.model': event.model.id,\n });\n });\n\n pi.on('agent_end', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.agent.finished', {\n ...contextAttributes(ctx),\n 'pi.message.count': event.messages.length,\n ...(agentStartedAt === undefined ? {} : { 'pi.agent.duration_ms': Date.now() - agentStartedAt }),\n });\n agentStartedAt = undefined;\n await telemetry?.flush();\n });\n\n pi.on('agent_settled', async (_event, ctx) => {\n getTelemetry(ctx);\n await emit('debug', 'pi.agent.settled', contextAttributes(ctx));\n });\n\n pi.on('session_shutdown', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.session.finished', {\n ...contextAttributes(ctx),\n 'pi.session.reason': event.reason,\n });\n await telemetry?.shutdown();\n });\n };\n}\n\nexport function registerLogMetricsLeaderBinding(\n pi: Pick<ExtensionAPI, 'events' | 'on'>,\n options: { source?: string } = {},\n): () => void {\n return registerDoomLeaderContribution(pi, {\n source: options.source ?? LEADER_SOURCE,\n bindings: [\n {\n id: 'log.metrics',\n path: [\n { key: 'h', label: 'help', order: HELP_GROUP_ORDER },\n { key: 'l', label: 'logs', detail: 'telemetry' },\n ],\n command: { name: LOG_METRICS_COMMAND },\n },\n ],\n });\n}\n\nexport async function openLogMetricsOverlay(\n ctx: ExtensionContext,\n getView: () => LogMetricsView,\n query?: MetricsQuery,\n): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new LogMetricsOverlayComponent(tui, theme, getView, done, query),\n {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n },\n );\n}\n\nexport function createDoomLogExtension(options: PiTelemetryExtensionOptions = {}) {\n return function registerDoomLog(pi: ExtensionAPI): void {\n const env = options.env ?? process.env;\n const metrics = new LogMetricsAggregator();\n const metricsSource = options.metricsSource ?? createMetricsSource({ env });\n let sink: SinkStatus | undefined;\n let lastDiagnostic: string | undefined;\n\n createPiTelemetryExtension({\n ...options,\n metrics,\n // Always supply a sink so doom-telemetry never falls back to\n // process.emitWarning, which writes raw stderr over the TUI frame.\n // Diagnostics surface in the Log Metrics overlay instead.\n onDiagnostic: (message) => {\n lastDiagnostic = message;\n if (options.onDiagnostic) options.onDiagnostic(message);\n else if (env.AGENT_OTEL_DIAGNOSTICS === DIAGNOSTICS_STDERR) process.emitWarning(message);\n },\n onSinkStatus: (status) => {\n sink = status;\n options.onSinkStatus?.(status);\n },\n })(pi);\n\n registerLogMetricsLeaderBinding(pi, { source: options.leaderSource });\n pi.registerCommand(LOG_METRICS_COMMAND, {\n description: LOG_METRICS_DESCRIPTION,\n handler: async (_args, ctx) => {\n await openLogMetricsOverlay(\n ctx,\n () => ({\n disabled: isEnabled(env.AGENT_TELEMETRY_DISABLED) || isEnabled(env.OTEL_SDK_DISABLED),\n snapshot: metrics.snapshot(),\n sink,\n transport: metricsSource.lastTransport(),\n lastDiagnostic,\n }),\n metricsSource.query,\n );\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LogMetricsRecorder } from "./metrics.cjs";
|
|
2
|
+
import { MetricsQuery, MetricsSource } from "./metricsSource.cjs";
|
|
3
|
+
import { LogMetricsView, SinkStatus } from "./tui/metricsOverlay.cjs";
|
|
4
|
+
import { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { DoomTelemetryOptions } from "@agimon-ai/doompi-telemetry";
|
|
6
|
+
//#region src/extension.d.ts
|
|
7
|
+
type TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;
|
|
8
|
+
interface PiTelemetryExtensionOptions {
|
|
9
|
+
allowFileFallback?: boolean;
|
|
10
|
+
/** Overrides the telemetry service identity. */
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
/** Overrides the leader contribution owner. */
|
|
13
|
+
leaderSource?: string;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
telemetryFactory?: TelemetryFactory;
|
|
16
|
+
/** Receives every sanitized record, whether or not a sink is live. */
|
|
17
|
+
metrics?: LogMetricsRecorder;
|
|
18
|
+
/** Called once the sink backend is resolved, so the overlay can report it. */
|
|
19
|
+
onSinkStatus?: (status: SinkStatus | undefined) => void;
|
|
20
|
+
/** Overrides the sink-history transport; injected in tests. */
|
|
21
|
+
metricsSource?: MetricsSource;
|
|
22
|
+
/** Receives telemetry lifecycle failures without writing into the TUI. */
|
|
23
|
+
onDiagnostic?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
declare function createPiTelemetryExtension(options?: PiTelemetryExtensionOptions): (pi: ExtensionAPI) => void;
|
|
26
|
+
declare function registerLogMetricsLeaderBinding(pi: Pick<ExtensionAPI, 'events' | 'on'>, options?: {
|
|
27
|
+
source?: string;
|
|
28
|
+
}): () => void;
|
|
29
|
+
declare function openLogMetricsOverlay(ctx: ExtensionContext, getView: () => LogMetricsView, query?: MetricsQuery): Promise<void>;
|
|
30
|
+
declare function createDoomLogExtension(options?: PiTelemetryExtensionOptions): (pi: ExtensionAPI) => void;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
|
|
33
|
+
//# sourceMappingURL=extension.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.cts","names":[],"sources":["../src/extension.ts"],"mappings":";;;;;;KA0BK,mBAAmB,YAAY;UAInB;EACf;;EAEA;;EAEA;EACA,MAAM,OAAO;EACb,mBAAmB;;EAEnB,UAAU;;EAEV,gBAAgB,QAAQ;;EAExB,gBAAgB;;EAEhB,gBAAgB;;iBA0DF,2BAA2B,UAAS,+BACd,IAAI;iBAoK1B,gCACd,IAAI,KAAK,gCACT;EAAW;;iBAiBS,sBACpB,KAAK,kBACL,eAAe,gBACf,QAAQ,eACP;iBAea,uBAAuB,UAAS,+BACd,IAAI"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LogMetricsRecorder } from "./metrics.mjs";
|
|
2
|
+
import { MetricsQuery, MetricsSource } from "./metricsSource.mjs";
|
|
3
|
+
import { LogMetricsView, SinkStatus } from "./tui/metricsOverlay.mjs";
|
|
4
|
+
import { DoomTelemetryOptions } from "@agimon-ai/doompi-telemetry";
|
|
5
|
+
import { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
//#region src/extension.d.ts
|
|
7
|
+
type TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;
|
|
8
|
+
interface PiTelemetryExtensionOptions {
|
|
9
|
+
allowFileFallback?: boolean;
|
|
10
|
+
/** Overrides the telemetry service identity. */
|
|
11
|
+
serviceName?: string;
|
|
12
|
+
/** Overrides the leader contribution owner. */
|
|
13
|
+
leaderSource?: string;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
telemetryFactory?: TelemetryFactory;
|
|
16
|
+
/** Receives every sanitized record, whether or not a sink is live. */
|
|
17
|
+
metrics?: LogMetricsRecorder;
|
|
18
|
+
/** Called once the sink backend is resolved, so the overlay can report it. */
|
|
19
|
+
onSinkStatus?: (status: SinkStatus | undefined) => void;
|
|
20
|
+
/** Overrides the sink-history transport; injected in tests. */
|
|
21
|
+
metricsSource?: MetricsSource;
|
|
22
|
+
/** Receives telemetry lifecycle failures without writing into the TUI. */
|
|
23
|
+
onDiagnostic?: (message: string) => void;
|
|
24
|
+
}
|
|
25
|
+
declare function createPiTelemetryExtension(options?: PiTelemetryExtensionOptions): (pi: ExtensionAPI) => void;
|
|
26
|
+
declare function registerLogMetricsLeaderBinding(pi: Pick<ExtensionAPI, 'events' | 'on'>, options?: {
|
|
27
|
+
source?: string;
|
|
28
|
+
}): () => void;
|
|
29
|
+
declare function openLogMetricsOverlay(ctx: ExtensionContext, getView: () => LogMetricsView, query?: MetricsQuery): Promise<void>;
|
|
30
|
+
declare function createDoomLogExtension(options?: PiTelemetryExtensionOptions): (pi: ExtensionAPI) => void;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
|
|
33
|
+
//# sourceMappingURL=extension.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.mts","names":[],"sources":["../src/extension.ts"],"mappings":";;;;;;KA0BK,mBAAmB,YAAY;UAInB;EACf;;EAEA;;EAEA;EACA,MAAM,OAAO;EACb,mBAAmB;;EAEnB,UAAU;;EAEV,gBAAgB,QAAQ;;EAExB,gBAAgB;;EAEhB,gBAAgB;;iBA0DF,2BAA2B,UAAS,+BACd,IAAI;iBAoK1B,gCACd,IAAI,KAAK,gCACT;EAAW;;iBAiBS,sBACpB,KAAK,kBACL,eAAe,gBACf,QAAQ,eACP;iBAea,uBAAuB,UAAS,+BACd,IAAI"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{LogMetricsAggregator as e}from"./metrics.mjs";import{createMetricsSource as t}from"./metricsSource.mjs";import{LogMetricsOverlayComponent as n}from"./tui/metricsOverlay.mjs";import{createDoomTelemetry as r}from"@agimon-ai/doompi-telemetry";import{registerDoomLeaderContribution as i}from"@agimon-ai/doompi-extension-contracts/leader";const a=`log-metrics`,o=new Set([`error`,`aborted`]);function s(e){return e===`1`||e?.toLowerCase()===`true`||e?.toLowerCase()===`yes`}function c(e){let t={"pi.mode":e.mode,"pi.session.id":e.sessionManager.getSessionId()};return e.thinkingLevel&&(t[`pi.thinking_level`]=e.thinkingLevel),e.model&&(t[`gen_ai.provider.name`]=e.model.provider,t[`gen_ai.request.model`]=e.model.id),t}function l(e){if(e.role!==`assistant`)return;let t={"gen_ai.response.model":e.responseModel??e.model,"gen_ai.response.finish_reasons":e.stopReason,"gen_ai.usage.input_tokens":e.usage.input,"gen_ai.usage.output_tokens":e.usage.output,"gen_ai.usage.cache_read_tokens":e.usage.cacheRead,"gen_ai.usage.cache_write_tokens":e.usage.cacheWrite,"gen_ai.usage.total_tokens":e.usage.totalTokens,"gen_ai.usage.cost":e.usage.cost.total};return e.usage.reasoning!==void 0&&(t[`gen_ai.usage.reasoning_tokens`]=e.usage.reasoning),t}function u(e){if(e.role!==`assistant`)return{"error.type":`AgentError`,outcome:`error`};let t=e.stopReason===`aborted`;return{"error.type":`ProviderError`,"error.code":e.stopReason,outcome:t?`aborted`:`error`}}function d(e){return{service:e.serviceName,backend:e.backend,endpoint:e.endpoint??`none`,endpointSource:e.endpointSource,traces:e.traces,redaction:!0,fileFallback:e.fileFallback}}function f(e={}){return function(t){let n=e.env??process.env,i=e.metrics,a=new Map,f,p,m=async(e,t,n)=>{p&&(e===`debug`?await p.recordDebug(t,n):e===`warn`?await p.recordWarning(t,void 0,n):e===`error`?await p.recordError(t,void 0,n):await p.recordEvent(t,n))},h=t=>(p??=r({serviceName:e.serviceName??`pi`,packageName:`@agimon-ai/doompi-log`,cwd:t.cwd,env:n,allowFileFallback:e.allowFileFallback,telemetryFactory:e.telemetryFactory,warn:e.onDiagnostic,enableLogs:!0,enableTraces:n.AGENT_OTEL_TRACES===void 0||s(n.AGENT_OTEL_TRACES),onRecord:e=>i?.record(e.event,e.attributes),onStatus:t=>e.onSinkStatus?.(d(t))}),e.onSinkStatus?.(d(p.status())),p);t.on(`session_start`,async(e,t)=>{h(t),await m(`info`,`pi.session.started`,{...c(t),"pi.session.reason":e.reason,"pi.session.previous":e.previousSessionFile!==void 0})}),t.on(`before_agent_start`,async(e,t)=>{h(t),await m(`info`,`pi.user_prompt`,{...c(t),"pi.user_message.length":e.prompt.length,"pi.user_message.image_count":e.images?.length??0})}),t.on(`agent_start`,async(e,t)=>{f=Date.now(),h(t),await m(`info`,`pi.agent.started`,c(t))}),t.on(`turn_start`,async(e,t)=>{h(t),await m(`debug`,`pi.turn.started`,{...c(t),"pi.turn.index":e.turnIndex})}),t.on(`turn_end`,async(e,t)=>{h(t);let n=l(e.message);if(await m(`info`,`pi.turn.finished`,{...c(t),"pi.turn.index":e.turnIndex,"pi.tool_result.count":e.toolResults.length,...n}),e.message.role===`assistant`&&o.has(e.message.stopReason)&&await m(e.message.stopReason===`aborted`?`warn`:`error`,`pi.turn.failed`,{...c(t),"pi.turn.index":e.turnIndex,...u(e.message),...n}),n)for(let r of e.toolResults)await m(`debug`,`pi.tool_token_sample`,{...c(t),...n,"tool.name":r.toolName,token_attribution:`toolCallTurn`})}),t.on(`tool_execution_start`,async(e,t)=>{a.set(e.toolCallId,Date.now()),h(t),await m(`debug`,`pi.tool_call`,{...c(t),"tool.name":e.toolName,"tool.call.id":e.toolCallId})}),t.on(`tool_execution_end`,async(e,t)=>{let n=a.get(e.toolCallId);a.delete(e.toolCallId),h(t),await m(`info`,`pi.tool_result`,{...c(t),success:!e.isError,"tool.name":e.toolName,"tool.call.id":e.toolCallId,"tool.result.error":e.isError,...n===void 0?{}:{"tool.duration_ms":Date.now()-n}})}),t.on(`after_provider_response`,async(e,t)=>{h(t);let n={...c(t),"http.response.status_code":e.status};e.status>=400?await m(`error`,`pi.api_error`,n):await m(`debug`,`pi.api_response`,n)}),t.on(`model_select`,async(e,t)=>{h(t),await m(`info`,`pi.model.selected`,{...c(t),"pi.model.source":e.source,"gen_ai.provider.name":e.model.provider,"gen_ai.request.model":e.model.id})}),t.on(`agent_end`,async(e,t)=>{h(t),await m(`info`,`pi.agent.finished`,{...c(t),"pi.message.count":e.messages.length,...f===void 0?{}:{"pi.agent.duration_ms":Date.now()-f}}),f=void 0,await p?.flush()}),t.on(`agent_settled`,async(e,t)=>{h(t),await m(`debug`,`pi.agent.settled`,c(t))}),t.on(`session_shutdown`,async(e,t)=>{h(t),await m(`info`,`pi.session.finished`,{...c(t),"pi.session.reason":e.reason}),await p?.shutdown()})}}function p(e,t={}){return i(e,{source:t.source??`@agimon-ai/doompi-log`,bindings:[{id:`log.metrics`,path:[{key:`h`,label:`help`,order:70},{key:`l`,label:`logs`,detail:`telemetry`}],command:{name:a}}]})}async function m(e,t,r){await e.ui.custom((e,i,a,o)=>new n(e,i,t,o,r),{overlay:!0,overlayOptions:{anchor:`top-left`,width:`100%`,maxHeight:`100%`,margin:0}})}function h(n={}){return function(r){let i=n.env??process.env,o=new e,c=n.metricsSource??t({env:i}),l,u;f({...n,metrics:o,onDiagnostic:e=>{u=e,n.onDiagnostic?n.onDiagnostic(e):i.AGENT_OTEL_DIAGNOSTICS===`stderr`&&process.emitWarning(e)},onSinkStatus:e=>{l=e,n.onSinkStatus?.(e)}})(r),p(r,{source:n.leaderSource}),r.registerCommand(a,{description:`Show session-local log metrics`,handler:async(e,t)=>{await m(t,()=>({disabled:s(i.AGENT_TELEMETRY_DISABLED)||s(i.OTEL_SDK_DISABLED),snapshot:o.snapshot(),sink:l,transport:c.lastTransport(),lastDiagnostic:u}),c.query)}})}}export{h as createDoomLogExtension,f as createPiTelemetryExtension,m as openLogMetricsOverlay,p as registerLogMetricsLeaderBinding};
|
|
2
|
+
//# sourceMappingURL=extension.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.mjs","names":[],"sources":["../src/extension.ts"],"sourcesContent":["/**\n * Pi telemetry wiring for the Doom harness.\n *\n * This extension deliberately emits metadata-only records. Prompt, message,\n * tool payload, file path, diagnostic text, exception, and stack data never\n * cross the shared telemetry boundary.\n */\nimport type { ExtensionAPI, ExtensionContext, TurnEndEvent } from '@earendil-works/pi-coding-agent';\nimport { createDoomTelemetry, type DoomTelemetry, type DoomTelemetryOptions } from '@agimon-ai/doompi-telemetry';\nimport { registerDoomLeaderContribution } from '@agimon-ai/doompi-extension-contracts/leader';\nimport { LogMetricsAggregator, type LogMetricsRecorder } from './metrics.ts';\nimport { createMetricsSource, type MetricsQuery, type MetricsSource } from './metricsSource.ts';\nimport { LogMetricsOverlayComponent, type LogMetricsView, type SinkStatus } from './tui/metricsOverlay.ts';\n\nconst SERVICE_NAME = 'pi';\nconst PACKAGE_NAME = '@agimon-ai/doompi-log';\n/** Opt back into stderr diagnostics when debugging outside a live TUI. */\nconst DIAGNOSTICS_STDERR = 'stderr';\nconst LEADER_SOURCE = '@agimon-ai/doompi-log';\nconst LOG_METRICS_COMMAND = 'log-metrics';\nconst LOG_METRICS_DESCRIPTION = 'Show session-local log metrics';\nconst HELP_GROUP_ORDER = 70;\nconst NO_ENDPOINT = 'none';\nconst API_ERROR_STATUS = 400;\nconst FAILURE_STOP_REASONS = new Set(['error', 'aborted']);\n\ntype TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;\ntype TelemetryAttributes = Record<string, string | number | boolean>;\ntype RecordLevel = 'debug' | 'info' | 'warn' | 'error';\n\nexport interface PiTelemetryExtensionOptions {\n allowFileFallback?: boolean;\n /** Overrides the telemetry service identity. */\n serviceName?: string;\n /** Overrides the leader contribution owner. */\n leaderSource?: string;\n env?: NodeJS.ProcessEnv;\n telemetryFactory?: TelemetryFactory;\n /** Receives every sanitized record, whether or not a sink is live. */\n metrics?: LogMetricsRecorder;\n /** Called once the sink backend is resolved, so the overlay can report it. */\n onSinkStatus?: (status: SinkStatus | undefined) => void;\n /** Overrides the sink-history transport; injected in tests. */\n metricsSource?: MetricsSource;\n /** Receives telemetry lifecycle failures without writing into the TUI. */\n onDiagnostic?: (message: string) => void;\n}\n\nfunction isEnabled(value: string | undefined): boolean {\n return value === '1' || value?.toLowerCase() === 'true' || value?.toLowerCase() === 'yes';\n}\n\nfunction contextAttributes(ctx: ExtensionContext): TelemetryAttributes {\n const attributes: TelemetryAttributes = {\n 'pi.mode': ctx.mode,\n 'pi.session.id': ctx.sessionManager.getSessionId(),\n };\n if (ctx.thinkingLevel) attributes['pi.thinking_level'] = ctx.thinkingLevel;\n if (ctx.model) {\n attributes['gen_ai.provider.name'] = ctx.model.provider;\n attributes['gen_ai.request.model'] = ctx.model.id;\n }\n return attributes;\n}\n\nfunction usageAttributes(message: TurnEndEvent['message']): TelemetryAttributes | undefined {\n if (message.role !== 'assistant') return undefined;\n const attributes: TelemetryAttributes = {\n 'gen_ai.response.model': message.responseModel ?? message.model,\n 'gen_ai.response.finish_reasons': message.stopReason,\n 'gen_ai.usage.input_tokens': message.usage.input,\n 'gen_ai.usage.output_tokens': message.usage.output,\n 'gen_ai.usage.cache_read_tokens': message.usage.cacheRead,\n 'gen_ai.usage.cache_write_tokens': message.usage.cacheWrite,\n 'gen_ai.usage.total_tokens': message.usage.totalTokens,\n 'gen_ai.usage.cost': message.usage.cost.total,\n };\n if (message.usage.reasoning !== undefined) attributes['gen_ai.usage.reasoning_tokens'] = message.usage.reasoning;\n return attributes;\n}\n\nfunction failureAttributes(message: TurnEndEvent['message']): TelemetryAttributes {\n if (message.role !== 'assistant') return { 'error.type': 'AgentError', outcome: 'error' };\n const aborted = message.stopReason === 'aborted';\n return {\n 'error.type': 'ProviderError',\n 'error.code': message.stopReason,\n outcome: aborted ? 'aborted' : 'error',\n };\n}\n\nfunction statusFromTelemetry(status: ReturnType<DoomTelemetry['status']>): SinkStatus {\n return {\n service: status.serviceName,\n backend: status.backend,\n endpoint: status.endpoint ?? NO_ENDPOINT,\n endpointSource: status.endpointSource,\n traces: status.traces,\n redaction: true,\n fileFallback: status.fileFallback,\n };\n}\n\nexport function createPiTelemetryExtension(options: PiTelemetryExtensionOptions = {}) {\n return function registerPiTelemetry(pi: ExtensionAPI): void {\n const env = options.env ?? process.env;\n const metrics = options.metrics;\n const toolStartedAt = new Map<string, number>();\n let agentStartedAt: number | undefined;\n let telemetry: DoomTelemetry | undefined;\n\n const emit = async (level: RecordLevel, name: string, attributes: TelemetryAttributes): Promise<void> => {\n if (!telemetry) return;\n if (level === 'debug') await telemetry.recordDebug(name, attributes);\n else if (level === 'warn') await telemetry.recordWarning(name, undefined, attributes);\n else if (level === 'error') await telemetry.recordError(name, undefined, attributes);\n else await telemetry.recordEvent(name, attributes);\n };\n\n const getTelemetry = (ctx: ExtensionContext): DoomTelemetry => {\n telemetry ??= createDoomTelemetry({\n serviceName: options.serviceName ?? SERVICE_NAME,\n packageName: PACKAGE_NAME,\n cwd: ctx.cwd,\n env,\n allowFileFallback: options.allowFileFallback,\n telemetryFactory: options.telemetryFactory,\n warn: options.onDiagnostic,\n enableLogs: true,\n enableTraces: env.AGENT_OTEL_TRACES === undefined || isEnabled(env.AGENT_OTEL_TRACES),\n onRecord: (record) => metrics?.record(record.event, record.attributes),\n onStatus: (status) => options.onSinkStatus?.(statusFromTelemetry(status)),\n });\n options.onSinkStatus?.(statusFromTelemetry(telemetry.status()));\n return telemetry;\n };\n\n pi.on('session_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.session.started', {\n ...contextAttributes(ctx),\n 'pi.session.reason': event.reason,\n 'pi.session.previous': event.previousSessionFile !== undefined,\n });\n });\n\n pi.on('before_agent_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.user_prompt', {\n ...contextAttributes(ctx),\n 'pi.user_message.length': event.prompt.length,\n 'pi.user_message.image_count': event.images?.length ?? 0,\n });\n });\n\n pi.on('agent_start', async (_event, ctx) => {\n agentStartedAt = Date.now();\n getTelemetry(ctx);\n await emit('info', 'pi.agent.started', contextAttributes(ctx));\n });\n\n pi.on('turn_start', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('debug', 'pi.turn.started', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n });\n });\n\n pi.on('turn_end', async (event, ctx) => {\n getTelemetry(ctx);\n const usage = usageAttributes(event.message);\n await emit('info', 'pi.turn.finished', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n 'pi.tool_result.count': event.toolResults.length,\n ...usage,\n });\n if (event.message.role === 'assistant' && FAILURE_STOP_REASONS.has(event.message.stopReason)) {\n await emit(event.message.stopReason === 'aborted' ? 'warn' : 'error', 'pi.turn.failed', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n ...failureAttributes(event.message),\n ...usage,\n });\n }\n if (!usage) return;\n for (const result of event.toolResults) {\n await emit('debug', 'pi.tool_token_sample', {\n ...contextAttributes(ctx),\n ...usage,\n 'tool.name': result.toolName,\n token_attribution: 'toolCallTurn',\n });\n }\n });\n\n pi.on('tool_execution_start', async (event, ctx) => {\n toolStartedAt.set(event.toolCallId, Date.now());\n getTelemetry(ctx);\n await emit('debug', 'pi.tool_call', {\n ...contextAttributes(ctx),\n 'tool.name': event.toolName,\n 'tool.call.id': event.toolCallId,\n });\n });\n\n pi.on('tool_execution_end', async (event, ctx) => {\n const startedAt = toolStartedAt.get(event.toolCallId);\n toolStartedAt.delete(event.toolCallId);\n getTelemetry(ctx);\n await emit('info', 'pi.tool_result', {\n ...contextAttributes(ctx),\n success: !event.isError,\n 'tool.name': event.toolName,\n 'tool.call.id': event.toolCallId,\n 'tool.result.error': event.isError,\n ...(startedAt === undefined ? {} : { 'tool.duration_ms': Date.now() - startedAt }),\n });\n });\n\n pi.on('after_provider_response', async (event, ctx) => {\n getTelemetry(ctx);\n const attributes = {\n ...contextAttributes(ctx),\n 'http.response.status_code': event.status,\n };\n if (event.status >= API_ERROR_STATUS) await emit('error', 'pi.api_error', attributes);\n else await emit('debug', 'pi.api_response', attributes);\n });\n\n pi.on('model_select', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.model.selected', {\n ...contextAttributes(ctx),\n 'pi.model.source': event.source,\n 'gen_ai.provider.name': event.model.provider,\n 'gen_ai.request.model': event.model.id,\n });\n });\n\n pi.on('agent_end', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.agent.finished', {\n ...contextAttributes(ctx),\n 'pi.message.count': event.messages.length,\n ...(agentStartedAt === undefined ? {} : { 'pi.agent.duration_ms': Date.now() - agentStartedAt }),\n });\n agentStartedAt = undefined;\n await telemetry?.flush();\n });\n\n pi.on('agent_settled', async (_event, ctx) => {\n getTelemetry(ctx);\n await emit('debug', 'pi.agent.settled', contextAttributes(ctx));\n });\n\n pi.on('session_shutdown', async (event, ctx) => {\n getTelemetry(ctx);\n await emit('info', 'pi.session.finished', {\n ...contextAttributes(ctx),\n 'pi.session.reason': event.reason,\n });\n await telemetry?.shutdown();\n });\n };\n}\n\nexport function registerLogMetricsLeaderBinding(\n pi: Pick<ExtensionAPI, 'events' | 'on'>,\n options: { source?: string } = {},\n): () => void {\n return registerDoomLeaderContribution(pi, {\n source: options.source ?? LEADER_SOURCE,\n bindings: [\n {\n id: 'log.metrics',\n path: [\n { key: 'h', label: 'help', order: HELP_GROUP_ORDER },\n { key: 'l', label: 'logs', detail: 'telemetry' },\n ],\n command: { name: LOG_METRICS_COMMAND },\n },\n ],\n });\n}\n\nexport async function openLogMetricsOverlay(\n ctx: ExtensionContext,\n getView: () => LogMetricsView,\n query?: MetricsQuery,\n): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new LogMetricsOverlayComponent(tui, theme, getView, done, query),\n {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n },\n );\n}\n\nexport function createDoomLogExtension(options: PiTelemetryExtensionOptions = {}) {\n return function registerDoomLog(pi: ExtensionAPI): void {\n const env = options.env ?? process.env;\n const metrics = new LogMetricsAggregator();\n const metricsSource = options.metricsSource ?? createMetricsSource({ env });\n let sink: SinkStatus | undefined;\n let lastDiagnostic: string | undefined;\n\n createPiTelemetryExtension({\n ...options,\n metrics,\n // Always supply a sink so doom-telemetry never falls back to\n // process.emitWarning, which writes raw stderr over the TUI frame.\n // Diagnostics surface in the Log Metrics overlay instead.\n onDiagnostic: (message) => {\n lastDiagnostic = message;\n if (options.onDiagnostic) options.onDiagnostic(message);\n else if (env.AGENT_OTEL_DIAGNOSTICS === DIAGNOSTICS_STDERR) process.emitWarning(message);\n },\n onSinkStatus: (status) => {\n sink = status;\n options.onSinkStatus?.(status);\n },\n })(pi);\n\n registerLogMetricsLeaderBinding(pi, { source: options.leaderSource });\n pi.registerCommand(LOG_METRICS_COMMAND, {\n description: LOG_METRICS_DESCRIPTION,\n handler: async (_args, ctx) => {\n await openLogMetricsOverlay(\n ctx,\n () => ({\n disabled: isEnabled(env.AGENT_TELEMETRY_DISABLED) || isEnabled(env.OTEL_SDK_DISABLED),\n snapshot: metrics.snapshot(),\n sink,\n transport: metricsSource.lastTransport(),\n lastDiagnostic,\n }),\n metricsSource.query,\n );\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.cjs","names":["createDoomLogExtension"],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { createDoomLogExtension } from '../extension.ts';\n\nexport default createDoomLogExtension();\n"],"mappings":"AAEA,IAAA,4BAAeA,CAAAA,CAAAA,uBAAuB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.d.cts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.d.mts","names":[],"sources":["../../src/extensions/pi.ts"],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pi.mjs","names":[],"sources":["../../src/extensions/pi.ts"],"sourcesContent":["import { createDoomLogExtension } from '../extension.ts';\n\nexport default createDoomLogExtension();\n"],"mappings":"0DAEA,IAAA,EAAe,EAAuB"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./metrics.cjs"),t=require("./tui/metricsOverlay.cjs"),n=require("./extension.cjs");exports.API_ERROR_RECORD=e.API_ERROR_RECORD,exports.LogMetricsAggregator=e.LogMetricsAggregator,exports.LogMetricsOverlayComponent=t.LogMetricsOverlayComponent,exports.MAX_RETAINED_STATE_PER_TOOL=e.MAX_RETAINED_STATE_PER_TOOL,exports.TOOL_RESULT_RECORD=e.TOOL_RESULT_RECORD,exports.TURN_FINISHED_RECORD=e.TURN_FINISHED_RECORD,exports.createDoomLogExtension=n.createDoomLogExtension,exports.createPiTelemetryExtension=n.createPiTelemetryExtension,exports.openLogMetricsOverlay=n.openLogMetricsOverlay,exports.registerLogMetricsLeaderBinding=n.registerLogMetricsLeaderBinding;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./metrics.cjs";
|
|
2
|
+
import { LogMetricsOverlayComponent, LogMetricsView, SinkStatus } from "./tui/metricsOverlay.cjs";
|
|
3
|
+
import { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding } from "./extension.cjs";
|
|
4
|
+
export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsOverlayComponent, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, type PiTelemetryExtensionOptions, SinkStatus, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD } from "./metrics.mjs";
|
|
2
|
+
import { LogMetricsOverlayComponent, LogMetricsView, SinkStatus } from "./tui/metricsOverlay.mjs";
|
|
3
|
+
import { PiTelemetryExtensionOptions, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding } from "./extension.mjs";
|
|
4
|
+
export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsOverlayComponent, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, LogMetricsView, MAX_RETAINED_STATE_PER_TOOL, type PiTelemetryExtensionOptions, SinkStatus, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD, createDoomLogExtension, createPiTelemetryExtension, openLogMetricsOverlay, registerLogMetricsLeaderBinding };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_ERROR_RECORD as e,LogMetricsAggregator as t,MAX_RETAINED_STATE_PER_TOOL as n,TOOL_RESULT_RECORD as r,TURN_FINISHED_RECORD as i}from"./metrics.mjs";import{LogMetricsOverlayComponent as a}from"./tui/metricsOverlay.mjs";import{createDoomLogExtension as o,createPiTelemetryExtension as s,openLogMetricsOverlay as c,registerLogMetricsLeaderBinding as l}from"./extension.mjs";export{e as API_ERROR_RECORD,t as LogMetricsAggregator,a as LogMetricsOverlayComponent,n as MAX_RETAINED_STATE_PER_TOOL,r as TOOL_RESULT_RECORD,i as TURN_FINISHED_RECORD,o as createDoomLogExtension,s as createPiTelemetryExtension,c as openLogMetricsOverlay,l as registerLogMetricsLeaderBinding};
|
package/dist/metrics.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=1.1,t=Math.log(e);function n(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function r(e){return typeof e==`string`&&e.length>0?e:void 0}function i(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function a(e){let n=Math.ceil(Math.log(Math.max(e,1))/t);return Math.min(Math.max(n,0),159)}function o(t){return e**t}function s(e){if(e.samples===0)return;let t=e.samples*.95,n=[...e.buckets.keys()].sort((e,t)=>e-t),r=0,i=n[0];for(let a of n)if(r+=e.buckets.get(a)??0,i=a,r>=t)break;return Math.round(o(i))}function c(e){return e.startsWith(`pi.`)?e.slice(3):e}var l=class{events=0;errors=0;toolCalls=0;failedToolCalls=0;cost=0;tokens={input:0,output:0,total:0};eventCounts=new Map;packageCounts=new Map;tools=new Map;operations=new Map;sanitizedFailures=0;errorLog=[];now;maxRecentErrors;constructor(e={}){this.now=e.now??Date.now,this.maxRecentErrors=e.maxRecentErrors??8}record(e,t){let n=i(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let a=r(t[`telemetry.package`]);if(a&&!a.startsWith(`/`)&&!a.includes(`\\`)){let e=i(a);this.packageCounts.set(e,(this.packageCounts.get(e)??0)+1)}this.recordOperationDuration(n,t),this.recordSanitizedFailure(n,t),n===`pi.tool_result`?this.recordToolResult(n,t):n===`pi.api_error`?this.recordApiError(n,t):n===`pi.turn.finished`&&this.recordTurnUsage(t)}snapshot(){let e=[...this.tools.entries()].map(([e,t])=>{let n=s(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1)),t=[...this.eventCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),n=[...this.packageCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),r=[...this.operations.entries()].map(([e,t])=>{let n=s(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1));return{events:this.events,errors:this.errors,toolCalls:this.toolCalls,failedToolCalls:this.failedToolCalls,tokens:{...this.tokens},cost:this.cost,toolLatency:e,eventVolume:t,packageEvents:n,sanitizedFailures:this.sanitizedFailures,operationDuration:r,recentErrors:this.errorLog.map(e=>({...e}))}}recordOperationDuration(e,t){let o=Object.entries(t).find(([e,t])=>e!==`tool.duration_ms`&&(e===`duration_ms`||e.endsWith(`.duration_ms`))&&n(t)!==void 0);if(!o)return;let s=i(r(t[`operation.name`])??e),c=this.operations.get(s)??{calls:0,samples:0,buckets:new Map};this.operations.set(s,c),c.calls+=1;let l=n(o[1]);if(l===void 0)return;let u=a(l);c.buckets.set(u,(c.buckets.get(u)??0)+1),c.samples+=1}recordSanitizedFailure(e,t){let n=r(t.outcome);if(t[`tool.result.error`]!==!0&&e!==`pi.api_error`&&n!==`error`&&n!==`failed`&&n!==`failure`||(this.sanitizedFailures+=1,e===`pi.tool_result`||e===`pi.api_error`))return;let a=r(t[`error.code`])??n??`error`;this.pushError(e,`${r(t[`error.type`])??`operation`} failed`,i(a))}recordToolResult(e,t){let i=r(t[`tool.name`])??`unknown`,o=this.tools.get(i)??{calls:0,samples:0,buckets:new Map};this.tools.set(i,o),o.calls+=1,this.toolCalls+=1;let s=n(t[`tool.duration_ms`]);if(s!==void 0){let e=a(s);o.buckets.set(e,(o.buckets.get(e)??0)+1),o.samples+=1}t[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.pushError(e,`${i}: tool call failed`,`tool`))}recordApiError(e,t){this.errors+=1;let n=String(t[`http.response.status_code`]);this.pushError(e,`provider responded ${n}`,n)}recordTurnUsage(e){this.tokens.input+=n(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=n(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=n(e[`gen_ai.usage.total_tokens`])??0,this.cost+=n(e[`gen_ai.usage.cost`])??0}pushError(e,t,n){this.errorLog.unshift({at:this.now(),event:c(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};exports.API_ERROR_RECORD=`pi.api_error`,exports.LogMetricsAggregator=l,exports.MAX_RETAINED_STATE_PER_TOOL=160,exports.TOOL_RESULT_RECORD=`pi.tool_result`,exports.TURN_FINISHED_RECORD=`pi.turn.finished`;
|
|
2
|
+
//# sourceMappingURL=metrics.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.cjs","names":[],"sources":["../src/metrics.ts"],"sourcesContent":["/**\n * Session-local aggregation of the log records the Pi telemetry extension\n * already emits.\n *\n * The aggregator is fed `(recordName, attributes)` pairs rather than Pi events\n * so it can only ever see what the sink sees: no extra instrumentation is added\n * for metrics, and nothing here depends on a live sink. Everything is scoped to\n * the current process, so there is no persistence and no query API.\n */\n\n/** Record names the aggregator derives numbers from. */\nexport const TOOL_RESULT_RECORD = 'pi.tool_result';\nexport const API_ERROR_RECORD = 'pi.api_error';\nexport const TURN_FINISHED_RECORD = 'pi.turn.finished';\n\nconst UNKNOWN_TOOL = 'unknown';\nconst TOOL_ERROR_CODE = 'tool';\nconst RECORD_PREFIX = 'pi.';\nconst DEFAULT_MAX_RECENT_ERRORS = 8;\nconst P95_QUANTILE = 0.95;\n\n/**\n * Latency is kept as a fixed logarithmic histogram rather than a sample array,\n * so a long session cannot grow the retained state without bound. Each bucket\n * is 10% wider than the last, which keeps the reported p95 within about 10% of\n * the true value while capping the state at one small map per tool.\n */\nconst LATENCY_BUCKET_GROWTH = 1.1;\nconst LATENCY_BUCKET_LOG_BASE = Math.log(LATENCY_BUCKET_GROWTH);\nconst MINIMUM_SAMPLE_MS = 1;\n\n/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */\nexport const MAX_RETAINED_STATE_PER_TOOL = 160;\n\nexport interface LogMetricsTokenTotals {\n input: number;\n output: number;\n total: number;\n}\n\nexport interface LogMetricsToolLatency {\n name: string;\n calls: number;\n p95Ms?: number;\n /** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */\n retainedStateSize: number;\n}\n\nexport interface LogMetricsEventCount {\n name: string;\n count: number;\n}\n\nexport interface LogMetricsOperationDuration {\n name: string;\n calls: number;\n p95Ms?: number;\n retainedStateSize: number;\n}\n\nexport interface LogMetricsError {\n at: number;\n event: string;\n message: string;\n code: string;\n}\n\nexport interface LogMetricsSnapshot {\n events: number;\n errors: number;\n toolCalls: number;\n failedToolCalls: number;\n tokens: LogMetricsTokenTotals;\n cost: number;\n toolLatency: LogMetricsToolLatency[];\n eventVolume: LogMetricsEventCount[];\n packageEvents: LogMetricsEventCount[];\n sanitizedFailures: number;\n operationDuration: LogMetricsOperationDuration[];\n recentErrors: LogMetricsError[];\n}\n\nexport interface LogMetricsAggregatorOptions {\n now?: () => number;\n maxRecentErrors?: number;\n}\n\n/** The slice of the aggregator the telemetry extension needs. */\nexport interface LogMetricsRecorder {\n record(name: string, attributes: Record<string, unknown>): void;\n}\n\ninterface ToolLatencyState {\n calls: number;\n samples: number;\n buckets: Map<number, number>;\n}\n\nfunction readNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction readString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n\nfunction safeMetricName(value: string): string {\n const normalized = value.replace(/[^a-zA-Z0-9@/._:-]/g, '_').slice(0, 128);\n return normalized || 'unknown';\n}\n\nfunction latencyBucket(durationMs: number): number {\n const index = Math.ceil(Math.log(Math.max(durationMs, MINIMUM_SAMPLE_MS)) / LATENCY_BUCKET_LOG_BASE);\n return Math.min(Math.max(index, 0), MAX_RETAINED_STATE_PER_TOOL - 1);\n}\n\nfunction bucketUpperBoundMs(index: number): number {\n return LATENCY_BUCKET_GROWTH ** index;\n}\n\n/**\n * The upper edge of the first bucket whose cumulative count crosses the\n * quantile. Reporting the edge rather than a bucket midpoint keeps the answer\n * an upper bound, so a single 250ms call never reports faster than 250ms.\n */\nfunction bucketedP95(state: ToolLatencyState): number | undefined {\n if (state.samples === 0) return undefined;\n\n const target = state.samples * P95_QUANTILE;\n const indexes = [...state.buckets.keys()].sort((left, right) => left - right);\n let cumulative = 0;\n let selected = indexes[0] as number;\n for (const index of indexes) {\n cumulative += state.buckets.get(index) ?? 0;\n selected = index;\n if (cumulative >= target) break;\n }\n return Math.round(bucketUpperBoundMs(selected));\n}\n\n/** Strips the `pi.` prefix so an error entry reads as the event it came from. */\nfunction errorEventName(recordName: string): string {\n return recordName.startsWith(RECORD_PREFIX) ? recordName.slice(RECORD_PREFIX.length) : recordName;\n}\n\nexport class LogMetricsAggregator implements LogMetricsRecorder {\n private events = 0;\n private errors = 0;\n private toolCalls = 0;\n private failedToolCalls = 0;\n private cost = 0;\n private readonly tokens: LogMetricsTokenTotals = { input: 0, output: 0, total: 0 };\n private readonly eventCounts = new Map<string, number>();\n private readonly packageCounts = new Map<string, number>();\n private readonly tools = new Map<string, ToolLatencyState>();\n private readonly operations = new Map<string, ToolLatencyState>();\n private sanitizedFailures = 0;\n private readonly errorLog: LogMetricsError[] = [];\n private readonly now: () => number;\n private readonly maxRecentErrors: number;\n\n constructor(options: LogMetricsAggregatorOptions = {}) {\n this.now = options.now ?? Date.now;\n this.maxRecentErrors = options.maxRecentErrors ?? DEFAULT_MAX_RECENT_ERRORS;\n }\n\n record(name: string, attributes: Record<string, unknown>): void {\n const metricName = safeMetricName(name);\n this.events += 1;\n this.eventCounts.set(metricName, (this.eventCounts.get(metricName) ?? 0) + 1);\n const packageName = readString(attributes['telemetry.package']);\n if (packageName && !packageName.startsWith('/') && !packageName.includes('\\\\')) {\n const safePackageName = safeMetricName(packageName);\n this.packageCounts.set(safePackageName, (this.packageCounts.get(safePackageName) ?? 0) + 1);\n }\n this.recordOperationDuration(metricName, attributes);\n this.recordSanitizedFailure(metricName, attributes);\n\n if (metricName === TOOL_RESULT_RECORD) this.recordToolResult(metricName, attributes);\n else if (metricName === API_ERROR_RECORD) this.recordApiError(metricName, attributes);\n else if (metricName === TURN_FINISHED_RECORD) this.recordTurnUsage(attributes);\n }\n\n snapshot(): LogMetricsSnapshot {\n const toolLatency = [...this.tools.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n const eventVolume = [...this.eventCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const packageEvents = [...this.packageCounts.entries()]\n .map(([name, count]) => ({ name, count }))\n .sort((left, right) => right.count - left.count);\n const operationDuration = [...this.operations.entries()]\n .map(([name, state]) => {\n const p95Ms = bucketedP95(state);\n return {\n name,\n calls: state.calls,\n retainedStateSize: state.buckets.size,\n ...(p95Ms === undefined ? {} : { p95Ms }),\n };\n })\n .sort((left, right) => (right.p95Ms ?? -1) - (left.p95Ms ?? -1));\n\n return {\n events: this.events,\n errors: this.errors,\n toolCalls: this.toolCalls,\n failedToolCalls: this.failedToolCalls,\n tokens: { ...this.tokens },\n cost: this.cost,\n toolLatency,\n eventVolume,\n packageEvents,\n sanitizedFailures: this.sanitizedFailures,\n operationDuration,\n recentErrors: this.errorLog.map((entry) => ({ ...entry })),\n };\n }\n\n private recordOperationDuration(recordName: string, attributes: Record<string, unknown>): void {\n const duration = Object.entries(attributes).find(\n ([key, value]) =>\n key !== 'tool.duration_ms' &&\n (key === 'duration_ms' || key.endsWith('.duration_ms')) &&\n readNumber(value) !== undefined,\n );\n if (!duration) return;\n const operationName = safeMetricName(readString(attributes['operation.name']) ?? recordName);\n const state = this.operations.get(operationName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.operations.set(operationName, state);\n state.calls += 1;\n const durationMs = readNumber(duration[1]);\n if (durationMs === undefined) return;\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n private recordSanitizedFailure(recordName: string, attributes: Record<string, unknown>): void {\n const outcome = readString(attributes.outcome);\n const failed =\n attributes['tool.result.error'] === true ||\n recordName === API_ERROR_RECORD ||\n outcome === 'error' ||\n outcome === 'failed' ||\n outcome === 'failure';\n if (!failed) return;\n this.sanitizedFailures += 1;\n if (recordName === TOOL_RESULT_RECORD || recordName === API_ERROR_RECORD) return;\n const code = readString(attributes['error.code']) ?? outcome ?? 'error';\n this.pushError(recordName, `${readString(attributes['error.type']) ?? 'operation'} failed`, safeMetricName(code));\n }\n\n private recordToolResult(recordName: string, attributes: Record<string, unknown>): void {\n const toolName = readString(attributes['tool.name']) ?? UNKNOWN_TOOL;\n const state = this.tools.get(toolName) ?? { calls: 0, samples: 0, buckets: new Map<number, number>() };\n this.tools.set(toolName, state);\n state.calls += 1;\n this.toolCalls += 1;\n\n // A call whose start was never seen carries no duration, so it counts as a\n // call but contributes no latency sample.\n const durationMs = readNumber(attributes['tool.duration_ms']);\n if (durationMs !== undefined) {\n const bucket = latencyBucket(durationMs);\n state.buckets.set(bucket, (state.buckets.get(bucket) ?? 0) + 1);\n state.samples += 1;\n }\n\n if (attributes['tool.result.error'] === true) {\n this.failedToolCalls += 1;\n // Redaction is on by default, so the tool's own output is unavailable and\n // the tool name is the only detail this entry can carry.\n this.pushError(recordName, `${toolName}: tool call failed`, TOOL_ERROR_CODE);\n }\n }\n\n private recordApiError(recordName: string, attributes: Record<string, unknown>): void {\n this.errors += 1;\n const status = String(attributes['http.response.status_code']);\n this.pushError(recordName, `provider responded ${status}`, status);\n }\n\n private recordTurnUsage(attributes: Record<string, unknown>): void {\n this.tokens.input += readNumber(attributes['gen_ai.usage.input_tokens']) ?? 0;\n this.tokens.output += readNumber(attributes['gen_ai.usage.output_tokens']) ?? 0;\n this.tokens.total += readNumber(attributes['gen_ai.usage.total_tokens']) ?? 0;\n this.cost += readNumber(attributes['gen_ai.usage.cost']) ?? 0;\n }\n\n private pushError(recordName: string, message: string, code: string): void {\n this.errorLog.unshift({ at: this.now(), event: errorEventName(recordName), message, code });\n if (this.errorLog.length > this.maxRecentErrors) this.errorLog.length = this.maxRecentErrors;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/metrics.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Session-local aggregation of the log records the Pi telemetry extension
|
|
4
|
+
* already emits.
|
|
5
|
+
*
|
|
6
|
+
* The aggregator is fed `(recordName, attributes)` pairs rather than Pi events
|
|
7
|
+
* so it can only ever see what the sink sees: no extra instrumentation is added
|
|
8
|
+
* for metrics, and nothing here depends on a live sink. Everything is scoped to
|
|
9
|
+
* the current process, so there is no persistence and no query API.
|
|
10
|
+
*/
|
|
11
|
+
/** Record names the aggregator derives numbers from. */
|
|
12
|
+
declare const TOOL_RESULT_RECORD = "pi.tool_result";
|
|
13
|
+
declare const API_ERROR_RECORD = "pi.api_error";
|
|
14
|
+
declare const TURN_FINISHED_RECORD = "pi.turn.finished";
|
|
15
|
+
/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
|
|
16
|
+
declare const MAX_RETAINED_STATE_PER_TOOL = 160;
|
|
17
|
+
interface LogMetricsTokenTotals {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
interface LogMetricsToolLatency {
|
|
23
|
+
name: string;
|
|
24
|
+
calls: number;
|
|
25
|
+
p95Ms?: number;
|
|
26
|
+
/** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
|
|
27
|
+
retainedStateSize: number;
|
|
28
|
+
}
|
|
29
|
+
interface LogMetricsEventCount {
|
|
30
|
+
name: string;
|
|
31
|
+
count: number;
|
|
32
|
+
}
|
|
33
|
+
interface LogMetricsOperationDuration {
|
|
34
|
+
name: string;
|
|
35
|
+
calls: number;
|
|
36
|
+
p95Ms?: number;
|
|
37
|
+
retainedStateSize: number;
|
|
38
|
+
}
|
|
39
|
+
interface LogMetricsError {
|
|
40
|
+
at: number;
|
|
41
|
+
event: string;
|
|
42
|
+
message: string;
|
|
43
|
+
code: string;
|
|
44
|
+
}
|
|
45
|
+
interface LogMetricsSnapshot {
|
|
46
|
+
events: number;
|
|
47
|
+
errors: number;
|
|
48
|
+
toolCalls: number;
|
|
49
|
+
failedToolCalls: number;
|
|
50
|
+
tokens: LogMetricsTokenTotals;
|
|
51
|
+
cost: number;
|
|
52
|
+
toolLatency: LogMetricsToolLatency[];
|
|
53
|
+
eventVolume: LogMetricsEventCount[];
|
|
54
|
+
packageEvents: LogMetricsEventCount[];
|
|
55
|
+
sanitizedFailures: number;
|
|
56
|
+
operationDuration: LogMetricsOperationDuration[];
|
|
57
|
+
recentErrors: LogMetricsError[];
|
|
58
|
+
}
|
|
59
|
+
interface LogMetricsAggregatorOptions {
|
|
60
|
+
now?: () => number;
|
|
61
|
+
maxRecentErrors?: number;
|
|
62
|
+
}
|
|
63
|
+
/** The slice of the aggregator the telemetry extension needs. */
|
|
64
|
+
interface LogMetricsRecorder {
|
|
65
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
66
|
+
}
|
|
67
|
+
declare class LogMetricsAggregator implements LogMetricsRecorder {
|
|
68
|
+
private events;
|
|
69
|
+
private errors;
|
|
70
|
+
private toolCalls;
|
|
71
|
+
private failedToolCalls;
|
|
72
|
+
private cost;
|
|
73
|
+
private readonly tokens;
|
|
74
|
+
private readonly eventCounts;
|
|
75
|
+
private readonly packageCounts;
|
|
76
|
+
private readonly tools;
|
|
77
|
+
private readonly operations;
|
|
78
|
+
private sanitizedFailures;
|
|
79
|
+
private readonly errorLog;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly maxRecentErrors;
|
|
82
|
+
constructor(options?: LogMetricsAggregatorOptions);
|
|
83
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
84
|
+
snapshot(): LogMetricsSnapshot;
|
|
85
|
+
private recordOperationDuration;
|
|
86
|
+
private recordSanitizedFailure;
|
|
87
|
+
private recordToolResult;
|
|
88
|
+
private recordApiError;
|
|
89
|
+
private recordTurnUsage;
|
|
90
|
+
private pushError;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
|
|
94
|
+
//# sourceMappingURL=metrics.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.cts","names":[],"sources":["../src/metrics.ts"],"mappings":";;;;;;;;;;;cAWa;cACA;cACA;;cAmBA;UAEI;EACf;EACA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,aAAa;EACb,aAAa;EACb,eAAe;EACf;EACA,mBAAmB;EACnB,cAAc;;UAGC;EACf;EACA;;;UAIe;EACf,OAAO,cAAc,YAAY;;cAwDtB,gCAAgC;UACnC;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;UACT;mBACS;mBACA;mBACA;EAEL,YAAA,UAAS;EAKrB,OAAO,cAAc,YAAY;EAiBjC,YAAY;UA+CJ;UAmBA;UAeA;UAwBA;UAMA;UAOA"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/metrics.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Session-local aggregation of the log records the Pi telemetry extension
|
|
4
|
+
* already emits.
|
|
5
|
+
*
|
|
6
|
+
* The aggregator is fed `(recordName, attributes)` pairs rather than Pi events
|
|
7
|
+
* so it can only ever see what the sink sees: no extra instrumentation is added
|
|
8
|
+
* for metrics, and nothing here depends on a live sink. Everything is scoped to
|
|
9
|
+
* the current process, so there is no persistence and no query API.
|
|
10
|
+
*/
|
|
11
|
+
/** Record names the aggregator derives numbers from. */
|
|
12
|
+
declare const TOOL_RESULT_RECORD = "pi.tool_result";
|
|
13
|
+
declare const API_ERROR_RECORD = "pi.api_error";
|
|
14
|
+
declare const TURN_FINISHED_RECORD = "pi.turn.finished";
|
|
15
|
+
/** Bucket ceiling per tool: covers 1ms to roughly an hour at 10% resolution. */
|
|
16
|
+
declare const MAX_RETAINED_STATE_PER_TOOL = 160;
|
|
17
|
+
interface LogMetricsTokenTotals {
|
|
18
|
+
input: number;
|
|
19
|
+
output: number;
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
interface LogMetricsToolLatency {
|
|
23
|
+
name: string;
|
|
24
|
+
calls: number;
|
|
25
|
+
p95Ms?: number;
|
|
26
|
+
/** Buckets currently held for this tool, bounded by MAX_RETAINED_STATE_PER_TOOL. */
|
|
27
|
+
retainedStateSize: number;
|
|
28
|
+
}
|
|
29
|
+
interface LogMetricsEventCount {
|
|
30
|
+
name: string;
|
|
31
|
+
count: number;
|
|
32
|
+
}
|
|
33
|
+
interface LogMetricsOperationDuration {
|
|
34
|
+
name: string;
|
|
35
|
+
calls: number;
|
|
36
|
+
p95Ms?: number;
|
|
37
|
+
retainedStateSize: number;
|
|
38
|
+
}
|
|
39
|
+
interface LogMetricsError {
|
|
40
|
+
at: number;
|
|
41
|
+
event: string;
|
|
42
|
+
message: string;
|
|
43
|
+
code: string;
|
|
44
|
+
}
|
|
45
|
+
interface LogMetricsSnapshot {
|
|
46
|
+
events: number;
|
|
47
|
+
errors: number;
|
|
48
|
+
toolCalls: number;
|
|
49
|
+
failedToolCalls: number;
|
|
50
|
+
tokens: LogMetricsTokenTotals;
|
|
51
|
+
cost: number;
|
|
52
|
+
toolLatency: LogMetricsToolLatency[];
|
|
53
|
+
eventVolume: LogMetricsEventCount[];
|
|
54
|
+
packageEvents: LogMetricsEventCount[];
|
|
55
|
+
sanitizedFailures: number;
|
|
56
|
+
operationDuration: LogMetricsOperationDuration[];
|
|
57
|
+
recentErrors: LogMetricsError[];
|
|
58
|
+
}
|
|
59
|
+
interface LogMetricsAggregatorOptions {
|
|
60
|
+
now?: () => number;
|
|
61
|
+
maxRecentErrors?: number;
|
|
62
|
+
}
|
|
63
|
+
/** The slice of the aggregator the telemetry extension needs. */
|
|
64
|
+
interface LogMetricsRecorder {
|
|
65
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
66
|
+
}
|
|
67
|
+
declare class LogMetricsAggregator implements LogMetricsRecorder {
|
|
68
|
+
private events;
|
|
69
|
+
private errors;
|
|
70
|
+
private toolCalls;
|
|
71
|
+
private failedToolCalls;
|
|
72
|
+
private cost;
|
|
73
|
+
private readonly tokens;
|
|
74
|
+
private readonly eventCounts;
|
|
75
|
+
private readonly packageCounts;
|
|
76
|
+
private readonly tools;
|
|
77
|
+
private readonly operations;
|
|
78
|
+
private sanitizedFailures;
|
|
79
|
+
private readonly errorLog;
|
|
80
|
+
private readonly now;
|
|
81
|
+
private readonly maxRecentErrors;
|
|
82
|
+
constructor(options?: LogMetricsAggregatorOptions);
|
|
83
|
+
record(name: string, attributes: Record<string, unknown>): void;
|
|
84
|
+
snapshot(): LogMetricsSnapshot;
|
|
85
|
+
private recordOperationDuration;
|
|
86
|
+
private recordSanitizedFailure;
|
|
87
|
+
private recordToolResult;
|
|
88
|
+
private recordApiError;
|
|
89
|
+
private recordTurnUsage;
|
|
90
|
+
private pushError;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
export { API_ERROR_RECORD, LogMetricsAggregator, LogMetricsAggregatorOptions, LogMetricsError, LogMetricsEventCount, LogMetricsOperationDuration, LogMetricsRecorder, LogMetricsSnapshot, LogMetricsTokenTotals, LogMetricsToolLatency, MAX_RETAINED_STATE_PER_TOOL, TOOL_RESULT_RECORD, TURN_FINISHED_RECORD };
|
|
94
|
+
//# sourceMappingURL=metrics.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.mts","names":[],"sources":["../src/metrics.ts"],"mappings":";;;;;;;;;;;cAWa;cACA;cACA;;cAmBA;UAEI;EACf;EACA;EACA;;UAGe;EACf;EACA;EACA;;EAEA;;UAGe;EACf;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,aAAa;EACb,aAAa;EACb,eAAe;EACf;EACA,mBAAmB;EACnB,cAAc;;UAGC;EACf;EACA;;;UAIe;EACf,OAAO,cAAc,YAAY;;cAwDtB,gCAAgC;UACnC;UACA;UACA;UACA;UACA;mBACS;mBACA;mBACA;mBACA;mBACA;UACT;mBACS;mBACA;mBACA;EAEL,YAAA,UAAS;EAKrB,OAAO,cAAc,YAAY;EAiBjC,YAAY;UA+CJ;UAmBA;UAeA;UAwBA;UAMA;UAOA"}
|
package/dist/metrics.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=`pi.tool_result`,t=`pi.api_error`,n=`pi.turn.finished`,r=1.1,i=Math.log(r),a=160;function o(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function s(e){return typeof e==`string`&&e.length>0?e:void 0}function c(e){return e.replace(/[^a-zA-Z0-9@/._:-]/g,`_`).slice(0,128)||`unknown`}function l(e){let t=Math.ceil(Math.log(Math.max(e,1))/i);return Math.min(Math.max(t,0),159)}function u(e){return r**e}function d(e){if(e.samples===0)return;let t=e.samples*.95,n=[...e.buckets.keys()].sort((e,t)=>e-t),r=0,i=n[0];for(let a of n)if(r+=e.buckets.get(a)??0,i=a,r>=t)break;return Math.round(u(i))}function f(e){return e.startsWith(`pi.`)?e.slice(3):e}var p=class{events=0;errors=0;toolCalls=0;failedToolCalls=0;cost=0;tokens={input:0,output:0,total:0};eventCounts=new Map;packageCounts=new Map;tools=new Map;operations=new Map;sanitizedFailures=0;errorLog=[];now;maxRecentErrors;constructor(e={}){this.now=e.now??Date.now,this.maxRecentErrors=e.maxRecentErrors??8}record(e,t){let n=c(e);this.events+=1,this.eventCounts.set(n,(this.eventCounts.get(n)??0)+1);let r=s(t[`telemetry.package`]);if(r&&!r.startsWith(`/`)&&!r.includes(`\\`)){let e=c(r);this.packageCounts.set(e,(this.packageCounts.get(e)??0)+1)}this.recordOperationDuration(n,t),this.recordSanitizedFailure(n,t),n===`pi.tool_result`?this.recordToolResult(n,t):n===`pi.api_error`?this.recordApiError(n,t):n===`pi.turn.finished`&&this.recordTurnUsage(t)}snapshot(){let e=[...this.tools.entries()].map(([e,t])=>{let n=d(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1)),t=[...this.eventCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),n=[...this.packageCounts.entries()].map(([e,t])=>({name:e,count:t})).sort((e,t)=>t.count-e.count),r=[...this.operations.entries()].map(([e,t])=>{let n=d(t);return{name:e,calls:t.calls,retainedStateSize:t.buckets.size,...n===void 0?{}:{p95Ms:n}}}).sort((e,t)=>(t.p95Ms??-1)-(e.p95Ms??-1));return{events:this.events,errors:this.errors,toolCalls:this.toolCalls,failedToolCalls:this.failedToolCalls,tokens:{...this.tokens},cost:this.cost,toolLatency:e,eventVolume:t,packageEvents:n,sanitizedFailures:this.sanitizedFailures,operationDuration:r,recentErrors:this.errorLog.map(e=>({...e}))}}recordOperationDuration(e,t){let n=Object.entries(t).find(([e,t])=>e!==`tool.duration_ms`&&(e===`duration_ms`||e.endsWith(`.duration_ms`))&&o(t)!==void 0);if(!n)return;let r=c(s(t[`operation.name`])??e),i=this.operations.get(r)??{calls:0,samples:0,buckets:new Map};this.operations.set(r,i),i.calls+=1;let a=o(n[1]);if(a===void 0)return;let u=l(a);i.buckets.set(u,(i.buckets.get(u)??0)+1),i.samples+=1}recordSanitizedFailure(e,t){let n=s(t.outcome);if(t[`tool.result.error`]!==!0&&e!==`pi.api_error`&&n!==`error`&&n!==`failed`&&n!==`failure`||(this.sanitizedFailures+=1,e===`pi.tool_result`||e===`pi.api_error`))return;let r=s(t[`error.code`])??n??`error`;this.pushError(e,`${s(t[`error.type`])??`operation`} failed`,c(r))}recordToolResult(e,t){let n=s(t[`tool.name`])??`unknown`,r=this.tools.get(n)??{calls:0,samples:0,buckets:new Map};this.tools.set(n,r),r.calls+=1,this.toolCalls+=1;let i=o(t[`tool.duration_ms`]);if(i!==void 0){let e=l(i);r.buckets.set(e,(r.buckets.get(e)??0)+1),r.samples+=1}t[`tool.result.error`]===!0&&(this.failedToolCalls+=1,this.pushError(e,`${n}: tool call failed`,`tool`))}recordApiError(e,t){this.errors+=1;let n=String(t[`http.response.status_code`]);this.pushError(e,`provider responded ${n}`,n)}recordTurnUsage(e){this.tokens.input+=o(e[`gen_ai.usage.input_tokens`])??0,this.tokens.output+=o(e[`gen_ai.usage.output_tokens`])??0,this.tokens.total+=o(e[`gen_ai.usage.total_tokens`])??0,this.cost+=o(e[`gen_ai.usage.cost`])??0}pushError(e,t,n){this.errorLog.unshift({at:this.now(),event:f(e),message:t,code:n}),this.errorLog.length>this.maxRecentErrors&&(this.errorLog.length=this.maxRecentErrors)}};export{t as API_ERROR_RECORD,p as LogMetricsAggregator,a as MAX_RETAINED_STATE_PER_TOOL,e as TOOL_RESULT_RECORD,n as TURN_FINISHED_RECORD};
|
|
2
|
+
//# sourceMappingURL=metrics.mjs.map
|