@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.2
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/CHANGELOG.md +92 -20
- package/dist/cli.js +3485 -3448
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +32 -7
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/mcp/transports/stdio.d.ts +13 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/sdk.d.ts +13 -2
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +14 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +42 -6
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +1 -1
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/transports/stdio.ts +103 -23
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +139 -54
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/sdk.ts +221 -42
- package/src/session/agent-session.ts +1285 -348
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +48 -0
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/task/executor.ts +1 -1
- package/src/task/label.ts +2 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/gh.ts +169 -2
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +7 -5
- package/src/tools/read.ts +48 -3
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/utils/title-generator.ts +15 -4
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
package/src/telemetry-export.ts
CHANGED
|
@@ -1,144 +1,500 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OTLP
|
|
2
|
+
* OTLP telemetry export bootstrap.
|
|
3
3
|
*
|
|
4
4
|
* oh-my-pi's agent core (`@oh-my-pi/pi-agent-core`) emits OpenTelemetry GenAI
|
|
5
|
-
* spans through the global `@opentelemetry/api` tracer,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* spans through the global `@opentelemetry/api` tracer, and exposes run-level
|
|
6
|
+
* callbacks for metrics/log pipelines. This module registers the OTLP/proto
|
|
7
|
+
* trace, log, and metric SDK providers when the standard `OTEL_*` endpoint env
|
|
8
|
+
* vars are set so `omp` can be observed by any OTLP collector without vendor
|
|
9
|
+
* coupling.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* provider-agnostic and ships no vendor coupling. Only the `http/protobuf`
|
|
17
|
-
* transport is supported — an `OTEL_EXPORTER_OTLP*_PROTOCOL` of `grpc` or
|
|
18
|
-
* `http/json` declines rather than misrouting spans.
|
|
19
|
-
*
|
|
20
|
-
* The OTLP/proto exporter on the 2.x line is used deliberately: the 1.x line
|
|
21
|
-
* deadlocks under Bun — its `req.on('close')` handler fires a spurious failure
|
|
22
|
-
* after the success path. `exporter-trace-otlp-proto@0.218` paired with
|
|
23
|
-
* `sdk-trace-base@2.7` exports cleanly on Bun.
|
|
11
|
+
* Only the `http/protobuf` transport is supported — an
|
|
12
|
+
* `OTEL_EXPORTER_OTLP*_PROTOCOL` of `grpc` or `http/json` declines rather than
|
|
13
|
+
* misrouting protobuf payloads. The exporter line is pinned to the 0.218/2.7
|
|
14
|
+
* family validated under Bun; the 1.x OTLP line deadlocks when its
|
|
15
|
+
* `req.on("close")` handler fires after a successful export.
|
|
24
16
|
*/
|
|
17
|
+
import type {
|
|
18
|
+
AgentRunCoverage,
|
|
19
|
+
AgentRunSummary,
|
|
20
|
+
AgentTelemetryConfig,
|
|
21
|
+
AgentTelemetryWarning,
|
|
22
|
+
ChatUsageEvent,
|
|
23
|
+
ToolStatus,
|
|
24
|
+
} from "@oh-my-pi/pi-agent-core";
|
|
25
25
|
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
26
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
type Attributes,
|
|
28
|
+
type AttributeValue,
|
|
29
|
+
type Counter,
|
|
30
|
+
context,
|
|
31
|
+
type Histogram,
|
|
32
|
+
type Meter,
|
|
33
|
+
metrics,
|
|
34
|
+
} from "@opentelemetry/api";
|
|
35
|
+
import { type LogAttributes, logs, type Logger as OtelLogger, SeverityNumber } from "@opentelemetry/api-logs";
|
|
36
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
37
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
38
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
39
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
40
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
41
|
+
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
42
|
+
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
43
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
44
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
27
45
|
|
|
28
46
|
/**
|
|
29
47
|
* Periodic flush interval. A long-lived `omp` process (the ACP server is
|
|
30
48
|
* spawned once and reused across many turns) would otherwise hold finished
|
|
31
|
-
*
|
|
49
|
+
* telemetry until a batch window elapses or the process exits.
|
|
32
50
|
*/
|
|
33
51
|
const FLUSH_INTERVAL_MS = 30_000;
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
const SERVICE_NAME = "oh-my-pi";
|
|
54
|
+
|
|
55
|
+
type TelemetrySignal = "trace" | "log" | "metric";
|
|
56
|
+
type OtelLogLevel = "none" | logger.LogLevel;
|
|
57
|
+
|
|
58
|
+
interface SignalConfig {
|
|
59
|
+
readonly trace: boolean;
|
|
60
|
+
readonly log: boolean;
|
|
61
|
+
readonly metric: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const LOG_SEVERITY: Record<logger.LogLevel, SeverityNumber> = {
|
|
65
|
+
error: SeverityNumber.ERROR,
|
|
66
|
+
warn: SeverityNumber.WARN,
|
|
67
|
+
info: SeverityNumber.INFO,
|
|
68
|
+
debug: SeverityNumber.DEBUG,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const LOG_LEVEL_WEIGHT: Record<logger.LogLevel, number> = {
|
|
72
|
+
error: 0,
|
|
73
|
+
warn: 1,
|
|
74
|
+
info: 2,
|
|
75
|
+
debug: 3,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const TOOL_STATUSES = ["ok", "error", "skipped", "blocked", "timeout", "aborted"] satisfies readonly ToolStatus[];
|
|
79
|
+
|
|
80
|
+
let traceProvider: NodeTracerProvider | undefined;
|
|
81
|
+
let logProvider: LoggerProvider | undefined;
|
|
82
|
+
let meterProvider: MeterProvider | undefined;
|
|
83
|
+
let metricRecorder: AgentMetricRecorder | undefined;
|
|
84
|
+
let otelLogger: OtelLogger | undefined;
|
|
85
|
+
let unregisterLogSink: (() => void) | undefined;
|
|
36
86
|
let initPromise: Promise<void> | undefined;
|
|
37
87
|
|
|
38
88
|
/**
|
|
39
|
-
* Whether {@link initTelemetryExport} registered
|
|
40
|
-
* this to decide whether to switch on the agent loop's telemetry
|
|
41
|
-
*
|
|
89
|
+
* Whether {@link initTelemetryExport} registered any real OTLP signal provider.
|
|
90
|
+
* The CLI uses this to decide whether to switch on the agent loop's telemetry
|
|
91
|
+
* hooks; metrics and structured logs need those callbacks even when traces are
|
|
92
|
+
* disabled.
|
|
42
93
|
*/
|
|
43
94
|
export function isTelemetryExportEnabled(): boolean {
|
|
44
|
-
|
|
95
|
+
if (traceProvider) return true;
|
|
96
|
+
if (logProvider) return true;
|
|
97
|
+
if (meterProvider) return true;
|
|
98
|
+
return false;
|
|
45
99
|
}
|
|
46
100
|
|
|
47
101
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
102
|
+
* Merge OTLP metrics/log hooks into an existing agent telemetry config.
|
|
103
|
+
*
|
|
104
|
+
* The caller still owns content-capture policy, cost estimation, and custom
|
|
105
|
+
* attributes. This only appends host-level metrics/log forwarding for the
|
|
106
|
+
* providers registered by {@link initTelemetryExport}.
|
|
107
|
+
*/
|
|
108
|
+
export function createTelemetryExportConfig(
|
|
109
|
+
config: AgentTelemetryConfig | undefined,
|
|
110
|
+
): AgentTelemetryConfig | undefined {
|
|
111
|
+
if (!isTelemetryExportEnabled()) return config;
|
|
112
|
+
return {
|
|
113
|
+
...config,
|
|
114
|
+
onChatUsage: async event => {
|
|
115
|
+
await config?.onChatUsage?.(event);
|
|
116
|
+
metricRecorder?.recordChatUsage(event);
|
|
117
|
+
},
|
|
118
|
+
onRunEnd: (summary, coverage) => {
|
|
119
|
+
config?.onRunEnd?.(summary, coverage);
|
|
120
|
+
metricRecorder?.recordRun(summary, coverage);
|
|
121
|
+
emitRunSummaryLog(summary, coverage);
|
|
122
|
+
},
|
|
123
|
+
onTelemetryWarning: warning => {
|
|
124
|
+
config?.onTelemetryWarning?.(warning);
|
|
125
|
+
emitTelemetryWarningLog(warning);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Register global trace/log/meter providers when OTLP endpoints are configured
|
|
132
|
+
* through env. Idempotent, and a no-op when no signal has an endpoint (or when
|
|
133
|
+
* the OTEL kill-switches are engaged), so startup can call it unconditionally.
|
|
52
134
|
*/
|
|
53
135
|
export async function initTelemetryExport(): Promise<void> {
|
|
54
|
-
if (
|
|
136
|
+
if (isTelemetryExportEnabled()) return;
|
|
55
137
|
if (initPromise) return initPromise;
|
|
56
138
|
|
|
57
|
-
// The OTEL env contract parses booleans and enum lists case-insensitively, so
|
|
58
|
-
// OTEL_SDK_DISABLED=TRUE and OTEL_TRACES_EXPORTER=None must also disable export.
|
|
59
139
|
if (process.env.OTEL_SDK_DISABLED?.trim().toLowerCase() === "true") return;
|
|
60
|
-
if (tracesExporterDisabled(process.env.OTEL_TRACES_EXPORTER)) return;
|
|
61
|
-
|
|
62
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
63
|
-
if (!endpoint) return;
|
|
64
|
-
|
|
65
|
-
// We only ship the http/protobuf transport (the line validated on Bun). The
|
|
66
|
-
// OTEL contract lets OTEL_EXPORTER_OTLP*_PROTOCOL select grpc / http/json;
|
|
67
|
-
// rather than silently send protobuf-over-HTTP to a grpc :4317 port and lose
|
|
68
|
-
// every span, decline when an unsupported protocol is requested.
|
|
69
|
-
const protocol = (process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL)
|
|
70
|
-
?.trim()
|
|
71
|
-
.toLowerCase();
|
|
72
|
-
if (protocol && protocol !== "http/protobuf") {
|
|
73
|
-
logger.warn(
|
|
74
|
-
`OTEL trace export disabled: OTEL_EXPORTER_OTLP_PROTOCOL=${protocol} is unsupported (only http/protobuf)`,
|
|
75
|
-
);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
140
|
|
|
79
|
-
|
|
141
|
+
const signalConfig = resolveSignalConfig();
|
|
142
|
+
if (!signalConfig.trace && !signalConfig.log && !signalConfig.metric) return;
|
|
143
|
+
|
|
144
|
+
initPromise = registerProviders(signalConfig);
|
|
80
145
|
return initPromise;
|
|
81
146
|
}
|
|
82
147
|
|
|
83
|
-
async function
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
{ OTLPTraceExporter },
|
|
87
|
-
{ resourceFromAttributes },
|
|
88
|
-
{ BatchSpanProcessor },
|
|
89
|
-
{ NodeTracerProvider },
|
|
90
|
-
] = await Promise.all([
|
|
91
|
-
import("@opentelemetry/context-async-hooks"),
|
|
92
|
-
import("@opentelemetry/exporter-trace-otlp-proto"),
|
|
93
|
-
import("@opentelemetry/resources"),
|
|
94
|
-
import("@opentelemetry/sdk-trace-base"),
|
|
95
|
-
import("@opentelemetry/sdk-trace-node"),
|
|
96
|
-
]);
|
|
97
|
-
|
|
98
|
-
// The exporter reads endpoint/headers/timeout from OTEL_EXPORTER_OTLP_* itself,
|
|
99
|
-
// so there is nothing to thread through here.
|
|
100
|
-
const exporter = new OTLPTraceExporter();
|
|
101
|
-
const tracerProvider = new NodeTracerProvider({
|
|
102
|
-
resource: resourceFromAttributes({
|
|
103
|
-
"service.name": process.env.OTEL_SERVICE_NAME ?? "oh-my-pi",
|
|
104
|
-
}),
|
|
105
|
-
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
148
|
+
async function registerProviders(signalConfig: SignalConfig): Promise<void> {
|
|
149
|
+
const resource = resourceFromAttributes({
|
|
150
|
+
"service.name": process.env.OTEL_SERVICE_NAME ?? SERVICE_NAME,
|
|
106
151
|
});
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
|
|
153
|
+
if (signalConfig.trace) {
|
|
154
|
+
const exporter = new OTLPTraceExporter();
|
|
155
|
+
traceProvider = new NodeTracerProvider({
|
|
156
|
+
resource,
|
|
157
|
+
spanProcessors: [new BatchSpanProcessor(exporter)],
|
|
158
|
+
});
|
|
159
|
+
traceProvider.register({ contextManager: new AsyncLocalStorageContextManager().enable() });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (signalConfig.metric) {
|
|
163
|
+
const exporter = new OTLPMetricExporter();
|
|
164
|
+
meterProvider = new MeterProvider({
|
|
165
|
+
resource,
|
|
166
|
+
readers: [new PeriodicExportingMetricReader({ exporter })],
|
|
167
|
+
});
|
|
168
|
+
metrics.setGlobalMeterProvider(meterProvider);
|
|
169
|
+
metricRecorder = new AgentMetricRecorder(metrics.getMeter("@oh-my-pi/pi-coding-agent"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (signalConfig.log) {
|
|
173
|
+
const exporter = new OTLPLogExporter();
|
|
174
|
+
logProvider = new LoggerProvider({
|
|
175
|
+
resource,
|
|
176
|
+
processors: [new BatchLogRecordProcessor({ exporter })],
|
|
177
|
+
});
|
|
178
|
+
logs.setGlobalLoggerProvider(logProvider);
|
|
179
|
+
otelLogger = logProvider.getLogger("@oh-my-pi/pi-coding-agent");
|
|
180
|
+
unregisterLogSink = logger.registerLogSink(event => {
|
|
181
|
+
emitOtelLog(
|
|
182
|
+
event.level,
|
|
183
|
+
event.message,
|
|
184
|
+
logAttributesFromContext(event.context),
|
|
185
|
+
"pi.omp.log",
|
|
186
|
+
event.timestamp,
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
112
190
|
|
|
113
191
|
const flushTimer = setInterval(() => {
|
|
114
|
-
|
|
192
|
+
flushTelemetryExport().catch(() => {});
|
|
115
193
|
}, FLUSH_INTERVAL_MS);
|
|
116
194
|
flushTimer.unref();
|
|
117
195
|
|
|
118
|
-
|
|
119
|
-
// owns SIGINT/SIGTERM/SIGHUP/exit and quit(), and awaits registered cleanups
|
|
120
|
-
// before calling process.exit — so the batch processor's final OTLP export
|
|
121
|
-
// completes instead of being cut off mid-flight on the shutdown path.
|
|
122
|
-
postmortem.register("otel-trace-export", async () => {
|
|
196
|
+
postmortem.register("otel-export", async () => {
|
|
123
197
|
clearInterval(flushTimer);
|
|
124
|
-
|
|
198
|
+
unregisterLogSink?.();
|
|
199
|
+
unregisterLogSink = undefined;
|
|
200
|
+
const shutdowns: Promise<void>[] = [];
|
|
201
|
+
if (traceProvider) shutdowns.push(traceProvider.shutdown());
|
|
202
|
+
if (logProvider) shutdowns.push(logProvider.shutdown());
|
|
203
|
+
if (meterProvider) shutdowns.push(meterProvider.shutdown());
|
|
204
|
+
await Promise.all(shutdowns);
|
|
125
205
|
});
|
|
126
206
|
}
|
|
127
207
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
208
|
+
function resolveSignalConfig(): SignalConfig {
|
|
209
|
+
const signalConfig: SignalConfig = {
|
|
210
|
+
trace: signalEnabled(
|
|
211
|
+
"trace",
|
|
212
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
213
|
+
process.env.OTEL_TRACES_EXPORTER,
|
|
214
|
+
process.env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
|
|
215
|
+
),
|
|
216
|
+
log: signalEnabled(
|
|
217
|
+
"log",
|
|
218
|
+
process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
219
|
+
process.env.OTEL_LOGS_EXPORTER,
|
|
220
|
+
process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
|
|
221
|
+
),
|
|
222
|
+
metric: signalEnabled(
|
|
223
|
+
"metric",
|
|
224
|
+
process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
225
|
+
process.env.OTEL_METRICS_EXPORTER,
|
|
226
|
+
process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL ?? process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
return signalConfig;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function signalEnabled(
|
|
233
|
+
signal: TelemetrySignal,
|
|
234
|
+
endpoint: string | undefined,
|
|
235
|
+
exporterSelection: string | undefined,
|
|
236
|
+
protocolSelection: string | undefined,
|
|
237
|
+
): boolean {
|
|
238
|
+
if (exporterSelection) {
|
|
239
|
+
for (const entry of exporterSelection.split(",")) {
|
|
240
|
+
if (entry.trim().toLowerCase() === "none") return false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!endpoint) return false;
|
|
244
|
+
|
|
245
|
+
const protocol = protocolSelection?.trim().toLowerCase();
|
|
246
|
+
if (protocol && protocol !== "http/protobuf") {
|
|
247
|
+
logger.warn(`OTEL ${signal} export disabled: OTEL_EXPORTER_OTLP_PROTOCOL=${protocol} is unsupported`, {
|
|
248
|
+
supported: "http/protobuf",
|
|
249
|
+
});
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class AgentMetricRecorder {
|
|
256
|
+
readonly #tokenUsage: Histogram<Attributes>;
|
|
257
|
+
readonly #chatCostUsd: Counter<Attributes>;
|
|
258
|
+
readonly #runs: Counter<Attributes>;
|
|
259
|
+
readonly #steps: Counter<Attributes>;
|
|
260
|
+
readonly #chatCalls: Counter<Attributes>;
|
|
261
|
+
readonly #chatDurationMs: Histogram<Attributes>;
|
|
262
|
+
readonly #toolCalls: Counter<Attributes>;
|
|
263
|
+
readonly #toolDurationMs: Histogram<Attributes>;
|
|
264
|
+
readonly #errors: Counter<Attributes>;
|
|
265
|
+
|
|
266
|
+
constructor(meter: Meter) {
|
|
267
|
+
this.#tokenUsage = meter.createHistogram("gen_ai.client.token.usage", {
|
|
268
|
+
description: "Token usage reported by GenAI chat calls.",
|
|
269
|
+
unit: "{token}",
|
|
270
|
+
});
|
|
271
|
+
this.#chatCostUsd = meter.createCounter("pi.omp.agent.chat.cost.estimated_usd", {
|
|
272
|
+
description: "Estimated USD cost for completed chat calls.",
|
|
273
|
+
unit: "USD",
|
|
274
|
+
});
|
|
275
|
+
this.#runs = meter.createCounter("pi.omp.agent.runs", {
|
|
276
|
+
description: "Completed agent runs.",
|
|
277
|
+
unit: "{run}",
|
|
278
|
+
});
|
|
279
|
+
this.#steps = meter.createCounter("pi.omp.agent.steps", {
|
|
280
|
+
description: "Agent loop steps completed inside a run.",
|
|
281
|
+
unit: "{step}",
|
|
282
|
+
});
|
|
283
|
+
this.#chatCalls = meter.createCounter("pi.omp.agent.chat.calls", {
|
|
284
|
+
description: "Chat calls completed inside agent runs.",
|
|
285
|
+
unit: "{call}",
|
|
286
|
+
});
|
|
287
|
+
this.#chatDurationMs = meter.createHistogram("pi.omp.agent.chat.duration", {
|
|
288
|
+
description: "Total chat latency observed in an agent run.",
|
|
289
|
+
unit: "ms",
|
|
290
|
+
});
|
|
291
|
+
this.#toolCalls = meter.createCounter("pi.omp.agent.tool.calls", {
|
|
292
|
+
description: "Tool calls completed inside agent runs.",
|
|
293
|
+
unit: "{call}",
|
|
294
|
+
});
|
|
295
|
+
this.#toolDurationMs = meter.createHistogram("pi.omp.agent.tool.duration", {
|
|
296
|
+
description: "Total tool latency observed in an agent run.",
|
|
297
|
+
unit: "ms",
|
|
298
|
+
});
|
|
299
|
+
this.#errors = meter.createCounter("pi.omp.agent.errors", {
|
|
300
|
+
description: "Errors observed in chat and tool execution.",
|
|
301
|
+
unit: "{error}",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
recordChatUsage(event: ChatUsageEvent): void {
|
|
306
|
+
const baseAttrs = metricAttributes({
|
|
307
|
+
"gen_ai.operation.name": "chat",
|
|
308
|
+
"gen_ai.provider.name": event.provider,
|
|
309
|
+
"gen_ai.request.model": event.model,
|
|
310
|
+
"gen_ai.response.service_tier": event.serviceTier,
|
|
311
|
+
"pi.gen_ai.agent.id": event.agent?.id,
|
|
312
|
+
"pi.gen_ai.agent.name": event.agent?.name,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this.#recordToken(event.usage.inputTokens, baseAttrs, "input");
|
|
316
|
+
this.#recordToken(event.usage.outputTokens, baseAttrs, "output");
|
|
317
|
+
this.#recordToken(event.usage.totalTokens, baseAttrs, "total");
|
|
318
|
+
this.#recordToken(event.usage.cachedInputTokens, baseAttrs, "cache_read_input");
|
|
319
|
+
this.#recordToken(event.usage.cacheWriteTokens, baseAttrs, "cache_write_input");
|
|
320
|
+
this.#recordToken(event.usage.reasoningOutputTokens, baseAttrs, "reasoning_output");
|
|
321
|
+
|
|
322
|
+
if (event.cost && "usd" in event.cost && event.cost.usd > 0) {
|
|
323
|
+
this.#chatCostUsd.add(event.cost.usd, baseAttrs);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
recordRun(summary: AgentRunSummary, coverage: AgentRunCoverage): void {
|
|
328
|
+
const runAttrs = metricAttributes({
|
|
329
|
+
"pi.omp.agent.models_used.count": coverage.modelsUsed.length,
|
|
330
|
+
"pi.omp.agent.providers_used.count": coverage.providersUsed.length,
|
|
331
|
+
"pi.omp.agent.tools_available.count": coverage.toolsAvailable.length,
|
|
332
|
+
"pi.omp.agent.tools_invoked.count": coverage.toolsInvoked.length,
|
|
333
|
+
"pi.omp.agent.tools_unused.count": coverage.toolsUnused.length,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
this.#runs.add(1, runAttrs);
|
|
337
|
+
if (summary.stepCount > 0) this.#steps.add(summary.stepCount, runAttrs);
|
|
338
|
+
if (summary.chats.totalLatencyMs > 0) this.#chatDurationMs.record(summary.chats.totalLatencyMs, runAttrs);
|
|
339
|
+
|
|
340
|
+
for (const reason in summary.chats.byStopReason) {
|
|
341
|
+
const count = summary.chats.byStopReason[reason];
|
|
342
|
+
if (count > 0)
|
|
343
|
+
this.#chatCalls.add(count, metricAttributes({ ...runAttrs, "gen_ai.response.finish_reason": reason }));
|
|
344
|
+
}
|
|
345
|
+
for (const toolName in summary.tools.byName) {
|
|
346
|
+
const counters = summary.tools.byName[toolName];
|
|
347
|
+
const toolAttrs = metricAttributes({ ...runAttrs, "gen_ai.tool.name": toolName });
|
|
348
|
+
if (counters.totalLatencyMs > 0) this.#toolDurationMs.record(counters.totalLatencyMs, toolAttrs);
|
|
349
|
+
for (const status of TOOL_STATUSES) {
|
|
350
|
+
const count = counters[status];
|
|
351
|
+
if (count > 0) this.#toolCalls.add(count, metricAttributes({ ...toolAttrs, "pi.omp.tool.status": status }));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
for (const errorType in summary.errors.byType) {
|
|
355
|
+
const count = summary.errors.byType[errorType];
|
|
356
|
+
if (count > 0) this.#errors.add(count, metricAttributes({ ...runAttrs, "error.type": errorType }));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#recordToken(value: number | undefined, baseAttrs: Attributes, tokenType: string): void {
|
|
361
|
+
if (!value || value <= 0) return;
|
|
362
|
+
this.#tokenUsage.record(value, metricAttributes({ ...baseAttrs, "gen_ai.token.type": tokenType }));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function metricAttributes(fields: Readonly<Record<string, unknown>>): Attributes {
|
|
367
|
+
const out: Attributes = {};
|
|
368
|
+
for (const key in fields) {
|
|
369
|
+
const value = fields[key];
|
|
370
|
+
if (value === undefined || value === null) continue;
|
|
371
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
372
|
+
out[key] = value;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
const text = String(value);
|
|
376
|
+
if (text.length > 0) out[key] = text;
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function emitRunSummaryLog(summary: AgentRunSummary, coverage: AgentRunCoverage): void {
|
|
382
|
+
emitOtelLog(
|
|
383
|
+
"info",
|
|
384
|
+
"agent run completed",
|
|
385
|
+
{
|
|
386
|
+
"pi.omp.agent.step_count": summary.stepCount,
|
|
387
|
+
"pi.omp.agent.chats.total": summary.chats.total,
|
|
388
|
+
"pi.omp.agent.chats.total_latency_ms": summary.chats.totalLatencyMs,
|
|
389
|
+
"pi.omp.agent.tools.total": summary.tools.total,
|
|
390
|
+
"pi.omp.agent.tools.ok": summary.tools.ok,
|
|
391
|
+
"pi.omp.agent.tools.error": summary.tools.error,
|
|
392
|
+
"pi.omp.agent.tools.skipped": summary.tools.skipped,
|
|
393
|
+
"pi.omp.agent.tools.blocked": summary.tools.blocked,
|
|
394
|
+
"pi.omp.agent.tools.timeout": summary.tools.timeout,
|
|
395
|
+
"pi.omp.agent.tools.aborted": summary.tools.aborted,
|
|
396
|
+
"pi.omp.agent.tools.total_latency_ms": summary.tools.totalLatencyMs,
|
|
397
|
+
"pi.omp.agent.usage.input_tokens": summary.usage.inputTokens,
|
|
398
|
+
"pi.omp.agent.usage.output_tokens": summary.usage.outputTokens,
|
|
399
|
+
"pi.omp.agent.usage.cached_input_tokens": summary.usage.cachedInputTokens,
|
|
400
|
+
"pi.omp.agent.usage.cache_write_tokens": summary.usage.cacheWriteTokens,
|
|
401
|
+
"pi.omp.agent.usage.reasoning_output_tokens": summary.usage.reasoningOutputTokens,
|
|
402
|
+
"pi.omp.agent.usage.total_tokens": summary.usage.totalTokens,
|
|
403
|
+
"pi.omp.agent.cost.estimated_usd": summary.cost.estimatedUsd,
|
|
404
|
+
"pi.omp.agent.cost.unavailable_reasons": summary.cost.unavailableReasons.join(","),
|
|
405
|
+
"pi.omp.agent.errors.total": summary.errors.total,
|
|
406
|
+
"pi.omp.agent.coverage.tools_available": coverage.toolsAvailable.join(","),
|
|
407
|
+
"pi.omp.agent.coverage.tools_invoked": coverage.toolsInvoked.join(","),
|
|
408
|
+
"pi.omp.agent.coverage.tools_unused": coverage.toolsUnused.join(","),
|
|
409
|
+
"pi.omp.agent.coverage.models_used": coverage.modelsUsed.join(","),
|
|
410
|
+
"pi.omp.agent.coverage.providers_used": coverage.providersUsed.join(","),
|
|
411
|
+
},
|
|
412
|
+
"pi.omp.agent.run.completed",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function emitTelemetryWarningLog(warning: AgentTelemetryWarning): void {
|
|
417
|
+
const attrs = logAttributesFromContext({
|
|
418
|
+
code: warning.code,
|
|
419
|
+
error: warning.error,
|
|
420
|
+
});
|
|
421
|
+
emitOtelLog("warn", warning.message, attrs, "pi.omp.telemetry.warning");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function emitOtelLog(
|
|
425
|
+
level: logger.LogLevel,
|
|
426
|
+
body: string,
|
|
427
|
+
attributes: LogAttributes,
|
|
428
|
+
eventName: string,
|
|
429
|
+
timestamp = new Date(),
|
|
430
|
+
): void {
|
|
431
|
+
if (!otelLogger) return;
|
|
432
|
+
const minLevel = parseOtelLogLevel(process.env.OTEL_LOG_LEVEL);
|
|
433
|
+
if (minLevel === "none") return;
|
|
434
|
+
if (LOG_LEVEL_WEIGHT[level] > LOG_LEVEL_WEIGHT[minLevel]) return;
|
|
435
|
+
otelLogger.emit({
|
|
436
|
+
eventName,
|
|
437
|
+
timestamp,
|
|
438
|
+
observedTimestamp: new Date(),
|
|
439
|
+
severityNumber: LOG_SEVERITY[level],
|
|
440
|
+
severityText: level.toUpperCase(),
|
|
441
|
+
body,
|
|
442
|
+
attributes,
|
|
443
|
+
context: context.active(),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function parseOtelLogLevel(raw: string | undefined): OtelLogLevel {
|
|
448
|
+
if (!raw) return "info";
|
|
449
|
+
switch (raw.trim().toLowerCase()) {
|
|
450
|
+
case "none":
|
|
451
|
+
return "none";
|
|
452
|
+
case "error":
|
|
453
|
+
return "error";
|
|
454
|
+
case "warn":
|
|
455
|
+
case "warning":
|
|
456
|
+
return "warn";
|
|
457
|
+
case "debug":
|
|
458
|
+
return "debug";
|
|
459
|
+
default:
|
|
460
|
+
return "info";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function logAttributesFromContext(input: Record<string, unknown> | undefined): LogAttributes {
|
|
465
|
+
const out: LogAttributes = { "process.pid": process.pid };
|
|
466
|
+
if (!input) return out;
|
|
467
|
+
for (const key in input) {
|
|
468
|
+
const attr = logAttributeValue(input[key]);
|
|
469
|
+
if (attr !== undefined) out[key] = attr;
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function logAttributeValue(value: unknown): AttributeValue | undefined {
|
|
475
|
+
if (value === undefined || value === null) return undefined;
|
|
476
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
477
|
+
if (value instanceof Error) {
|
|
478
|
+
return `${value.name}: ${value.message}`;
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
const text = JSON.stringify(value);
|
|
482
|
+
if (text && text.length > 0) return text;
|
|
483
|
+
} catch {
|
|
484
|
+
return String(value);
|
|
485
|
+
}
|
|
486
|
+
return String(value);
|
|
135
487
|
}
|
|
136
488
|
|
|
137
489
|
/**
|
|
138
|
-
* Flush
|
|
490
|
+
* Flush buffered spans, log records, and metrics. No-op when export is disabled.
|
|
139
491
|
* Hosts embedding the agent can call this at natural boundaries (e.g. the end
|
|
140
|
-
* of a turn) so
|
|
492
|
+
* of a turn) so telemetry surfaces promptly rather than on the batch interval.
|
|
141
493
|
*/
|
|
142
494
|
export async function flushTelemetryExport(): Promise<void> {
|
|
143
|
-
|
|
495
|
+
const flushes: Promise<void>[] = [];
|
|
496
|
+
if (traceProvider) flushes.push(traceProvider.forceFlush());
|
|
497
|
+
if (logProvider) flushes.push(logProvider.forceFlush());
|
|
498
|
+
if (meterProvider) flushes.push(meterProvider.forceFlush());
|
|
499
|
+
await Promise.all(flushes);
|
|
144
500
|
}
|
package/src/tools/approval.ts
CHANGED
|
@@ -75,6 +75,17 @@ function getToolDecision(tool: ApprovalSubject, args: unknown): Omit<ResolvedApp
|
|
|
75
75
|
return normalizeDecision(decision);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Evaluate a tool's own approval declaration against `args` and return the
|
|
80
|
+
* resulting capability tier, defaulting to `exec` when the tool omits an
|
|
81
|
+
* approval. Unlike reading `tool.approval` directly, this runs function-valued
|
|
82
|
+
* approvals — the write tool's `xd://` gate uses it to take a mounted device's
|
|
83
|
+
* argument-dependent tier instead of falling back to `exec`.
|
|
84
|
+
*/
|
|
85
|
+
export function resolveToolTier(tool: ApprovalSubject, args: unknown): ToolTier {
|
|
86
|
+
return getToolDecision(tool, args).tier;
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
function modeApprovesTier(mode: ApprovalMode, tier: ToolTier): boolean {
|
|
79
90
|
return TIER_RANK[tier] <= TIER_RANK[APPROVAL_MODE_MAX_TIER[mode]];
|
|
80
91
|
}
|