@agimon-ai/doompi-log 0.0.1-alpha.6 → 0.0.1-alpha.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -21
- package/dist/adapters/hubApi.cjs +2 -0
- package/dist/adapters/hubApi.cjs.map +1 -0
- package/dist/adapters/hubApi.d.cts +15 -0
- package/dist/adapters/hubApi.d.cts.map +1 -0
- package/dist/adapters/hubApi.d.mts +15 -0
- package/dist/adapters/hubApi.d.mts.map +1 -0
- package/dist/adapters/hubApi.mjs +2 -0
- package/dist/adapters/hubApi.mjs.map +1 -0
- package/dist/adapters/node/issuesSource.cjs +2 -0
- package/dist/adapters/node/issuesSource.cjs.map +1 -0
- package/dist/adapters/node/issuesSource.mjs +2 -0
- package/dist/adapters/node/issuesSource.mjs.map +1 -0
- package/dist/adapters/node/metricsSource.cjs +2 -0
- package/dist/adapters/node/metricsSource.cjs.map +1 -0
- package/dist/adapters/node/metricsSource.d.cts +16 -0
- package/dist/adapters/node/metricsSource.d.cts.map +1 -0
- package/dist/adapters/node/metricsSource.d.mts +16 -0
- package/dist/adapters/node/metricsSource.d.mts.map +1 -0
- package/dist/adapters/node/metricsSource.mjs +2 -0
- package/dist/adapters/node/metricsSource.mjs.map +1 -0
- package/dist/adapters/pi/extension.cjs +2 -0
- package/dist/adapters/pi/extension.cjs.map +1 -0
- package/dist/adapters/pi/extension.d.cts +39 -0
- package/dist/adapters/pi/extension.d.cts.map +1 -0
- package/dist/adapters/pi/extension.d.mts +39 -0
- package/dist/adapters/pi/extension.d.mts.map +1 -0
- package/dist/adapters/pi/extension.mjs +2 -0
- package/dist/adapters/pi/extension.mjs.map +1 -0
- package/dist/extensions/pi.cjs +1 -2
- package/dist/extensions/pi.d.cts +2 -4
- package/dist/extensions/pi.d.mts +2 -5
- package/dist/extensions/pi.mjs +1 -2
- package/dist/hubApi.cjs +1 -0
- package/dist/hubApi.d.cts +2 -0
- package/dist/hubApi.d.mts +2 -0
- package/dist/hubApi.mjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +1 -1
- package/dist/metrics.cjs +1 -2
- package/dist/metrics.d.cts +3 -94
- package/dist/metrics.d.mts +3 -94
- package/dist/metrics.mjs +1 -2
- package/dist/metricsSource.cjs +1 -2
- package/dist/metricsSource.d.cts +3 -25
- package/dist/metricsSource.d.mts +3 -25
- package/dist/metricsSource.mjs +1 -2
- package/dist/services/findings.cjs +2 -0
- package/dist/services/findings.cjs.map +1 -0
- package/dist/services/findings.d.cts +22 -0
- package/dist/services/findings.d.cts.map +1 -0
- package/dist/services/findings.d.mts +22 -0
- package/dist/services/findings.d.mts.map +1 -0
- package/dist/services/findings.mjs +2 -0
- package/dist/services/findings.mjs.map +1 -0
- package/dist/services/metrics.cjs +2 -0
- package/dist/services/metrics.cjs.map +1 -0
- package/dist/services/metrics.d.cts +164 -0
- package/dist/services/metrics.d.cts.map +1 -0
- package/dist/services/metrics.d.mts +164 -0
- package/dist/services/metrics.d.mts.map +1 -0
- package/dist/services/metrics.mjs +2 -0
- package/dist/services/metrics.mjs.map +1 -0
- package/dist/tui/doomOverlay.d.cts +3 -4
- package/dist/tui/doomOverlay.d.cts.map +1 -1
- package/dist/tui/doomOverlay.d.mts +3 -4
- package/dist/tui/doomOverlay.d.mts.map +1 -1
- package/dist/tui/logMetricsOverlay.cjs +2 -0
- package/dist/tui/logMetricsOverlay.cjs.map +1 -0
- package/dist/tui/logMetricsOverlay.d.cts +89 -0
- package/dist/tui/logMetricsOverlay.d.cts.map +1 -0
- package/dist/tui/logMetricsOverlay.d.mts +89 -0
- package/dist/tui/logMetricsOverlay.d.mts.map +1 -0
- package/dist/tui/logMetricsOverlay.mjs +2 -0
- package/dist/tui/logMetricsOverlay.mjs.map +1 -0
- package/dist/tui/metricsOverlay.cjs +1 -2
- package/dist/tui/metricsOverlay.d.cts +2 -71
- package/dist/tui/metricsOverlay.d.mts +2 -71
- package/dist/tui/metricsOverlay.mjs +1 -2
- package/dist/types/issuesSource.d.cts +57 -0
- package/dist/types/issuesSource.d.cts.map +1 -0
- package/dist/types/issuesSource.d.mts +57 -0
- package/dist/types/issuesSource.d.mts.map +1 -0
- package/dist/types/metricsSource.d.cts +48 -0
- package/dist/types/metricsSource.d.cts.map +1 -0
- package/dist/types/metricsSource.d.mts +48 -0
- package/dist/types/metricsSource.d.mts.map +1 -0
- package/dist/types/webMetrics.cjs +2 -0
- package/dist/types/webMetrics.cjs.map +1 -0
- package/dist/types/webMetrics.mjs +2 -0
- package/dist/types/webMetrics.mjs.map +1 -0
- package/package.json +67 -22
- package/src/exports/webClient.ts +1 -0
- package/src/types/issueGrouping.ts +86 -0
- package/src/types/webMetrics.ts +245 -0
- package/src/web/api/metricsApi.ts +114 -0
- package/src/web/components/IssuesDetail.tsx +147 -0
- package/src/web/components/IssuesSection.tsx +83 -0
- package/src/web/components/MetricsNotice.tsx +59 -0
- package/src/web/components/MetricsPanel.tsx +126 -0
- package/src/web/components/MetricsReportView.tsx +132 -0
- package/src/web/components/charts/GroupBars.tsx +83 -0
- package/src/web/components/charts/TimelineChart.tsx +76 -0
- package/src/web/index.ts +23 -0
- package/src/web/lib/chartScale.ts +47 -0
- package/src/web/tsconfig.json +23 -0
- package/dist/extension.cjs +0 -2
- package/dist/extension.cjs.map +0 -1
- package/dist/extension.d.cts +0 -33
- package/dist/extension.d.cts.map +0 -1
- package/dist/extension.d.mts +0 -33
- package/dist/extension.d.mts.map +0 -1
- package/dist/extension.mjs +0 -2
- package/dist/extension.mjs.map +0 -1
- package/dist/extensions/pi.cjs.map +0 -1
- package/dist/extensions/pi.d.cts.map +0 -1
- package/dist/extensions/pi.d.mts.map +0 -1
- package/dist/extensions/pi.mjs.map +0 -1
- package/dist/metrics.cjs.map +0 -1
- package/dist/metrics.d.cts.map +0 -1
- package/dist/metrics.d.mts.map +0 -1
- package/dist/metrics.mjs.map +0 -1
- package/dist/metricsSource.cjs.map +0 -1
- package/dist/metricsSource.d.cts.map +0 -1
- package/dist/metricsSource.d.mts.map +0 -1
- package/dist/metricsSource.mjs.map +0 -1
- package/dist/tui/metricsOverlay.cjs.map +0 -1
- package/dist/tui/metricsOverlay.d.cts.map +0 -1
- package/dist/tui/metricsOverlay.d.mts.map +0 -1
- package/dist/tui/metricsOverlay.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,40 +1,89 @@
|
|
|
1
1
|
# @agimon-ai/doompi-log
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Session metrics, findings, sink status, and a Log Metrics overlay for Pi.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Part of the [DoomPi distribution](https://www.npmjs.com/package/@agimon-ai/doompi).
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
The extension aggregates telemetry it observes in-process and can query historical data from
|
|
8
|
+
`@agimon-ai/log-sink-mcp`.
|
|
9
|
+
|
|
10
|
+
> **Alpha:** metric attribution and UI contracts may change between releases.
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- Node.js 22.19.0 or newer
|
|
15
|
+
- Pi 0.85.0 and Pi TUI 0.85.0
|
|
10
16
|
|
|
11
17
|
## Install
|
|
12
18
|
|
|
19
|
+
`doompi init` and `dpi init` include Log in `default.packages` in `.doom/modes.yaml`. Remove it
|
|
20
|
+
or move it to a named layer to change which major modes load it. Pi discovers its sole extension
|
|
21
|
+
entry through `package.json.pi.extensions`. For standalone Pi:
|
|
22
|
+
|
|
13
23
|
```bash
|
|
14
|
-
|
|
24
|
+
pi install npm:@agimon-ai/doompi-log
|
|
15
25
|
```
|
|
16
26
|
|
|
17
|
-
|
|
27
|
+
Use `SPC h l` to open the current session's Log Metrics overlay.
|
|
28
|
+
|
|
29
|
+
## What is collected
|
|
30
|
+
|
|
31
|
+
The in-process aggregator tracks operational records such as turn token totals, cache use, tool
|
|
32
|
+
calls and failures, operation durations, recent errors, and event counts. It derives findings from
|
|
33
|
+
those records without requiring an external sink.
|
|
34
|
+
|
|
35
|
+
Historical panels resolve a sink over HTTP first and then use the Log Sink CLI fallback. Without a
|
|
36
|
+
reachable sink, current-session headline metrics, findings, errors, and tool metrics still work;
|
|
37
|
+
historical top-consumer and burn-rate views are unavailable. Sink failures are reported in status
|
|
38
|
+
rather than failing the Pi session.
|
|
39
|
+
|
|
40
|
+
Turn cost attribution is contextual: when a turn invokes several tools, the emitted turn total can
|
|
41
|
+
be associated with each participating tool. The overlay uses that value for ranking, not as a
|
|
42
|
+
precise per-tool share.
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
`.doom/modes.yaml`: making it optional would only create broken configurations.
|
|
44
|
+
Keys: `r` refreshes, `g` changes dimension, `p` changes period, `?` opens details, and Escape closes.
|
|
21
45
|
|
|
22
|
-
|
|
46
|
+
## Telemetry controls
|
|
23
47
|
|
|
24
|
-
|
|
48
|
+
| Variable | Effect |
|
|
49
|
+
| -------------------------- | ------------------------------------------------------ |
|
|
50
|
+
| `AGENT_TELEMETRY_DISABLED` | Disables the extension's telemetry export |
|
|
51
|
+
| `OTEL_SDK_DISABLED` | Disables OpenTelemetry SDK export |
|
|
52
|
+
| `AGENT_OTEL_TRACES` | Enables/disables trace export; logs can remain enabled |
|
|
25
53
|
|
|
26
|
-
|
|
54
|
+
File fallback is opt-in through host options. The absence of a configured or reachable endpoint
|
|
55
|
+
does not prevent in-process aggregation. Exceptions and caller-supplied attributes can still be
|
|
56
|
+
sensitive; do not assume "metadata" is harmless.
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
The overlay requires a TUI. The aggregator, findings, and metrics-source APIs can be embedded in
|
|
59
|
+
headless hosts.
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
installDoomLogRuntime,
|
|
66
|
+
deriveFindings,
|
|
67
|
+
LogMetricsAggregator,
|
|
68
|
+
openLogMetricsOverlay,
|
|
69
|
+
} from '@agimon-ai/doompi-log';
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Normal Pi activation should use the discovered `extensions/pi` entry. Host integrations can call
|
|
73
|
+
`installDoomLogRuntime` to install package resources into the shared runner-scoped Cordis host. The
|
|
74
|
+
extension entry manages its plugin lifecycle and releases the host lease when the Pi session shuts
|
|
75
|
+
down. Focused exports provide metrics types, the metrics source, and the overlay component.
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pnpm build
|
|
81
|
+
pnpm typecheck
|
|
82
|
+
pnpm test
|
|
83
|
+
pnpm lint
|
|
84
|
+
```
|
|
29
85
|
|
|
30
|
-
|
|
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` |
|
|
86
|
+
Maintained by [Agimon](https://agimon.ai/about).
|
|
38
87
|
|
|
39
88
|
## License
|
|
40
89
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./node/issuesSource.cjs"),t=require("./node/metricsSource.cjs"),n=require("../types/webMetrics.cjs");let r=require("hono");const i={session:`session`,agent:`agent`,model:`model`,provider:`provider`};function a(e){return{label:e.label,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens}}function o(e){return{key:e.key,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens,issueCount:e.issueCount,failed:e.failed}}function s(e){return{name:e.toolName,calls:e.invocationCount,p90TotalTokens:e.toolCallTurn.p90TotalTokens}}const c={session:`sessionId`,agent:`agentName`,model:`model`,provider:`provider`};function l(e,t){let n=e.filters;if(n===void 0)return;if(t===`session`)return n.sessionId;if(t===`agent`)return n.agentName;let r=t===`model`?n.model:n.provider;if(r!==void 0)return Array.isArray(r)?r[0]:r}function u(e){if(e instanceof Date)return e.toISOString();let t=new Date(e);return Number.isNaN(t.getTime())?new Date(0).toISOString():t.toISOString()}function d(e,t,n){let r=l(e,n);return{generatedAt:u(e.generatedAt),dimension:e.groupBy,period:e.period,bucketUnit:String(e.bucket),transport:t.lastTransport(),...r===void 0?{}:{focus:r},totals:{totalTokens:e.totals.totalTokens,inputTokens:e.totals.inputTokens,outputTokens:e.totals.outputTokens,cachedTokens:e.totals.cachedInputTokens,reasoningTokens:e.totals.reasoningOutputTokens,groupCount:e.totals.groupCount,failedGroups:e.totals.failedGroups,issueCount:e.totals.issueCount},timeline:e.timeline.map(a),groups:e.groups.map(o),tools:e.tools.rows.map(s)}}function f(e){return e.totals.totalTokens===0&&e.groups.length===0&&e.tools.length===0}function p(a={}){let o=new r.Hono,s=a.source??t.createMetricsSource(),l=a.issues??e.createIssuesSource();return o.get(`/metrics`,async e=>{let t=e.req.query(n.METRICS_QUERY_PARAMS.dimension)??`model`,r=e.req.query(n.METRICS_QUERY_PARAMS.period)??`week`;if(!n.isMetricsDimension(t))return e.json({error:`Unknown dimension '${t}'.`},400);if(!n.isMetricsPeriod(r))return e.json({error:`Unknown period '${r}'.`},400);let a=e.req.query(n.METRICS_QUERY_PARAMS.focus),o=a===void 0||a===``?void 0:{[c[t]]:a},l;try{l=await s.query({groupBy:i[t],period:r,limit:20,toolLimit:15,...o===void 0?{}:{filter:o}})}catch(t){let n={unavailable:`no-sink`,detail:t instanceof Error?t.message:`The log sink did not answer.`};return e.json(n)}let u=d(l,s,t);return f(u)?e.json({unavailable:`no-data`,detail:`The log sink is running but has recorded no usage for this period.`}):e.json(u)}),o.get(`/issues`,async e=>{let t=e.req.query(n.METRICS_QUERY_PARAMS.focus);try{let n=await l.query({limit:50,...t===void 0||t===``?{}:{sessionId:t}});return e.json(n)}catch(t){let n={unavailable:`no-sink`,detail:t instanceof Error?t.message:`The log sink did not answer.`};return e.json(n)}}),o}const m={basePath:`log`,start(e){let t=p();return{fetch:e=>t.fetch(e),close:()=>void 0}}};exports.api=m,exports.createLogHubApi=p;
|
|
2
|
+
//# sourceMappingURL=hubApi.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubApi.cjs","names":["Hono","createMetricsSource","createIssuesSource","METRICS_QUERY_PARAMS","isMetricsDimension","isMetricsPeriod"],"sources":["../../src/adapters/hubApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport type { LogMetricGroupRow, LogMetricsReport, ToolMetricRow } from '@agimon-ai/log-sink-mcp';\nimport { Hono } from 'hono';\nimport { createIssuesSource } from './node/issuesSource.ts';\nimport { createMetricsSource } from './node/metricsSource.ts';\nimport type { IssuesSource } from '../types/issuesSource.ts';\nimport type { MetricsFilter, MetricsSource } from '../types/metricsSource.ts';\nimport {\n isMetricsDimension,\n isMetricsPeriod,\n LOG_API_BASE_PATH,\n ISSUE_SAMPLE_LIMIT,\n METRICS_GROUP_LIMIT,\n METRICS_QUERY_PARAMS,\n METRICS_TOOL_LIMIT,\n type MetricsBucket,\n type MetricsDimension,\n type MetricsGroup,\n type MetricsPeriod,\n type MetricsReport,\n type MetricsTool,\n type MetricsUnavailable,\n type IssuesView,\n} from '../types/webMetrics.ts';\n\n/**\n * The cockpit's half of this package's metrics access.\n *\n * Hub-scoped rather than session-scoped: the questions this answers, where the\n * tokens went and which agents failed, are about the machine over time, and a\n * session server can only see itself. The sink is already machine-wide, so the\n * hub is the only place the two agree.\n *\n * The query path is not reimplemented here. `createMetricsSource` already owns\n * the sink's two transports and its fallback order, and the TUI overlay reads\n * through the same source, so both surfaces answer from one implementation.\n */\n\nconst DEFAULT_DIMENSION: MetricsDimension = 'model';\nconst DEFAULT_PERIOD: MetricsPeriod = 'week';\n\n/** The sink groups sessions under a different name than the page's dimension. */\nconst SINK_GROUP_BY = {\n session: 'session',\n agent: 'agent',\n model: 'model',\n provider: 'provider',\n} as const;\n\nfunction toBucket(bucket: LogMetricsReport['timeline'][number]): MetricsBucket {\n return {\n label: bucket.label,\n totalTokens: bucket.totalTokens,\n inputTokens: bucket.inputTokens,\n outputTokens: bucket.outputTokens,\n };\n}\n\nfunction toGroup(row: LogMetricGroupRow): MetricsGroup {\n return {\n key: row.key,\n totalTokens: row.totalTokens,\n inputTokens: row.inputTokens,\n outputTokens: row.outputTokens,\n issueCount: row.issueCount,\n failed: row.failed,\n };\n}\n\n/**\n * The turn's p90 rather than a sum: the sink attributes a whole turn's tokens\n * to every tool that ran in it, so adding them across calls would multiply the\n * same tokens by however many tools shared the turn.\n */\nfunction toTool(row: ToolMetricRow): MetricsTool {\n return {\n name: row.toolName,\n calls: row.invocationCount,\n p90TotalTokens: row.toolCallTurn.p90TotalTokens,\n };\n}\n\n/** Which filter field carries a focus value, per dimension. */\nconst FOCUS_FIELD: Record<MetricsDimension, keyof MetricsFilter> = {\n session: 'sessionId',\n agent: 'agentName',\n model: 'model',\n provider: 'provider',\n};\n\n/**\n * The focus the sink actually applied, read from its own echo.\n *\n * A daemon that predates a filter ignores it and answers with everything. If\n * the page trusted the request instead of this echo, it would label the whole\n * machine's numbers as one model's.\n */\nfunction appliedFocus(report: LogMetricsReport, dimension: MetricsDimension): string | undefined {\n // The daemon is a separate process on its own release cadence, so its\n // response is parsed defensively rather than trusted to carry every field.\n const filters = report.filters as LogMetricsReport['filters'] | undefined;\n if (filters === undefined) return undefined;\n if (dimension === 'session') return filters.sessionId;\n if (dimension === 'agent') return filters.agentName;\n const applied = dimension === 'model' ? filters.model : filters.provider;\n if (applied === undefined) return undefined;\n return Array.isArray(applied) ? applied[0] : applied;\n}\n\n/**\n * The sink's own type says Date, but both transports parse JSON, so what\n * arrives is an ISO string. Trusting the declared type here throws on every\n * real response, which no test using a hand-built Date would ever catch.\n */\nfunction generatedAtIso(value: LogMetricsReport['generatedAt']): string {\n if (value instanceof Date) return value.toISOString();\n const parsed = new Date(value as unknown as string);\n return Number.isNaN(parsed.getTime()) ? new Date(0).toISOString() : parsed.toISOString();\n}\n\nfunction toReport(report: LogMetricsReport, source: MetricsSource, dimension: MetricsDimension): MetricsReport {\n const focus = appliedFocus(report, dimension);\n return {\n generatedAt: generatedAtIso(report.generatedAt),\n dimension: report.groupBy as MetricsDimension,\n period: report.period as MetricsPeriod,\n bucketUnit: String(report.bucket),\n transport: source.lastTransport(),\n ...(focus === undefined ? {} : { focus }),\n totals: {\n totalTokens: report.totals.totalTokens,\n inputTokens: report.totals.inputTokens,\n outputTokens: report.totals.outputTokens,\n cachedTokens: report.totals.cachedInputTokens,\n reasoningTokens: report.totals.reasoningOutputTokens,\n groupCount: report.totals.groupCount,\n failedGroups: report.totals.failedGroups,\n issueCount: report.totals.issueCount,\n },\n timeline: report.timeline.map(toBucket),\n groups: report.groups.map(toGroup),\n tools: report.tools.rows.map(toTool),\n };\n}\n\n/**\n * A report the sink answered but with nothing in it reads as \"no data\", not as\n * an empty chart. The distinction the reader needs is whether telemetry is not\n * being recorded or simply has not been recorded yet.\n */\nfunction isEmpty(report: MetricsReport): boolean {\n return report.totals.totalTokens === 0 && report.groups.length === 0 && report.tools.length === 0;\n}\n\nexport interface HubApiOptions {\n /** Injected by tests; production resolves the machine's sink. */\n source?: MetricsSource;\n issues?: IssuesSource;\n}\n\nexport function createLogHubApi(options: HubApiOptions = {}): Hono {\n const app = new Hono();\n // One source for the life of the hub: it caches the resolved sink endpoint,\n // and rebuilding it per request would re-probe the daemon every time.\n const source = options.source ?? createMetricsSource();\n const issues = options.issues ?? createIssuesSource();\n\n app.get('/metrics', async (context) => {\n const requestedDimension = context.req.query(METRICS_QUERY_PARAMS.dimension) ?? DEFAULT_DIMENSION;\n const requestedPeriod = context.req.query(METRICS_QUERY_PARAMS.period) ?? DEFAULT_PERIOD;\n if (!isMetricsDimension(requestedDimension)) {\n return context.json({ error: `Unknown dimension '${requestedDimension}'.` }, 400);\n }\n if (!isMetricsPeriod(requestedPeriod)) {\n return context.json({ error: `Unknown period '${requestedPeriod}'.` }, 400);\n }\n\n const focus = context.req.query(METRICS_QUERY_PARAMS.focus);\n const filter: MetricsFilter | undefined =\n focus === undefined || focus === '' ? undefined : { [FOCUS_FIELD[requestedDimension]]: focus };\n\n let report: LogMetricsReport;\n try {\n report = await source.query({\n groupBy: SINK_GROUP_BY[requestedDimension],\n period: requestedPeriod,\n limit: METRICS_GROUP_LIMIT,\n toolLimit: METRICS_TOOL_LIMIT,\n ...(filter === undefined ? {} : { filter }),\n });\n } catch (error) {\n // Both transports declined. That is the ordinary state on a machine\n // where the sink has never run, so it is reported as absence rather\n // than raised as a fault the page has to render as a crash.\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-sink',\n detail: error instanceof Error ? error.message : 'The log sink did not answer.',\n };\n return context.json(unavailable);\n }\n\n const body = toReport(report, source, requestedDimension);\n if (isEmpty(body)) {\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-data',\n detail: 'The log sink is running but has recorded no usage for this period.',\n };\n return context.json(unavailable);\n }\n return context.json(body);\n });\n\n /**\n * The detail behind the count the report carries. Separate from /metrics\n * because it costs a subprocess: the running daemon has no issues route, so\n * folding it into every report would make the whole page as slow as its\n * slowest transport.\n */\n app.get('/issues', async (context) => {\n const focus = context.req.query(METRICS_QUERY_PARAMS.focus);\n try {\n const report = await issues.query({\n limit: ISSUE_SAMPLE_LIMIT,\n ...(focus === undefined || focus === '' ? {} : { sessionId: focus }),\n });\n return context.json(report satisfies IssuesView);\n } catch (error) {\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-sink',\n detail: error instanceof Error ? error.message : 'The log sink did not answer.',\n };\n return context.json(unavailable);\n }\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built hub entry. */\nexport const api: DoomApi = {\n basePath: LOG_API_BASE_PATH,\n start(_context: DoomApiContext): DoomApiHandler {\n const app = createLogHubApi();\n return {\n fetch: (request) => app.fetch(request),\n // The source holds no handle of its own: the HTTP transport is a fetch\n // per query and the CLI transport is a subprocess bounded by its timeout.\n close: () => undefined,\n };\n },\n};\n"],"mappings":"4IAsCA,MAIM,EAAgB,CACpB,QAAS,UACT,MAAO,QACP,MAAO,QACP,SAAU,UACZ,EAEA,SAAS,EAAS,EAA6D,CAC7E,MAAO,CACL,MAAO,EAAO,MACd,YAAa,EAAO,YACpB,YAAa,EAAO,YACpB,aAAc,EAAO,YACvB,CACF,CAEA,SAAS,EAAQ,EAAsC,CACrD,MAAO,CACL,IAAK,EAAI,IACT,YAAa,EAAI,YACjB,YAAa,EAAI,YACjB,aAAc,EAAI,aAClB,WAAY,EAAI,WAChB,OAAQ,EAAI,MACd,CACF,CAOA,SAAS,EAAO,EAAiC,CAC/C,MAAO,CACL,KAAM,EAAI,SACV,MAAO,EAAI,gBACX,eAAgB,EAAI,aAAa,cACnC,CACF,CAGA,MAAM,EAA6D,CACjE,QAAS,YACT,MAAO,YACP,MAAO,QACP,SAAU,UACZ,EASA,SAAS,EAAa,EAA0B,EAAiD,CAG/F,IAAM,EAAU,EAAO,QACvB,GAAI,IAAY,IAAA,GAAW,OAC3B,GAAI,IAAc,UAAW,OAAO,EAAQ,UAC5C,GAAI,IAAc,QAAS,OAAO,EAAQ,UAC1C,IAAM,EAAU,IAAc,QAAU,EAAQ,MAAQ,EAAQ,SAC5D,OAAY,IAAA,GAChB,OAAO,MAAM,QAAQ,CAAO,EAAI,EAAQ,GAAK,CAC/C,CAOA,SAAS,EAAe,EAAgD,CACtE,GAAI,aAAiB,KAAM,OAAO,EAAM,YAAY,EACpD,IAAM,EAAS,IAAI,KAAK,CAA0B,EAClD,OAAO,OAAO,MAAM,EAAO,QAAQ,CAAC,EAAI,IAAI,KAAK,CAAC,CAAA,CAAE,YAAY,EAAI,EAAO,YAAY,CACzF,CAEA,SAAS,EAAS,EAA0B,EAAuB,EAA4C,CAC7G,IAAM,EAAQ,EAAa,EAAQ,CAAS,EAC5C,MAAO,CACL,YAAa,EAAe,EAAO,WAAW,EAC9C,UAAW,EAAO,QAClB,OAAQ,EAAO,OACf,WAAY,OAAO,EAAO,MAAM,EAChC,UAAW,EAAO,cAAc,EAChC,GAAI,IAAU,IAAA,GAAY,CAAC,EAAI,CAAE,OAAM,EACvC,OAAQ,CACN,YAAa,EAAO,OAAO,YAC3B,YAAa,EAAO,OAAO,YAC3B,aAAc,EAAO,OAAO,aAC5B,aAAc,EAAO,OAAO,kBAC5B,gBAAiB,EAAO,OAAO,sBAC/B,WAAY,EAAO,OAAO,WAC1B,aAAc,EAAO,OAAO,aAC5B,WAAY,EAAO,OAAO,UAC5B,EACA,SAAU,EAAO,SAAS,IAAI,CAAQ,EACtC,OAAQ,EAAO,OAAO,IAAI,CAAO,EACjC,MAAO,EAAO,MAAM,KAAK,IAAI,CAAM,CACrC,CACF,CAOA,SAAS,EAAQ,EAAgC,CAC/C,OAAO,EAAO,OAAO,cAAgB,GAAK,EAAO,OAAO,SAAW,GAAK,EAAO,MAAM,SAAW,CAClG,CAQA,SAAgB,EAAgB,EAAyB,CAAC,EAAS,CACjE,IAAM,EAAM,IAAIA,EAAAA,KAGV,EAAS,EAAQ,QAAUC,EAAAA,oBAAoB,EAC/C,EAAS,EAAQ,QAAUC,EAAAA,mBAAmB,EAsEpD,OApEA,EAAI,IAAI,WAAY,KAAO,IAAY,CACrC,IAAM,EAAqB,EAAQ,IAAI,MAAMC,EAAAA,qBAAqB,SAAS,GAAK,QAC1E,EAAkB,EAAQ,IAAI,MAAMA,EAAAA,qBAAqB,MAAM,GAAK,OAC1E,GAAI,CAACC,EAAAA,mBAAmB,CAAkB,EACxC,OAAO,EAAQ,KAAK,CAAE,MAAO,sBAAsB,EAAmB,GAAI,EAAG,GAAG,EAElF,GAAI,CAACC,EAAAA,gBAAgB,CAAe,EAClC,OAAO,EAAQ,KAAK,CAAE,MAAO,mBAAmB,EAAgB,GAAI,EAAG,GAAG,EAG5E,IAAM,EAAQ,EAAQ,IAAI,MAAMF,EAAAA,qBAAqB,KAAK,EACpD,EACJ,IAAU,IAAA,IAAa,IAAU,GAAK,IAAA,GAAY,EAAG,EAAY,IAAsB,CAAM,EAE3F,EACJ,GAAI,CACF,EAAS,MAAM,EAAO,MAAM,CAC1B,QAAS,EAAc,GACvB,OAAQ,EACR,MAAA,GACA,UAAA,GACA,GAAI,IAAW,IAAA,GAAY,CAAC,EAAI,CAAE,QAAO,CAC3C,CAAC,CACH,OAAS,EAAO,CAId,IAAM,EAAkC,CACtC,YAAa,UACb,OAAQ,aAAiB,MAAQ,EAAM,QAAU,8BACnD,EACA,OAAO,EAAQ,KAAK,CAAW,CACjC,CAEA,IAAM,EAAO,EAAS,EAAQ,EAAQ,CAAkB,EAQxD,OAPI,EAAQ,CAAI,EAKP,EAAQ,KAAK,CAHlB,YAAa,UACb,OAAQ,oEAEoB,CAAC,EAE1B,EAAQ,KAAK,CAAI,CAC1B,CAAC,EAQD,EAAI,IAAI,UAAW,KAAO,IAAY,CACpC,IAAM,EAAQ,EAAQ,IAAI,MAAMA,EAAAA,qBAAqB,KAAK,EAC1D,GAAI,CACF,IAAM,EAAS,MAAM,EAAO,MAAM,CAChC,MAAA,GACA,GAAI,IAAU,IAAA,IAAa,IAAU,GAAK,CAAC,EAAI,CAAE,UAAW,CAAM,CACpE,CAAC,EACD,OAAO,EAAQ,KAAK,CAA2B,CACjD,OAAS,EAAO,CACd,IAAM,EAAkC,CACtC,YAAa,UACb,OAAQ,aAAiB,MAAQ,EAAM,QAAU,8BACnD,EACA,OAAO,EAAQ,KAAK,CAAW,CACjC,CACF,CAAC,EAEM,CACT,CAGA,MAAa,EAAe,CAC1B,SAAA,MACA,MAAM,EAA0C,CAC9C,IAAM,EAAM,EAAgB,EAC5B,MAAO,CACL,MAAQ,GAAY,EAAI,MAAM,CAAO,EAGrC,UAAa,IAAA,EACf,CACF,CACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MetricsSource } from "../types/metricsSource.cjs";
|
|
2
|
+
import { IssuesSource } from "../types/issuesSource.cjs";
|
|
3
|
+
import { DoomApi } from "@agimon-ai/doompi-extension-contracts/package-api";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
//#region src/adapters/hubApi.d.ts
|
|
6
|
+
export interface HubApiOptions {
|
|
7
|
+
/** Injected by tests; production resolves the machine's sink. */
|
|
8
|
+
source?: MetricsSource;
|
|
9
|
+
issues?: IssuesSource;
|
|
10
|
+
}
|
|
11
|
+
export declare function createLogHubApi(options?: HubApiOptions): Hono;
|
|
12
|
+
/** The named export a host imports from this package's built hub entry. */
|
|
13
|
+
export declare const api: DoomApi;
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=hubApi.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubApi.d.cts","names":[],"sources":["../../src/adapters/hubApi.ts"],"mappings":";;;;;iBA0JiB;;EAEf,SAAS;EACT,SAAS;;wBAGK,gBAAgB,UAAS,gBAAqB;;qBA+EjD,KAAK"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { MetricsSource } from "../types/metricsSource.mjs";
|
|
2
|
+
import { IssuesSource } from "../types/issuesSource.mjs";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import { DoomApi } from "@agimon-ai/doompi-extension-contracts/package-api";
|
|
5
|
+
//#region src/adapters/hubApi.d.ts
|
|
6
|
+
export interface HubApiOptions {
|
|
7
|
+
/** Injected by tests; production resolves the machine's sink. */
|
|
8
|
+
source?: MetricsSource;
|
|
9
|
+
issues?: IssuesSource;
|
|
10
|
+
}
|
|
11
|
+
export declare function createLogHubApi(options?: HubApiOptions): Hono;
|
|
12
|
+
/** The named export a host imports from this package's built hub entry. */
|
|
13
|
+
export declare const api: DoomApi;
|
|
14
|
+
//#endregion
|
|
15
|
+
//# sourceMappingURL=hubApi.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubApi.d.mts","names":[],"sources":["../../src/adapters/hubApi.ts"],"mappings":";;;;;iBA0JiB;;EAEf,SAAS;EACT,SAAS;;wBAGK,gBAAgB,UAAS,gBAAqB;;qBA+EjD,KAAK"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createIssuesSource as e}from"./node/issuesSource.mjs";import{createMetricsSource as t}from"./node/metricsSource.mjs";import{METRICS_QUERY_PARAMS as n,isMetricsDimension as r,isMetricsPeriod as i}from"../types/webMetrics.mjs";import{Hono as a}from"hono";const o={session:`session`,agent:`agent`,model:`model`,provider:`provider`};function s(e){return{label:e.label,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens}}function c(e){return{key:e.key,totalTokens:e.totalTokens,inputTokens:e.inputTokens,outputTokens:e.outputTokens,issueCount:e.issueCount,failed:e.failed}}function l(e){return{name:e.toolName,calls:e.invocationCount,p90TotalTokens:e.toolCallTurn.p90TotalTokens}}const u={session:`sessionId`,agent:`agentName`,model:`model`,provider:`provider`};function d(e,t){let n=e.filters;if(n===void 0)return;if(t===`session`)return n.sessionId;if(t===`agent`)return n.agentName;let r=t===`model`?n.model:n.provider;if(r!==void 0)return Array.isArray(r)?r[0]:r}function f(e){if(e instanceof Date)return e.toISOString();let t=new Date(e);return Number.isNaN(t.getTime())?new Date(0).toISOString():t.toISOString()}function p(e,t,n){let r=d(e,n);return{generatedAt:f(e.generatedAt),dimension:e.groupBy,period:e.period,bucketUnit:String(e.bucket),transport:t.lastTransport(),...r===void 0?{}:{focus:r},totals:{totalTokens:e.totals.totalTokens,inputTokens:e.totals.inputTokens,outputTokens:e.totals.outputTokens,cachedTokens:e.totals.cachedInputTokens,reasoningTokens:e.totals.reasoningOutputTokens,groupCount:e.totals.groupCount,failedGroups:e.totals.failedGroups,issueCount:e.totals.issueCount},timeline:e.timeline.map(s),groups:e.groups.map(c),tools:e.tools.rows.map(l)}}function m(e){return e.totals.totalTokens===0&&e.groups.length===0&&e.tools.length===0}function h(s={}){let c=new a,l=s.source??t(),d=s.issues??e();return c.get(`/metrics`,async e=>{let t=e.req.query(n.dimension)??`model`,a=e.req.query(n.period)??`week`;if(!r(t))return e.json({error:`Unknown dimension '${t}'.`},400);if(!i(a))return e.json({error:`Unknown period '${a}'.`},400);let s=e.req.query(n.focus),c=s===void 0||s===``?void 0:{[u[t]]:s},d;try{d=await l.query({groupBy:o[t],period:a,limit:20,toolLimit:15,...c===void 0?{}:{filter:c}})}catch(t){let n={unavailable:`no-sink`,detail:t instanceof Error?t.message:`The log sink did not answer.`};return e.json(n)}let f=p(d,l,t);return m(f)?e.json({unavailable:`no-data`,detail:`The log sink is running but has recorded no usage for this period.`}):e.json(f)}),c.get(`/issues`,async e=>{let t=e.req.query(n.focus);try{let n=await d.query({limit:50,...t===void 0||t===``?{}:{sessionId:t}});return e.json(n)}catch(t){let n={unavailable:`no-sink`,detail:t instanceof Error?t.message:`The log sink did not answer.`};return e.json(n)}}),c}const g={basePath:`log`,start(e){let t=h();return{fetch:e=>t.fetch(e),close:()=>void 0}}};export{g as api,h as createLogHubApi};
|
|
2
|
+
//# sourceMappingURL=hubApi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hubApi.mjs","names":[],"sources":["../../src/adapters/hubApi.ts"],"sourcesContent":["import type { DoomApi, DoomApiContext, DoomApiHandler } from '@agimon-ai/doompi-extension-contracts/package-api';\nimport type { LogMetricGroupRow, LogMetricsReport, ToolMetricRow } from '@agimon-ai/log-sink-mcp';\nimport { Hono } from 'hono';\nimport { createIssuesSource } from './node/issuesSource.ts';\nimport { createMetricsSource } from './node/metricsSource.ts';\nimport type { IssuesSource } from '../types/issuesSource.ts';\nimport type { MetricsFilter, MetricsSource } from '../types/metricsSource.ts';\nimport {\n isMetricsDimension,\n isMetricsPeriod,\n LOG_API_BASE_PATH,\n ISSUE_SAMPLE_LIMIT,\n METRICS_GROUP_LIMIT,\n METRICS_QUERY_PARAMS,\n METRICS_TOOL_LIMIT,\n type MetricsBucket,\n type MetricsDimension,\n type MetricsGroup,\n type MetricsPeriod,\n type MetricsReport,\n type MetricsTool,\n type MetricsUnavailable,\n type IssuesView,\n} from '../types/webMetrics.ts';\n\n/**\n * The cockpit's half of this package's metrics access.\n *\n * Hub-scoped rather than session-scoped: the questions this answers, where the\n * tokens went and which agents failed, are about the machine over time, and a\n * session server can only see itself. The sink is already machine-wide, so the\n * hub is the only place the two agree.\n *\n * The query path is not reimplemented here. `createMetricsSource` already owns\n * the sink's two transports and its fallback order, and the TUI overlay reads\n * through the same source, so both surfaces answer from one implementation.\n */\n\nconst DEFAULT_DIMENSION: MetricsDimension = 'model';\nconst DEFAULT_PERIOD: MetricsPeriod = 'week';\n\n/** The sink groups sessions under a different name than the page's dimension. */\nconst SINK_GROUP_BY = {\n session: 'session',\n agent: 'agent',\n model: 'model',\n provider: 'provider',\n} as const;\n\nfunction toBucket(bucket: LogMetricsReport['timeline'][number]): MetricsBucket {\n return {\n label: bucket.label,\n totalTokens: bucket.totalTokens,\n inputTokens: bucket.inputTokens,\n outputTokens: bucket.outputTokens,\n };\n}\n\nfunction toGroup(row: LogMetricGroupRow): MetricsGroup {\n return {\n key: row.key,\n totalTokens: row.totalTokens,\n inputTokens: row.inputTokens,\n outputTokens: row.outputTokens,\n issueCount: row.issueCount,\n failed: row.failed,\n };\n}\n\n/**\n * The turn's p90 rather than a sum: the sink attributes a whole turn's tokens\n * to every tool that ran in it, so adding them across calls would multiply the\n * same tokens by however many tools shared the turn.\n */\nfunction toTool(row: ToolMetricRow): MetricsTool {\n return {\n name: row.toolName,\n calls: row.invocationCount,\n p90TotalTokens: row.toolCallTurn.p90TotalTokens,\n };\n}\n\n/** Which filter field carries a focus value, per dimension. */\nconst FOCUS_FIELD: Record<MetricsDimension, keyof MetricsFilter> = {\n session: 'sessionId',\n agent: 'agentName',\n model: 'model',\n provider: 'provider',\n};\n\n/**\n * The focus the sink actually applied, read from its own echo.\n *\n * A daemon that predates a filter ignores it and answers with everything. If\n * the page trusted the request instead of this echo, it would label the whole\n * machine's numbers as one model's.\n */\nfunction appliedFocus(report: LogMetricsReport, dimension: MetricsDimension): string | undefined {\n // The daemon is a separate process on its own release cadence, so its\n // response is parsed defensively rather than trusted to carry every field.\n const filters = report.filters as LogMetricsReport['filters'] | undefined;\n if (filters === undefined) return undefined;\n if (dimension === 'session') return filters.sessionId;\n if (dimension === 'agent') return filters.agentName;\n const applied = dimension === 'model' ? filters.model : filters.provider;\n if (applied === undefined) return undefined;\n return Array.isArray(applied) ? applied[0] : applied;\n}\n\n/**\n * The sink's own type says Date, but both transports parse JSON, so what\n * arrives is an ISO string. Trusting the declared type here throws on every\n * real response, which no test using a hand-built Date would ever catch.\n */\nfunction generatedAtIso(value: LogMetricsReport['generatedAt']): string {\n if (value instanceof Date) return value.toISOString();\n const parsed = new Date(value as unknown as string);\n return Number.isNaN(parsed.getTime()) ? new Date(0).toISOString() : parsed.toISOString();\n}\n\nfunction toReport(report: LogMetricsReport, source: MetricsSource, dimension: MetricsDimension): MetricsReport {\n const focus = appliedFocus(report, dimension);\n return {\n generatedAt: generatedAtIso(report.generatedAt),\n dimension: report.groupBy as MetricsDimension,\n period: report.period as MetricsPeriod,\n bucketUnit: String(report.bucket),\n transport: source.lastTransport(),\n ...(focus === undefined ? {} : { focus }),\n totals: {\n totalTokens: report.totals.totalTokens,\n inputTokens: report.totals.inputTokens,\n outputTokens: report.totals.outputTokens,\n cachedTokens: report.totals.cachedInputTokens,\n reasoningTokens: report.totals.reasoningOutputTokens,\n groupCount: report.totals.groupCount,\n failedGroups: report.totals.failedGroups,\n issueCount: report.totals.issueCount,\n },\n timeline: report.timeline.map(toBucket),\n groups: report.groups.map(toGroup),\n tools: report.tools.rows.map(toTool),\n };\n}\n\n/**\n * A report the sink answered but with nothing in it reads as \"no data\", not as\n * an empty chart. The distinction the reader needs is whether telemetry is not\n * being recorded or simply has not been recorded yet.\n */\nfunction isEmpty(report: MetricsReport): boolean {\n return report.totals.totalTokens === 0 && report.groups.length === 0 && report.tools.length === 0;\n}\n\nexport interface HubApiOptions {\n /** Injected by tests; production resolves the machine's sink. */\n source?: MetricsSource;\n issues?: IssuesSource;\n}\n\nexport function createLogHubApi(options: HubApiOptions = {}): Hono {\n const app = new Hono();\n // One source for the life of the hub: it caches the resolved sink endpoint,\n // and rebuilding it per request would re-probe the daemon every time.\n const source = options.source ?? createMetricsSource();\n const issues = options.issues ?? createIssuesSource();\n\n app.get('/metrics', async (context) => {\n const requestedDimension = context.req.query(METRICS_QUERY_PARAMS.dimension) ?? DEFAULT_DIMENSION;\n const requestedPeriod = context.req.query(METRICS_QUERY_PARAMS.period) ?? DEFAULT_PERIOD;\n if (!isMetricsDimension(requestedDimension)) {\n return context.json({ error: `Unknown dimension '${requestedDimension}'.` }, 400);\n }\n if (!isMetricsPeriod(requestedPeriod)) {\n return context.json({ error: `Unknown period '${requestedPeriod}'.` }, 400);\n }\n\n const focus = context.req.query(METRICS_QUERY_PARAMS.focus);\n const filter: MetricsFilter | undefined =\n focus === undefined || focus === '' ? undefined : { [FOCUS_FIELD[requestedDimension]]: focus };\n\n let report: LogMetricsReport;\n try {\n report = await source.query({\n groupBy: SINK_GROUP_BY[requestedDimension],\n period: requestedPeriod,\n limit: METRICS_GROUP_LIMIT,\n toolLimit: METRICS_TOOL_LIMIT,\n ...(filter === undefined ? {} : { filter }),\n });\n } catch (error) {\n // Both transports declined. That is the ordinary state on a machine\n // where the sink has never run, so it is reported as absence rather\n // than raised as a fault the page has to render as a crash.\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-sink',\n detail: error instanceof Error ? error.message : 'The log sink did not answer.',\n };\n return context.json(unavailable);\n }\n\n const body = toReport(report, source, requestedDimension);\n if (isEmpty(body)) {\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-data',\n detail: 'The log sink is running but has recorded no usage for this period.',\n };\n return context.json(unavailable);\n }\n return context.json(body);\n });\n\n /**\n * The detail behind the count the report carries. Separate from /metrics\n * because it costs a subprocess: the running daemon has no issues route, so\n * folding it into every report would make the whole page as slow as its\n * slowest transport.\n */\n app.get('/issues', async (context) => {\n const focus = context.req.query(METRICS_QUERY_PARAMS.focus);\n try {\n const report = await issues.query({\n limit: ISSUE_SAMPLE_LIMIT,\n ...(focus === undefined || focus === '' ? {} : { sessionId: focus }),\n });\n return context.json(report satisfies IssuesView);\n } catch (error) {\n const unavailable: MetricsUnavailable = {\n unavailable: 'no-sink',\n detail: error instanceof Error ? error.message : 'The log sink did not answer.',\n };\n return context.json(unavailable);\n }\n });\n\n return app;\n}\n\n/** The named export a host imports from this package's built hub entry. */\nexport const api: DoomApi = {\n basePath: LOG_API_BASE_PATH,\n start(_context: DoomApiContext): DoomApiHandler {\n const app = createLogHubApi();\n return {\n fetch: (request) => app.fetch(request),\n // The source holds no handle of its own: the HTTP transport is a fetch\n // per query and the CLI transport is a subprocess bounded by its timeout.\n close: () => undefined,\n };\n },\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:child_process"),n=require("node:path");n=e.__toESM(n,1);let r=require("node:module"),i=require("@agimon-ai/log-sink-mcp");function a(){let e=(0,r.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.default.join(n.default.dirname(e),`dist`,`cli.mjs`)}function o(e,n){return new Promise((r,i)=>{(0,t.execFile)(process.execPath,[a(),`logs`,`agent-issues`,`--limit`,String(e.limit),...e.sessionId===void 0?[]:[`--session-id`,e.sessionId],...n===void 0?[]:[`--db-path`,n]],{timeout:6e4,maxBuffer:33554432},(e,t,n)=>{if(e){i(Error(n.trim()||e.message));return}try{r(JSON.parse(t))}catch{i(Error(`The log sink returned output this version cannot read.`))}})})}function s(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function c(e){if(!s(e))return{};let t=Object.entries(e).flatMap(([e,t])=>typeof t==`number`?[[e,t]]:[]);return Object.fromEntries(t)}function l(e){return typeof e==`string`?e:``}function u(e){return typeof e==`string`&&e!==``?e:null}function d(e){return s(e)?[{fingerprint:l(e.fingerprint),occurrenceCount:typeof e.occurrenceCount==`number`?e.occurrenceCount:1,category:l(e.category),timestamp:l(e.timestamp),level:l(e.level),message:l(e.message),detail:u(e.detail)??l(e.message),tool:u(e.tool),errorType:u(e.errorType),agentName:u(e.agentName),model:u(e.model),statusCode:u(e.statusCode)}]:[]}function f(e){if(!s(e))throw Error(`The log sink returned output this version cannot read.`);let t=Array.isArray(e.issues)?e.issues.flatMap(d):[];return{totalIssues:typeof e.totalIssues==`number`?e.totalIssues:0,uniqueIncidents:typeof e.uniqueIncidents==`number`?e.uniqueIncidents:0,byCategory:c(e.byCategory),byTool:c(e.byTool),byErrorType:c(e.byErrorType),samples:t}}function p(e={}){let t=(0,i.resolveLogSinkInstance)({env:e.env,cwd:e.cwd,packageName:e.packageName,serviceName:e.serviceName}),n=e.cliRunner??(e=>o(e,t?.dbPath));return{async query(e){return f(await n(e))}}}exports.createIssuesSource=p;
|
|
2
|
+
//# sourceMappingURL=issuesSource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuesSource.cjs","names":["createRequire","path","resolveLogSinkInstance"],"sources":["../../../src/adapters/node/issuesSource.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\nimport { resolveLogSinkInstance } from '@agimon-ai/log-sink-mcp';\nimport type { AgentIssueSample, IssuesQueryParams, IssuesReport, IssuesSource } from '../../types/issuesSource.ts';\n\n/**\n * Agent-issue analysis over the sink's CLI.\n *\n * One transport, not two, because the daemon serves only /api/metrics and\n * /api/logs: there is no issues route to prefer. Every call is a subprocess,\n * which is why the page asks for this only when a reader opens the section\n * rather than alongside each report.\n */\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst CLI_TIMEOUT_MS = 60000;\n/** The analysis scans the whole window, so its output is larger than a metrics report. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\n\nexport interface IssuesSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n packageName?: string;\n serviceName?: string;\n /** Overrides the subprocess in tests. */\n cliRunner?: (params: IssuesQueryParams) => Promise<unknown>;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\nfunction runCli(params: IssuesQueryParams, dbPath: string | undefined): Promise<unknown> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'agent-issues',\n '--limit',\n String(params.limit),\n ...(params.sessionId === undefined ? [] : ['--session-id', params.sessionId]),\n ...(dbPath === undefined ? [] : ['--db-path', dbPath]),\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as unknown);\n } catch {\n reject(new Error('The log sink returned output this version cannot read.'));\n }\n },\n );\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction counts(value: unknown): Record<string, number> {\n if (!isRecord(value)) return {};\n const entries = Object.entries(value).flatMap(([key, count]) =>\n typeof count === 'number' ? [[key, count] as [string, number]] : [],\n );\n return Object.fromEntries(entries);\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction nullableText(value: unknown): string | null {\n return typeof value === 'string' && value !== '' ? value : null;\n}\n\nfunction toSample(value: unknown): AgentIssueSample[] {\n if (!isRecord(value)) return [];\n return [\n {\n fingerprint: text(value.fingerprint),\n occurrenceCount: typeof value.occurrenceCount === 'number' ? value.occurrenceCount : 1,\n category: text(value.category),\n timestamp: text(value.timestamp),\n level: text(value.level),\n message: text(value.message),\n // The sink leaves detail null when the message already is the detail.\n detail: nullableText(value.detail) ?? text(value.message),\n tool: nullableText(value.tool),\n errorType: nullableText(value.errorType),\n agentName: nullableText(value.agentName),\n model: nullableText(value.model),\n statusCode: nullableText(value.statusCode),\n },\n ];\n}\n\n/**\n * The CLI's report is a foreign process's JSON, so every field is narrowed\n * here rather than cast. A sink one version ahead may add fields, and a sink\n * one version behind may omit them; neither should throw.\n */\nfunction toReport(raw: unknown): IssuesReport {\n if (!isRecord(raw)) throw new Error('The log sink returned output this version cannot read.');\n const samples = Array.isArray(raw.issues) ? raw.issues.flatMap(toSample) : [];\n return {\n totalIssues: typeof raw.totalIssues === 'number' ? raw.totalIssues : 0,\n uniqueIncidents: typeof raw.uniqueIncidents === 'number' ? raw.uniqueIncidents : 0,\n byCategory: counts(raw.byCategory),\n byTool: counts(raw.byTool),\n byErrorType: counts(raw.byErrorType),\n samples,\n };\n}\n\nexport function createIssuesSource(options: IssuesSourceOptions = {}): IssuesSource {\n const instance = resolveLogSinkInstance({\n env: options.env,\n cwd: options.cwd,\n packageName: options.packageName,\n serviceName: options.serviceName,\n });\n const cliRunner = options.cliRunner ?? ((params: IssuesQueryParams) => runCli(params, instance?.dbPath));\n\n return {\n async query(params) {\n return toReport(await cliRunner(params));\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{execFile as t}from"node:child_process";import n from"node:path";import{resolveLogSinkInstance as r}from"@agimon-ai/log-sink-mcp";function i(){let t=e(import.meta.url).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.join(n.dirname(t),`dist`,`cli.mjs`)}function a(e,n){return new Promise((r,a)=>{t(process.execPath,[i(),`logs`,`agent-issues`,`--limit`,String(e.limit),...e.sessionId===void 0?[]:[`--session-id`,e.sessionId],...n===void 0?[]:[`--db-path`,n]],{timeout:6e4,maxBuffer:33554432},(e,t,n)=>{if(e){a(Error(n.trim()||e.message));return}try{r(JSON.parse(t))}catch{a(Error(`The log sink returned output this version cannot read.`))}})})}function o(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function s(e){if(!o(e))return{};let t=Object.entries(e).flatMap(([e,t])=>typeof t==`number`?[[e,t]]:[]);return Object.fromEntries(t)}function c(e){return typeof e==`string`?e:``}function l(e){return typeof e==`string`&&e!==``?e:null}function u(e){return o(e)?[{fingerprint:c(e.fingerprint),occurrenceCount:typeof e.occurrenceCount==`number`?e.occurrenceCount:1,category:c(e.category),timestamp:c(e.timestamp),level:c(e.level),message:c(e.message),detail:l(e.detail)??c(e.message),tool:l(e.tool),errorType:l(e.errorType),agentName:l(e.agentName),model:l(e.model),statusCode:l(e.statusCode)}]:[]}function d(e){if(!o(e))throw Error(`The log sink returned output this version cannot read.`);let t=Array.isArray(e.issues)?e.issues.flatMap(u):[];return{totalIssues:typeof e.totalIssues==`number`?e.totalIssues:0,uniqueIncidents:typeof e.uniqueIncidents==`number`?e.uniqueIncidents:0,byCategory:s(e.byCategory),byTool:s(e.byTool),byErrorType:s(e.byErrorType),samples:t}}function f(e={}){let t=r({env:e.env,cwd:e.cwd,packageName:e.packageName,serviceName:e.serviceName}),n=e.cliRunner??(e=>a(e,t?.dbPath));return{async query(e){return d(await n(e))}}}export{f as createIssuesSource};
|
|
2
|
+
//# sourceMappingURL=issuesSource.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issuesSource.mjs","names":[],"sources":["../../../src/adapters/node/issuesSource.ts"],"sourcesContent":["import { execFile } from 'node:child_process';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\nimport { resolveLogSinkInstance } from '@agimon-ai/log-sink-mcp';\nimport type { AgentIssueSample, IssuesQueryParams, IssuesReport, IssuesSource } from '../../types/issuesSource.ts';\n\n/**\n * Agent-issue analysis over the sink's CLI.\n *\n * One transport, not two, because the daemon serves only /api/metrics and\n * /api/logs: there is no issues route to prefer. Every call is a subprocess,\n * which is why the page asks for this only when a reader opens the section\n * rather than alongside each report.\n */\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst CLI_TIMEOUT_MS = 60000;\n/** The analysis scans the whole window, so its output is larger than a metrics report. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\n\nexport interface IssuesSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n packageName?: string;\n serviceName?: string;\n /** Overrides the subprocess in tests. */\n cliRunner?: (params: IssuesQueryParams) => Promise<unknown>;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\nfunction runCli(params: IssuesQueryParams, dbPath: string | undefined): Promise<unknown> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'agent-issues',\n '--limit',\n String(params.limit),\n ...(params.sessionId === undefined ? [] : ['--session-id', params.sessionId]),\n ...(dbPath === undefined ? [] : ['--db-path', dbPath]),\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as unknown);\n } catch {\n reject(new Error('The log sink returned output this version cannot read.'));\n }\n },\n );\n });\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction counts(value: unknown): Record<string, number> {\n if (!isRecord(value)) return {};\n const entries = Object.entries(value).flatMap(([key, count]) =>\n typeof count === 'number' ? [[key, count] as [string, number]] : [],\n );\n return Object.fromEntries(entries);\n}\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value : '';\n}\n\nfunction nullableText(value: unknown): string | null {\n return typeof value === 'string' && value !== '' ? value : null;\n}\n\nfunction toSample(value: unknown): AgentIssueSample[] {\n if (!isRecord(value)) return [];\n return [\n {\n fingerprint: text(value.fingerprint),\n occurrenceCount: typeof value.occurrenceCount === 'number' ? value.occurrenceCount : 1,\n category: text(value.category),\n timestamp: text(value.timestamp),\n level: text(value.level),\n message: text(value.message),\n // The sink leaves detail null when the message already is the detail.\n detail: nullableText(value.detail) ?? text(value.message),\n tool: nullableText(value.tool),\n errorType: nullableText(value.errorType),\n agentName: nullableText(value.agentName),\n model: nullableText(value.model),\n statusCode: nullableText(value.statusCode),\n },\n ];\n}\n\n/**\n * The CLI's report is a foreign process's JSON, so every field is narrowed\n * here rather than cast. A sink one version ahead may add fields, and a sink\n * one version behind may omit them; neither should throw.\n */\nfunction toReport(raw: unknown): IssuesReport {\n if (!isRecord(raw)) throw new Error('The log sink returned output this version cannot read.');\n const samples = Array.isArray(raw.issues) ? raw.issues.flatMap(toSample) : [];\n return {\n totalIssues: typeof raw.totalIssues === 'number' ? raw.totalIssues : 0,\n uniqueIncidents: typeof raw.uniqueIncidents === 'number' ? raw.uniqueIncidents : 0,\n byCategory: counts(raw.byCategory),\n byTool: counts(raw.byTool),\n byErrorType: counts(raw.byErrorType),\n samples,\n };\n}\n\nexport function createIssuesSource(options: IssuesSourceOptions = {}): IssuesSource {\n const instance = resolveLogSinkInstance({\n env: options.env,\n cwd: options.cwd,\n packageName: options.packageName,\n serviceName: options.serviceName,\n });\n const cliRunner = options.cliRunner ?? ((params: IssuesQueryParams) => runCli(params, instance?.dbPath));\n\n return {\n async query(params) {\n return toReport(await cliRunner(params));\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs");let t=require("node:child_process"),n=require("node:path");n=e.__toESM(n,1);let r=require("node:module"),i=require("@agimon-ai/log-sink-mcp");const a=`total-tokens`;function o(){let e=(0,r.createRequire)(require("url").pathToFileURL(__filename).href).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.default.join(n.default.dirname(e),`dist`,`cli.mjs`)}const s={sessionId:`sessionId`,agentName:`agentName`,model:`model`,provider:`provider`},c={sessionId:`--session-id`,agentName:`--agent-name`,model:`--model`,provider:`--provider`};function l(e){let t=e.filter;return t===void 0?[]:Object.keys(s).flatMap(e=>{let n=t[e];return n===void 0||n===``?[]:[[e,n]]})}function u(e){let t=new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:a,limit:String(e.limit),toolLimit:String(e.toolLimit??1)});for(let[n,r]of l(e))t.set(s[n],r);return t.toString()}async function d(e,t,n){let r=await n(`${e}/api/metrics?${u(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function f(e,n){return new Promise((r,i)=>{(0,t.execFile)(process.execPath,[o(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,a,`--limit`,String(e.limit),`--tool-limit`,String(e.toolLimit??1),...l(e).flatMap(([e,t])=>[c[e],t]),...n===void 0?[]:[`--db-path`,n]],{timeout:3e4,maxBuffer:33554432},(e,t,n)=>{if(e){i(Error(n.trim()||e.message));return}try{r(JSON.parse(t))}catch{i(Error(`metrics CLI returned unparsable output`))}})})}function p(e){try{let t=(0,i.resolveLogSinkInstance)(e);return{scope:t.scope,dbPath:t.dbPath,...t.registeredName===void 0?{}:{registeredName:t.registeredName}}}catch{return}}function m(e={}){let t=e.fetchImpl??globalThis.fetch,n={env:e.env,cwd:e.cwd,packageName:e.packageName,serviceName:e.serviceName},r,a,o=p(n),s=e.cliRunner??(e=>f(e,o?.dbPath)),c=async()=>{if(a!==void 0)return a??void 0;try{a=(await(0,i.resolveLogSinkPort)({...n,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>r,instance:()=>o,async query(e){let n=await c();if(n&&t)try{let i=await d(n,e,t);return r=`http`,i}catch{a=null}let i=await s(e);return r=`cli`,i}}}exports.createMetricsSource=m;
|
|
2
|
+
//# sourceMappingURL=metricsSource.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.cjs","names":["createRequire","path","resolveLogSinkInstance","resolveLogSinkPort"],"sources":["../../../src/adapters/node/metricsSource.ts"],"sourcesContent":["/**\n * Historical metrics access for the `SPC h l` overlay.\n *\n * Two transports, in order: the running sink's HTTP API, then the installed\n * CLI as a subprocess. The sink daemon is long-lived and often lags the\n * installed package, so a daemon that rejects newer query parameters is treated\n * as unavailable rather than fatal. The reader exported by log-sink-mcp is\n * deliberately not used in-process: better-sqlite3 is synchronous and a\n * one-day window blocks for seconds, which would freeze the TUI.\n *\n * Both transports resolve the sink instance from the same identity the Pi\n * telemetry extension writes under. Without it log-sink falls back to\n * `npm_package_name`, which a running Pi process does not set, so the reader\n * would land on the local instance while the writer exported to the global one\n * and the panel would report an empty history that no session ever filled.\n */\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type { LogMetricsReport } from '@agimon-ai/log-sink-mcp';\nimport { resolveLogSinkInstance, resolveLogSinkPort } from '@agimon-ai/log-sink-mcp';\nimport type {\n MetricsInstance,\n MetricsQueryParams,\n MetricsSource,\n MetricsTransport,\n} from '../../types/metricsSource.ts';\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst HTTP_TIMEOUT_MS = 5000;\nconst CLI_TIMEOUT_MS = 30000;\n/** A day of dev telemetry is already ~140k records; keep the payload bounded. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\n/**\n * The overlay ranks one tool, but a page pairing tool calls against failure\n * counts needs a denominator for every tool that failed, so callers ask.\n */\nconst DEFAULT_TOOL_LIMIT = 1;\n/** The panel ranks consumers, so ask for the biggest rather than the most recent. */\nconst TOKEN_SORT = 'total-tokens';\n\n/** The identity both transports resolve their sink instance from. */\ninterface ResolveIdentity {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n packageName?: string;\n serviceName?: string;\n}\n\nexport interface MetricsSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n /** Identity the sink is registered under; must match the telemetry writer's. */\n packageName?: string;\n serviceName?: string;\n /** Overrides transport discovery in tests. */\n fetchImpl?: typeof fetch;\n cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n // The published bin is CJS; the ESM build sits beside it and starts faster.\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\n/**\n * The sink names these the same way on the wire and on the command line, apart\n * from the case convention, so the two transports stay in step.\n */\nconst FILTER_QUERY_KEYS = {\n sessionId: 'sessionId',\n agentName: 'agentName',\n model: 'model',\n provider: 'provider',\n} as const;\n\nconst FILTER_CLI_FLAGS = {\n sessionId: '--session-id',\n agentName: '--agent-name',\n model: '--model',\n provider: '--provider',\n} as const;\n\nfunction filterEntries(params: MetricsQueryParams): [keyof typeof FILTER_QUERY_KEYS, string][] {\n const filter = params.filter;\n if (filter === undefined) return [];\n return (Object.keys(FILTER_QUERY_KEYS) as (keyof typeof FILTER_QUERY_KEYS)[]).flatMap((key) => {\n const value = filter[key];\n return value === undefined || value === '' ? [] : [[key, value] as [keyof typeof FILTER_QUERY_KEYS, string]];\n });\n}\n\nfunction searchParams(params: MetricsQueryParams): string {\n const search = new URLSearchParams({\n groupBy: params.groupBy,\n period: params.period,\n sort: TOKEN_SORT,\n limit: String(params.limit),\n toolLimit: String(params.toolLimit ?? DEFAULT_TOOL_LIMIT),\n });\n for (const [key, value] of filterEntries(params)) search.set(FILTER_QUERY_KEYS[key], value);\n return search.toString();\n}\n\nasync function queryHttp(\n endpoint: string,\n params: MetricsQueryParams,\n fetchImpl: typeof fetch,\n): Promise<LogMetricsReport> {\n const response = await fetchImpl(`${endpoint}/api/metrics?${searchParams(params)}`, {\n signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),\n });\n if (!response.ok) throw new Error(`sink responded ${response.status}`);\n\n const body = (await response.json()) as LogMetricsReport & { error?: string };\n // An older daemon rejects unknown group-by values and omits the timeline.\n if (body.error || !Array.isArray(body.timeline)) throw new Error(body.error ?? 'sink is an older version');\n return body;\n}\n\n/**\n * The subprocess resolves an instance of its own from the inherited cwd and\n * environment, so the database is passed explicitly rather than left to agree\n * with this process by coincidence.\n */\nfunction runCli(params: MetricsQueryParams, dbPath: string | undefined): Promise<LogMetricsReport> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'metrics',\n '--group-by',\n params.groupBy,\n '--period',\n params.period,\n '--sort',\n TOKEN_SORT,\n '--limit',\n String(params.limit),\n '--tool-limit',\n String(params.toolLimit ?? DEFAULT_TOOL_LIMIT),\n ...filterEntries(params).flatMap(([key, value]) => [FILTER_CLI_FLAGS[key], value]),\n ...(dbPath === undefined ? [] : ['--db-path', dbPath]),\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as LogMetricsReport);\n } catch {\n reject(new Error('metrics CLI returned unparsable output'));\n }\n },\n );\n });\n}\n\n/**\n * A malformed `.logsink.yaml` must not take the overlay down with it. Both\n * transports still run when resolution fails; they fall back to log-sink's own\n * resolution and the panel reports an unknown instance.\n */\nfunction resolveInstance(identity: ResolveIdentity): MetricsInstance | undefined {\n try {\n const resolved = resolveLogSinkInstance(identity);\n return {\n scope: resolved.scope,\n dbPath: resolved.dbPath,\n ...(resolved.registeredName === undefined ? {} : { registeredName: resolved.registeredName }),\n };\n } catch {\n return undefined;\n }\n}\n\nexport function createMetricsSource(options: MetricsSourceOptions = {}): MetricsSource {\n const fetchImpl = options.fetchImpl ?? globalThis.fetch;\n const identity = {\n env: options.env,\n cwd: options.cwd,\n packageName: options.packageName,\n serviceName: options.serviceName,\n };\n let transport: MetricsTransport | undefined;\n let endpoint: string | undefined | null;\n const instance = resolveInstance(identity);\n const cliRunner = options.cliRunner ?? ((params: MetricsQueryParams) => runCli(params, instance?.dbPath));\n\n const resolveEndpoint = async (): Promise<string | undefined> => {\n if (endpoint !== undefined) return endpoint ?? undefined;\n try {\n const resolved = await resolveLogSinkPort({ ...identity, healthCheck: true });\n endpoint = resolved?.endpoint ?? null;\n } catch {\n endpoint = null;\n }\n return endpoint ?? undefined;\n };\n\n return {\n lastTransport: () => transport,\n instance: () => instance,\n async query(params) {\n const httpEndpoint = await resolveEndpoint();\n if (httpEndpoint && fetchImpl) {\n try {\n const report = await queryHttp(httpEndpoint, params, fetchImpl);\n transport = 'http';\n return report;\n } catch {\n // A stale or unhealthy daemon must not hide data the CLI can still read.\n endpoint = null;\n }\n }\n\n const report = await cliRunner(params);\n transport = 'cli';\n return report;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MetricsQueryParams, MetricsSource } from "../../types/metricsSource.cjs";
|
|
2
|
+
import { LogMetricsReport } from "@agimon-ai/log-sink-mcp";
|
|
3
|
+
//#region src/adapters/node/metricsSource.d.ts
|
|
4
|
+
export interface MetricsSourceOptions {
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** Identity the sink is registered under; must match the telemetry writer's. */
|
|
8
|
+
packageName?: string;
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
/** Overrides transport discovery in tests. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
|
|
15
|
+
//#endregion
|
|
16
|
+
//# sourceMappingURL=metricsSource.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.d.cts","names":[],"sources":["../../../src/adapters/node/metricsSource.ts"],"mappings":";;;iBAiDiB;EACf,MAAM,OAAO;EACb;;EAEA;EACA;;EAEA,mBAAmB;EACnB,aAAa,QAAQ,uBAAuB,QAAQ;;wBA6HtC,oBAAoB,UAAS,uBAA4B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MetricsQueryParams, MetricsSource } from "../../types/metricsSource.mjs";
|
|
2
|
+
import { LogMetricsReport } from "@agimon-ai/log-sink-mcp";
|
|
3
|
+
//#region src/adapters/node/metricsSource.d.ts
|
|
4
|
+
export interface MetricsSourceOptions {
|
|
5
|
+
env?: NodeJS.ProcessEnv;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** Identity the sink is registered under; must match the telemetry writer's. */
|
|
8
|
+
packageName?: string;
|
|
9
|
+
serviceName?: string;
|
|
10
|
+
/** Overrides transport discovery in tests. */
|
|
11
|
+
fetchImpl?: typeof fetch;
|
|
12
|
+
cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createMetricsSource(options?: MetricsSourceOptions): MetricsSource;
|
|
15
|
+
//#endregion
|
|
16
|
+
//# sourceMappingURL=metricsSource.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.d.mts","names":[],"sources":["../../../src/adapters/node/metricsSource.ts"],"mappings":";;;iBAiDiB;EACf,MAAM,OAAO;EACb;;EAEA;EACA;;EAEA,mBAAmB;EACnB,aAAa,QAAQ,uBAAuB,QAAQ;;wBA6HtC,oBAAoB,UAAS,uBAA4B"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{createRequire as e}from"node:module";import{execFile as t}from"node:child_process";import n from"node:path";import{resolveLogSinkInstance as r,resolveLogSinkPort as i}from"@agimon-ai/log-sink-mcp";const a=`total-tokens`;function o(){let t=e(import.meta.url).resolve(`@agimon-ai/log-sink-mcp/package.json`);return n.join(n.dirname(t),`dist`,`cli.mjs`)}const s={sessionId:`sessionId`,agentName:`agentName`,model:`model`,provider:`provider`},c={sessionId:`--session-id`,agentName:`--agent-name`,model:`--model`,provider:`--provider`};function l(e){let t=e.filter;return t===void 0?[]:Object.keys(s).flatMap(e=>{let n=t[e];return n===void 0||n===``?[]:[[e,n]]})}function u(e){let t=new URLSearchParams({groupBy:e.groupBy,period:e.period,sort:a,limit:String(e.limit),toolLimit:String(e.toolLimit??1)});for(let[n,r]of l(e))t.set(s[n],r);return t.toString()}async function d(e,t,n){let r=await n(`${e}/api/metrics?${u(t)}`,{signal:AbortSignal.timeout(5e3)});if(!r.ok)throw Error(`sink responded ${r.status}`);let i=await r.json();if(i.error||!Array.isArray(i.timeline))throw Error(i.error??`sink is an older version`);return i}function f(e,n){return new Promise((r,i)=>{t(process.execPath,[o(),`logs`,`metrics`,`--group-by`,e.groupBy,`--period`,e.period,`--sort`,a,`--limit`,String(e.limit),`--tool-limit`,String(e.toolLimit??1),...l(e).flatMap(([e,t])=>[c[e],t]),...n===void 0?[]:[`--db-path`,n]],{timeout:3e4,maxBuffer:33554432},(e,t,n)=>{if(e){i(Error(n.trim()||e.message));return}try{r(JSON.parse(t))}catch{i(Error(`metrics CLI returned unparsable output`))}})})}function p(e){try{let t=r(e);return{scope:t.scope,dbPath:t.dbPath,...t.registeredName===void 0?{}:{registeredName:t.registeredName}}}catch{return}}function m(e={}){let t=e.fetchImpl??globalThis.fetch,n={env:e.env,cwd:e.cwd,packageName:e.packageName,serviceName:e.serviceName},r,a,o=p(n),s=e.cliRunner??(e=>f(e,o?.dbPath)),c=async()=>{if(a!==void 0)return a??void 0;try{a=(await i({...n,healthCheck:!0}))?.endpoint??null}catch{a=null}return a??void 0};return{lastTransport:()=>r,instance:()=>o,async query(e){let n=await c();if(n&&t)try{let i=await d(n,e,t);return r=`http`,i}catch{a=null}let i=await s(e);return r=`cli`,i}}}export{m as createMetricsSource};
|
|
2
|
+
//# sourceMappingURL=metricsSource.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metricsSource.mjs","names":[],"sources":["../../../src/adapters/node/metricsSource.ts"],"sourcesContent":["/**\n * Historical metrics access for the `SPC h l` overlay.\n *\n * Two transports, in order: the running sink's HTTP API, then the installed\n * CLI as a subprocess. The sink daemon is long-lived and often lags the\n * installed package, so a daemon that rejects newer query parameters is treated\n * as unavailable rather than fatal. The reader exported by log-sink-mcp is\n * deliberately not used in-process: better-sqlite3 is synchronous and a\n * one-day window blocks for seconds, which would freeze the TUI.\n *\n * Both transports resolve the sink instance from the same identity the Pi\n * telemetry extension writes under. Without it log-sink falls back to\n * `npm_package_name`, which a running Pi process does not set, so the reader\n * would land on the local instance while the writer exported to the global one\n * and the panel would report an empty history that no session ever filled.\n */\nimport { execFile } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type { LogMetricsReport } from '@agimon-ai/log-sink-mcp';\nimport { resolveLogSinkInstance, resolveLogSinkPort } from '@agimon-ai/log-sink-mcp';\nimport type {\n MetricsInstance,\n MetricsQueryParams,\n MetricsSource,\n MetricsTransport,\n} from '../../types/metricsSource.ts';\n\nconst PACKAGE_NAME = '@agimon-ai/log-sink-mcp';\nconst HTTP_TIMEOUT_MS = 5000;\nconst CLI_TIMEOUT_MS = 30000;\n/** A day of dev telemetry is already ~140k records; keep the payload bounded. */\nconst MAX_CLI_BUFFER = 32 * 1024 * 1024;\n/**\n * The overlay ranks one tool, but a page pairing tool calls against failure\n * counts needs a denominator for every tool that failed, so callers ask.\n */\nconst DEFAULT_TOOL_LIMIT = 1;\n/** The panel ranks consumers, so ask for the biggest rather than the most recent. */\nconst TOKEN_SORT = 'total-tokens';\n\n/** The identity both transports resolve their sink instance from. */\ninterface ResolveIdentity {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n packageName?: string;\n serviceName?: string;\n}\n\nexport interface MetricsSourceOptions {\n env?: NodeJS.ProcessEnv;\n cwd?: string;\n /** Identity the sink is registered under; must match the telemetry writer's. */\n packageName?: string;\n serviceName?: string;\n /** Overrides transport discovery in tests. */\n fetchImpl?: typeof fetch;\n cliRunner?: (params: MetricsQueryParams) => Promise<LogMetricsReport>;\n}\n\nfunction cliEntryPoint(): string {\n const require = createRequire(import.meta.url);\n // The published bin is CJS; the ESM build sits beside it and starts faster.\n const packageJson = require.resolve(`${PACKAGE_NAME}/package.json`);\n return path.join(path.dirname(packageJson), 'dist', 'cli.mjs');\n}\n\n/**\n * The sink names these the same way on the wire and on the command line, apart\n * from the case convention, so the two transports stay in step.\n */\nconst FILTER_QUERY_KEYS = {\n sessionId: 'sessionId',\n agentName: 'agentName',\n model: 'model',\n provider: 'provider',\n} as const;\n\nconst FILTER_CLI_FLAGS = {\n sessionId: '--session-id',\n agentName: '--agent-name',\n model: '--model',\n provider: '--provider',\n} as const;\n\nfunction filterEntries(params: MetricsQueryParams): [keyof typeof FILTER_QUERY_KEYS, string][] {\n const filter = params.filter;\n if (filter === undefined) return [];\n return (Object.keys(FILTER_QUERY_KEYS) as (keyof typeof FILTER_QUERY_KEYS)[]).flatMap((key) => {\n const value = filter[key];\n return value === undefined || value === '' ? [] : [[key, value] as [keyof typeof FILTER_QUERY_KEYS, string]];\n });\n}\n\nfunction searchParams(params: MetricsQueryParams): string {\n const search = new URLSearchParams({\n groupBy: params.groupBy,\n period: params.period,\n sort: TOKEN_SORT,\n limit: String(params.limit),\n toolLimit: String(params.toolLimit ?? DEFAULT_TOOL_LIMIT),\n });\n for (const [key, value] of filterEntries(params)) search.set(FILTER_QUERY_KEYS[key], value);\n return search.toString();\n}\n\nasync function queryHttp(\n endpoint: string,\n params: MetricsQueryParams,\n fetchImpl: typeof fetch,\n): Promise<LogMetricsReport> {\n const response = await fetchImpl(`${endpoint}/api/metrics?${searchParams(params)}`, {\n signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),\n });\n if (!response.ok) throw new Error(`sink responded ${response.status}`);\n\n const body = (await response.json()) as LogMetricsReport & { error?: string };\n // An older daemon rejects unknown group-by values and omits the timeline.\n if (body.error || !Array.isArray(body.timeline)) throw new Error(body.error ?? 'sink is an older version');\n return body;\n}\n\n/**\n * The subprocess resolves an instance of its own from the inherited cwd and\n * environment, so the database is passed explicitly rather than left to agree\n * with this process by coincidence.\n */\nfunction runCli(params: MetricsQueryParams, dbPath: string | undefined): Promise<LogMetricsReport> {\n return new Promise((resolve, reject) => {\n execFile(\n process.execPath,\n [\n cliEntryPoint(),\n 'logs',\n 'metrics',\n '--group-by',\n params.groupBy,\n '--period',\n params.period,\n '--sort',\n TOKEN_SORT,\n '--limit',\n String(params.limit),\n '--tool-limit',\n String(params.toolLimit ?? DEFAULT_TOOL_LIMIT),\n ...filterEntries(params).flatMap(([key, value]) => [FILTER_CLI_FLAGS[key], value]),\n ...(dbPath === undefined ? [] : ['--db-path', dbPath]),\n ],\n { timeout: CLI_TIMEOUT_MS, maxBuffer: MAX_CLI_BUFFER },\n (error, stdout, stderr) => {\n if (error) {\n reject(new Error(stderr.trim() || error.message));\n return;\n }\n try {\n resolve(JSON.parse(stdout) as LogMetricsReport);\n } catch {\n reject(new Error('metrics CLI returned unparsable output'));\n }\n },\n );\n });\n}\n\n/**\n * A malformed `.logsink.yaml` must not take the overlay down with it. Both\n * transports still run when resolution fails; they fall back to log-sink's own\n * resolution and the panel reports an unknown instance.\n */\nfunction resolveInstance(identity: ResolveIdentity): MetricsInstance | undefined {\n try {\n const resolved = resolveLogSinkInstance(identity);\n return {\n scope: resolved.scope,\n dbPath: resolved.dbPath,\n ...(resolved.registeredName === undefined ? {} : { registeredName: resolved.registeredName }),\n };\n } catch {\n return undefined;\n }\n}\n\nexport function createMetricsSource(options: MetricsSourceOptions = {}): MetricsSource {\n const fetchImpl = options.fetchImpl ?? globalThis.fetch;\n const identity = {\n env: options.env,\n cwd: options.cwd,\n packageName: options.packageName,\n serviceName: options.serviceName,\n };\n let transport: MetricsTransport | undefined;\n let endpoint: string | undefined | null;\n const instance = resolveInstance(identity);\n const cliRunner = options.cliRunner ?? ((params: MetricsQueryParams) => runCli(params, instance?.dbPath));\n\n const resolveEndpoint = async (): Promise<string | undefined> => {\n if (endpoint !== undefined) return endpoint ?? undefined;\n try {\n const resolved = await resolveLogSinkPort({ ...identity, healthCheck: true });\n endpoint = resolved?.endpoint ?? null;\n } catch {\n endpoint = null;\n }\n return endpoint ?? undefined;\n };\n\n return {\n lastTransport: () => transport,\n instance: () => instance,\n async query(params) {\n const httpEndpoint = await resolveEndpoint();\n if (httpEndpoint && fetchImpl) {\n try {\n const report = await queryHttp(httpEndpoint, params, fetchImpl);\n transport = 'http';\n return report;\n } catch {\n // A stale or unhealthy daemon must not hide data the CLI can still read.\n endpoint = null;\n }\n }\n\n const report = await cliRunner(params);\n transport = 'cli';\n return report;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("../node/metricsSource.cjs"),t=require("../../services/metrics.cjs"),n=require("../../tui/logMetricsOverlay.cjs");let r=require("@agimon-ai/doompi-extension-contracts/cordis-host"),i=require("@agimon-ai/doompi-extension-contracts/readiness"),a=require("@agimon-ai/doompi-extension-contracts/ui-hub"),o=require("@agimon-ai/doompi-telemetry");const s=`@agimon-ai/doompi-log`,c=`log-metrics`,l=new Set([`error`,`aborted`]);function u(e){return e===`1`||e?.toLowerCase()===`true`||e?.toLowerCase()===`yes`}function d(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 f(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 p(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 m(e){return{service:e.serviceName,backend:e.backend,endpoint:e.endpoint??`none`,endpointSource:e.endpointSource,traces:e.traces,redaction:!0,fileFallback:e.fileFallback}}function h(e,t,n={},r=!1){let a=n.env??process.env,c=n.metrics,h=new Map,g=!0,_=0,v,y,b,x,S=Promise.resolve(),C=e=>{S=S.then(e).catch(()=>void 0)};e.effect(()=>async()=>{g=!1,_+=1,x=void 0,h.clear(),v=void 0;let e=y;y=void 0,b=void 0,await e?.shutdown()},`${s}/telemetry`);let w=async(e,t,n)=>{g&&y&&(e===`debug`?await y.recordDebug(t,n):e===`warn`?await y.recordWarning(t,void 0,n):e===`error`?await y.recordError(t,void 0,n):await y.recordEvent(t,n))},T=e=>{if(!g)throw Error(`Log telemetry runtime is disposed.`);let t=e.sessionManager.getSessionId();if(y&&b!==t){let e=y;y=void 0,b=void 0,C(()=>e.shutdown())}return y||=(b=t,(0,o.createDoomTelemetry)({serviceName:n.serviceName??`pi`,packageName:s,cwd:e.cwd,env:{...a,PI_SESSION_ID:t},allowFileFallback:n.allowFileFallback,telemetryFactory:n.telemetryFactory,warn:n.onDiagnostic,enableLogs:!0,enableTraces:a.AGENT_OTEL_TRACES===void 0||u(a.AGENT_OTEL_TRACES),onRecord:e=>{g&&c?.record(e.event,e.attributes)},onStatus:e=>{g&&n.onSinkStatus?.(m(e))}})),g&&n.onSinkStatus?.(m(y.status())),y},E=async e=>{let t=x;if(t){if(t.sessionManager!==e.sessionManager)throw Error(`Log readiness belongs to a stale Pi session.`);if(await t.operation,!g||t!==x)throw Error(`Log readiness belongs to a stale extension generation.`)}},D=(e,t)=>{let n=x,i=async()=>{if(n){if(!await n.operation.then(()=>!0,()=>!1)||!g||n!==x||n.sessionManager!==e.sessionManager)return}else if(!g)return;await t()};if(!r)return i();C(i)};return t.on(`session_start`,(t,n)=>{if(!g)return;let a=++_;h.clear(),v=void 0;let o=async e=>{e?.throwIfAborted(),T(n),await w(`info`,`pi.session.started`,{...d(n),"pi.session.reason":t.reason,"pi.session.previous":t.previousSessionFile!==void 0}),e?.throwIfAborted()},c=r?(0,i.readDoomReadinessCoordinator)(e):void 0;if(!c)return o();let l=x,u=(async()=>{l?.coordinator===c&&await l.operation.catch(()=>void 0),g&&a===_&&await c.start(s,`${n.sessionManager.getSessionId()}:${a}`,async e=>(await o(e),{value:void 0})).wait()})();u.catch(()=>void 0),x={sessionManager:n.sessionManager,coordinator:c,operation:u}}),t.on(`before_agent_start`,(e,t)=>{if(g)return D(t,async()=>{T(t),await w(`info`,`pi.user_prompt`,{...d(t),"pi.user_message.length":e.prompt.length,"pi.user_message.image_count":e.images?.length??0})})}),t.on(`agent_start`,(e,t)=>{if(g)return D(t,async()=>{v=Date.now(),T(t),await w(`info`,`pi.agent.started`,d(t))})}),t.on(`turn_start`,(e,t)=>{if(g)return D(t,async()=>{T(t),await w(`debug`,`pi.turn.started`,{...d(t),"pi.turn.index":e.turnIndex})})}),t.on(`turn_end`,(e,t)=>{if(g)return D(t,async()=>{T(t);let n=f(e.message);if(await w(`info`,`pi.turn.finished`,{...d(t),"pi.turn.index":e.turnIndex,"pi.tool_result.count":e.toolResults.length,...n}),e.message.role===`assistant`&&l.has(e.message.stopReason)&&await w(e.message.stopReason===`aborted`?`warn`:`error`,`pi.turn.failed`,{...d(t),"pi.turn.index":e.turnIndex,...p(e.message),...n}),n)for(let r of e.toolResults)await w(`debug`,`pi.tool_token_sample`,{...d(t),...n,"tool.name":r.toolName,token_attribution:`toolCallTurn`})})}),t.on(`tool_execution_start`,(e,t)=>{if(g)return D(t,async()=>{h.set(e.toolCallId,Date.now()),T(t),await w(`debug`,`pi.tool_call`,{...d(t),"tool.name":e.toolName,"tool.call.id":e.toolCallId})})}),t.on(`tool_execution_end`,(e,t)=>{if(g)return D(t,async()=>{let n=h.get(e.toolCallId);h.delete(e.toolCallId),T(t),await w(`info`,`pi.tool_result`,{...d(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`,(e,t)=>{if(g)return D(t,async()=>{T(t);let n={...d(t),"http.response.status_code":e.status};e.status>=400?await w(`error`,`pi.api_error`,n):await w(`debug`,`pi.api_response`,n)})}),t.on(`model_select`,(e,t)=>{if(g)return D(t,async()=>{T(t),await w(`info`,`pi.model.selected`,{...d(t),"pi.model.source":e.source,"gen_ai.provider.name":e.model.provider,"gen_ai.request.model":e.model.id})})}),t.on(`agent_end`,(e,t)=>{if(!g)return;let n=_;return D(t,async()=>{T(t),await w(`info`,`pi.agent.finished`,{...d(t),"pi.message.count":e.messages.length,...v===void 0?{}:{"pi.agent.duration_ms":Date.now()-v}}),g&&n===_&&(v=void 0,await y?.flush())})}),t.on(`agent_settled`,(e,t)=>{if(g)return D(t,async()=>{T(t),await w(`debug`,`pi.agent.settled`,d(t))})}),{waitForSession:E,async finishSession(e,t){if(!g)return;await S;let n=x;n&&await n.operation.catch(()=>void 0),g&&(n&&n!==x||y&&(T(t),await w(`info`,`pi.session.finished`,{...d(t),"pi.session.reason":e})))}}}function g(e,t={}){let n=e.registerLeader({source:t.source??`@agimon-ai/doompi-log`,bindings:[{id:`log.metrics`,path:[{key:`h`,label:`help`,detail:`package docs and logs`,order:70},{key:`l`,label:`logs`,detail:`telemetry`}],command:{name:c}}]});return()=>n.dispose()}async function _(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 v(n,r,i={},o=!1){let l=i.env??process.env,d=new t.LogMetricsAggregator,f=i.metricsSource,p=t=>f??=e.createMetricsSource({env:l,cwd:t.cwd,packageName:s,serviceName:i.serviceName??`pi`}),m=!0,v,y;n.effect(()=>()=>{m=!1,v=void 0,y=void 0},`${s}/runtime`);let b=h(n,r,{...i,metrics:d,onDiagnostic:e=>{m&&(y=e,i.onDiagnostic?i.onDiagnostic(e):l.AGENT_OTEL_DIAGNOSTICS===`stderr`&&process.emitWarning(e))},onSinkStatus:e=>{m&&(v=e,i.onSinkStatus?.(e))}},o);return n.inject([a.DOOM_UI_HUB_SERVICE],e=>g((0,a.requireDoomUiHub)(e),{source:i.leaderSource})),r.registerCommand(c,{description:`Show session-local log metrics`,handler:async(e,t)=>{if(!m||(await b.waitForSession(t),!m))return;let n=p(t);await _(t,()=>({disabled:u(l.AGENT_TELEMETRY_DISABLED)||u(l.OTEL_SDK_DISABLED),snapshot:d.snapshot(),sink:v,transport:n.lastTransport(),instance:n.instance?.(),lastDiagnostic:y}),n.query)}}),b}function y(e,{pi:t}){let n=v(e,t,{},!0),r;t.on(`session_shutdown`,(e,t)=>(r??=n.finishSession(e.reason,t),r))}async function b(e){let t=await(0,r.connectDoomCordisHost)(e,s),n=t.root.plugin(y,{pi:e});try{await n}catch(e){try{await n.dispose()}finally{await t.dispose()}throw e}let i;e.on(`session_shutdown`,()=>i??=(async()=>{try{await n.dispose()}finally{await t.dispose()}})())}exports.doomLogExtension=b,exports.installDoomLogRuntime=v,exports.installPiTelemetryRuntime=h,exports.openLogMetricsOverlay=_,exports.registerLogMetricsLeaderBinding=g;
|
|
2
|
+
//# sourceMappingURL=extension.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.cjs","names":["createDoomTelemetry","readDoomReadinessCoordinator","LogMetricsOverlayComponent","LogMetricsAggregator","createMetricsSource","DOOM_UI_HUB_SERVICE","requireDoomUiHub","connectDoomCordisHost"],"sources":["../../../src/adapters/pi/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 */\n\nimport { connectDoomCordisHost } from '@agimon-ai/doompi-extension-contracts/cordis-host';\nimport {\n type DoomReadinessCoordinator,\n readDoomReadinessCoordinator,\n} from '@agimon-ai/doompi-extension-contracts/readiness';\nimport {\n DOOM_UI_HUB_SERVICE,\n type DoomUiHubService,\n requireDoomUiHub,\n} from '@agimon-ai/doompi-extension-contracts/ui-hub';\nimport { createDoomTelemetry, type DoomTelemetry, type DoomTelemetryOptions } from '@agimon-ai/doompi-telemetry';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext, TurnEndEvent } from '@earendil-works/pi-coding-agent';\nimport { LogMetricsAggregator, type LogMetricsRecorder } from '../../services/metrics.ts';\nimport { LogMetricsOverlayComponent, type LogMetricsView, type SinkStatus } from '../../tui/logMetricsOverlay.ts';\nimport type { MetricsQuery, MetricsSource } from '../../types/metricsSource.ts';\nimport { createMetricsSource } from '../node/metricsSource.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;\n/** Must match the Help group published by doompi-ui, or the registry drops this. */\nconst HELP_GROUP_DETAIL = 'package docs and logs';\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\nexport interface PiTelemetryRuntimeHandle {\n waitForSession(ctx: ExtensionContext): Promise<void>;\n finishSession(reason: string, ctx: ExtensionContext): Promise<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 installPiTelemetryRuntime(\n cordis: Context,\n pi: ExtensionAPI,\n options: PiTelemetryExtensionOptions = {},\n splitReadiness = false,\n): PiTelemetryRuntimeHandle {\n const env = options.env ?? process.env;\n const metrics = options.metrics;\n const toolStartedAt = new Map<string, number>();\n let active = true;\n let sessionGeneration = 0;\n let agentStartedAt: number | undefined;\n let telemetry: DoomTelemetry | undefined;\n let telemetrySessionId: string | undefined;\n let sessionReadiness:\n | {\n readonly sessionManager: object;\n readonly coordinator: DoomReadinessCoordinator;\n readonly operation: Promise<void>;\n }\n | undefined;\n let backgroundQueue = Promise.resolve();\n\n const enqueue = (operation: () => Promise<void>): void => {\n const scheduled = backgroundQueue.then(operation);\n // Telemetry is advisory and the coordinator owns startup failure notification.\n backgroundQueue = scheduled.catch(() => undefined);\n };\n\n cordis.effect(\n () => async () => {\n active = false;\n sessionGeneration += 1;\n sessionReadiness = undefined;\n toolStartedAt.clear();\n agentStartedAt = undefined;\n const ownedTelemetry = telemetry;\n telemetry = undefined;\n telemetrySessionId = undefined;\n await ownedTelemetry?.shutdown();\n },\n `${PACKAGE_NAME}/telemetry`,\n );\n\n const emit = async (level: RecordLevel, name: string, attributes: TelemetryAttributes): Promise<void> => {\n if (!active || !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 if (!active) throw new Error('Log telemetry runtime is disposed.');\n // The sink groups records by the x-agent-session-id header, which the SDK derives from\n // the environment once per handle. Nothing stamps PI_SESSION_ID for a top-level session\n // (only doompi-workflow does, for spawned children), so without this overlay every record\n // lands in a single unattributed group and per-agent cost cannot be queried.\n const sessionId = ctx.sessionManager.getSessionId();\n // The handle caches those headers, so a second session in the same process would keep\n // reporting under the first session's id. Rebuild when the session changes.\n if (telemetry && telemetrySessionId !== sessionId) {\n const staleTelemetry = telemetry;\n telemetry = undefined;\n telemetrySessionId = undefined;\n enqueue(() => staleTelemetry.shutdown());\n }\n if (!telemetry) {\n telemetrySessionId = sessionId;\n telemetry = createDoomTelemetry({\n serviceName: options.serviceName ?? SERVICE_NAME,\n packageName: PACKAGE_NAME,\n cwd: ctx.cwd,\n env: { ...env, PI_SESSION_ID: sessionId },\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) => {\n if (active) metrics?.record(record.event, record.attributes);\n },\n onStatus: (status) => {\n if (active) options.onSinkStatus?.(statusFromTelemetry(status));\n },\n });\n }\n if (active) options.onSinkStatus?.(statusFromTelemetry(telemetry.status()));\n return telemetry;\n };\n\n const waitForSession = async (ctx: ExtensionContext): Promise<void> => {\n const current = sessionReadiness;\n if (!current) return;\n if (current.sessionManager !== ctx.sessionManager) {\n throw new Error('Log readiness belongs to a stale Pi session.');\n }\n await current.operation;\n if (!active || current !== sessionReadiness) {\n throw new Error('Log readiness belongs to a stale extension generation.');\n }\n };\n\n const enqueueForSession = (ctx: ExtensionContext, operation: () => Promise<void>): void | Promise<void> => {\n const expected = sessionReadiness;\n const run = async (): Promise<void> => {\n if (expected) {\n const ready = await expected.operation.then(\n () => true,\n () => false,\n );\n if (!ready || !active || expected !== sessionReadiness || expected.sessionManager !== ctx.sessionManager) {\n return;\n }\n } else if (!active) {\n return;\n }\n await operation();\n };\n if (!splitReadiness) return run();\n enqueue(run);\n return undefined;\n };\n\n pi.on('session_start', (event, ctx) => {\n if (!active) return undefined;\n const ownGeneration = ++sessionGeneration;\n toolStartedAt.clear();\n agentStartedAt = undefined;\n const initialize = async (signal?: AbortSignal): Promise<void> => {\n signal?.throwIfAborted();\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 signal?.throwIfAborted();\n };\n const coordinator = splitReadiness ? readDoomReadinessCoordinator(cordis) : undefined;\n if (!coordinator) return initialize();\n\n const previous = sessionReadiness;\n const operation = (async (): Promise<void> => {\n if (previous?.coordinator === coordinator) await previous.operation.catch(() => undefined);\n if (!active || ownGeneration !== sessionGeneration) return;\n const handle = coordinator.start(\n PACKAGE_NAME,\n `${ctx.sessionManager.getSessionId()}:${ownGeneration}`,\n async (signal) => {\n await initialize(signal);\n return { value: undefined };\n },\n );\n await handle.wait();\n })();\n // Config's coordinator owns the single user-facing failure notification.\n void operation.catch(() => undefined);\n sessionReadiness = { sessionManager: ctx.sessionManager, coordinator, operation };\n return undefined;\n });\n\n pi.on('before_agent_start', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('agent_start', (_event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\n agentStartedAt = Date.now();\n getTelemetry(ctx);\n await emit('info', 'pi.agent.started', contextAttributes(ctx));\n });\n });\n\n pi.on('turn_start', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\n getTelemetry(ctx);\n await emit('debug', 'pi.turn.started', {\n ...contextAttributes(ctx),\n 'pi.turn.index': event.turnIndex,\n });\n });\n });\n\n pi.on('turn_end', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('tool_execution_start', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('tool_execution_end', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('after_provider_response', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('model_select', (event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\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\n pi.on('agent_end', (event, ctx) => {\n if (!active) return;\n const generation = sessionGeneration;\n return enqueueForSession(ctx, async () => {\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 if (!active || generation !== sessionGeneration) return;\n agentStartedAt = undefined;\n await telemetry?.flush();\n });\n });\n\n pi.on('agent_settled', (_event, ctx) => {\n if (!active) return;\n return enqueueForSession(ctx, async () => {\n getTelemetry(ctx);\n await emit('debug', 'pi.agent.settled', contextAttributes(ctx));\n });\n });\n\n return {\n waitForSession,\n async finishSession(reason, ctx) {\n if (!active) return;\n await backgroundQueue;\n const current = sessionReadiness;\n if (current) await current.operation.catch(() => undefined);\n if (!active) return;\n if (current && current !== sessionReadiness) return;\n if (!telemetry) return;\n getTelemetry(ctx);\n await emit('info', 'pi.session.finished', {\n ...contextAttributes(ctx),\n 'pi.session.reason': reason,\n });\n },\n };\n}\n\nexport function registerLogMetricsLeaderBinding(hub: DoomUiHubService, options: { source?: string } = {}): () => void {\n const contribution = hub.registerLeader({\n source: options.source ?? LEADER_SOURCE,\n bindings: [\n {\n id: 'log.metrics',\n path: [\n { key: 'h', label: 'help', detail: HELP_GROUP_DETAIL, order: HELP_GROUP_ORDER },\n { key: 'l', label: 'logs', detail: 'telemetry' },\n ],\n command: { name: LOG_METRICS_COMMAND },\n },\n ],\n });\n return () => contribution.dispose();\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 installDoomLogRuntime(\n cordis: Context,\n pi: ExtensionAPI,\n options: PiTelemetryExtensionOptions = {},\n splitReadiness = false,\n): PiTelemetryRuntimeHandle {\n const env = options.env ?? process.env;\n const metrics = new LogMetricsAggregator();\n // Built lazily so the history transports resolve their sink instance from the\n // same cwd and package identity the telemetry writer registers under; at\n // install time there is no ExtensionContext to read `cwd` from yet.\n let metricsSource = options.metricsSource;\n const resolveMetricsSource = (ctx: ExtensionContext): MetricsSource =>\n (metricsSource ??= createMetricsSource({\n env,\n cwd: ctx.cwd,\n packageName: PACKAGE_NAME,\n serviceName: options.serviceName ?? SERVICE_NAME,\n }));\n let active = true;\n let sink: SinkStatus | undefined;\n let lastDiagnostic: string | undefined;\n\n cordis.effect(\n () => () => {\n active = false;\n sink = undefined;\n lastDiagnostic = undefined;\n },\n `${PACKAGE_NAME}/runtime`,\n );\n\n const telemetryRuntime = installPiTelemetryRuntime(\n cordis,\n pi,\n {\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 if (!active) return;\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 if (!active) return;\n sink = status;\n options.onSinkStatus?.(status);\n },\n },\n splitReadiness,\n );\n\n cordis.inject([DOOM_UI_HUB_SERVICE], (uiContext) => {\n return registerLogMetricsLeaderBinding(requireDoomUiHub(uiContext), { source: options.leaderSource });\n });\n pi.registerCommand(LOG_METRICS_COMMAND, {\n description: LOG_METRICS_DESCRIPTION,\n handler: async (_args, ctx) => {\n if (!active) return;\n await telemetryRuntime.waitForSession(ctx);\n if (!active) return;\n const source = resolveMetricsSource(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: source.lastTransport(),\n instance: source.instance?.(),\n lastDiagnostic,\n }),\n source.query,\n );\n },\n });\n return telemetryRuntime;\n}\n\ninterface LogPluginConfig {\n readonly pi: ExtensionAPI;\n}\n\nfunction logPlugin(cordis: Context, { pi }: LogPluginConfig): void {\n const runtime = installDoomLogRuntime(cordis, pi, {}, true);\n let shutdown: Promise<void> | undefined;\n pi.on('session_shutdown', (event, ctx) => {\n shutdown ??= runtime.finishSession(event.reason, ctx);\n return shutdown;\n });\n}\n\n/** The package's single standard Pi factory. */\nexport async function doomLogExtension(pi: ExtensionAPI): Promise<void> {\n const connection = await connectDoomCordisHost(pi, PACKAGE_NAME);\n const fiber = connection.root.plugin(logPlugin, { pi });\n try {\n await fiber;\n } catch (error) {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n throw error;\n }\n let disposal: Promise<void> | undefined;\n pi.on(\n 'session_shutdown',\n () =>\n (disposal ??= (async () => {\n try {\n await fiber.dispose();\n } finally {\n await connection.dispose();\n }\n })()),\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LogMetricsRecorder } from "../../services/metrics.cjs";
|
|
2
|
+
import { MetricsQuery, MetricsSource } from "../../types/metricsSource.cjs";
|
|
3
|
+
import { LogMetricsView, SinkStatus } from "../../tui/logMetricsOverlay.cjs";
|
|
4
|
+
import { DoomUiHubService } from "@agimon-ai/doompi-extension-contracts/ui-hub";
|
|
5
|
+
import { DoomTelemetryOptions } from "@agimon-ai/doompi-telemetry";
|
|
6
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
//#region src/adapters/pi/extension.d.ts
|
|
9
|
+
type TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;
|
|
10
|
+
export interface PiTelemetryExtensionOptions {
|
|
11
|
+
allowFileFallback?: boolean;
|
|
12
|
+
/** Overrides the telemetry service identity. */
|
|
13
|
+
serviceName?: string;
|
|
14
|
+
/** Overrides the leader contribution owner. */
|
|
15
|
+
leaderSource?: string;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
telemetryFactory?: TelemetryFactory;
|
|
18
|
+
/** Receives every sanitized record, whether or not a sink is live. */
|
|
19
|
+
metrics?: LogMetricsRecorder;
|
|
20
|
+
/** Called once the sink backend is resolved, so the overlay can report it. */
|
|
21
|
+
onSinkStatus?: (status: SinkStatus | undefined) => void;
|
|
22
|
+
/** Overrides the sink-history transport; injected in tests. */
|
|
23
|
+
metricsSource?: MetricsSource;
|
|
24
|
+
/** Receives telemetry lifecycle failures without writing into the TUI. */
|
|
25
|
+
onDiagnostic?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export interface PiTelemetryRuntimeHandle {
|
|
28
|
+
waitForSession(ctx: ExtensionContext): Promise<void>;
|
|
29
|
+
finishSession(reason: string, ctx: ExtensionContext): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function registerLogMetricsLeaderBinding(hub: DoomUiHubService, options?: {
|
|
32
|
+
source?: string;
|
|
33
|
+
}): () => void;
|
|
34
|
+
export declare function openLogMetricsOverlay(ctx: ExtensionContext, getView: () => LogMetricsView, query?: MetricsQuery): Promise<void>;
|
|
35
|
+
export declare function installDoomLogRuntime(cordis: Context, pi: ExtensionAPI, options?: PiTelemetryExtensionOptions, splitReadiness?: boolean): PiTelemetryRuntimeHandle;
|
|
36
|
+
/** The package's single standard Pi factory. */
|
|
37
|
+
export declare function doomLogExtension(pi: ExtensionAPI): Promise<void>;
|
|
38
|
+
//#endregion
|
|
39
|
+
//# sourceMappingURL=extension.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;;;;;KAwCK,mBAAmB,YAAY;iBAInB;EACf;;EAEA;;EAEA;EACA,MAAM,OAAO;EACb,mBAAmB;;EAEnB,UAAU;;EAEV,gBAAgB,QAAQ;;EAExB,gBAAgB;;EAEhB,gBAAgB;;iBAGD;EACf,eAAe,KAAK,mBAAmB;EACvC,cAAc,gBAAgB,KAAK,mBAAmB;;wBA8XxC,gCAAgC,KAAK,kBAAkB;EAAW;;wBAiB5D,sBACpB,KAAK,kBACL,eAAe,gBACf,QAAQ,eACP;wBAea,sBACd,QAAQ,SACR,IAAI,cACJ,UAAS,6BACT,2BACC;;wBA4FmB,iBAAiB,IAAI,eAAe"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { LogMetricsRecorder } from "../../services/metrics.mjs";
|
|
2
|
+
import { MetricsQuery, MetricsSource } from "../../types/metricsSource.mjs";
|
|
3
|
+
import { LogMetricsView, SinkStatus } from "../../tui/logMetricsOverlay.mjs";
|
|
4
|
+
import { DoomUiHubService } from "@agimon-ai/doompi-extension-contracts/ui-hub";
|
|
5
|
+
import { DoomTelemetryOptions } from "@agimon-ai/doompi-telemetry";
|
|
6
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
import { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
//#region src/adapters/pi/extension.d.ts
|
|
9
|
+
type TelemetryFactory = NonNullable<DoomTelemetryOptions['telemetryFactory']>;
|
|
10
|
+
export interface PiTelemetryExtensionOptions {
|
|
11
|
+
allowFileFallback?: boolean;
|
|
12
|
+
/** Overrides the telemetry service identity. */
|
|
13
|
+
serviceName?: string;
|
|
14
|
+
/** Overrides the leader contribution owner. */
|
|
15
|
+
leaderSource?: string;
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
telemetryFactory?: TelemetryFactory;
|
|
18
|
+
/** Receives every sanitized record, whether or not a sink is live. */
|
|
19
|
+
metrics?: LogMetricsRecorder;
|
|
20
|
+
/** Called once the sink backend is resolved, so the overlay can report it. */
|
|
21
|
+
onSinkStatus?: (status: SinkStatus | undefined) => void;
|
|
22
|
+
/** Overrides the sink-history transport; injected in tests. */
|
|
23
|
+
metricsSource?: MetricsSource;
|
|
24
|
+
/** Receives telemetry lifecycle failures without writing into the TUI. */
|
|
25
|
+
onDiagnostic?: (message: string) => void;
|
|
26
|
+
}
|
|
27
|
+
export interface PiTelemetryRuntimeHandle {
|
|
28
|
+
waitForSession(ctx: ExtensionContext): Promise<void>;
|
|
29
|
+
finishSession(reason: string, ctx: ExtensionContext): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare function registerLogMetricsLeaderBinding(hub: DoomUiHubService, options?: {
|
|
32
|
+
source?: string;
|
|
33
|
+
}): () => void;
|
|
34
|
+
export declare function openLogMetricsOverlay(ctx: ExtensionContext, getView: () => LogMetricsView, query?: MetricsQuery): Promise<void>;
|
|
35
|
+
export declare function installDoomLogRuntime(cordis: Context, pi: ExtensionAPI, options?: PiTelemetryExtensionOptions, splitReadiness?: boolean): PiTelemetryRuntimeHandle;
|
|
36
|
+
/** The package's single standard Pi factory. */
|
|
37
|
+
export declare function doomLogExtension(pi: ExtensionAPI): Promise<void>;
|
|
38
|
+
//#endregion
|
|
39
|
+
//# sourceMappingURL=extension.d.mts.map
|