@revealui/ai 0.2.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +200 -26
  2. package/dist/a2a/handler.d.ts +3 -1
  3. package/dist/a2a/handler.d.ts.map +1 -1
  4. package/dist/a2a/handler.js +24 -7
  5. package/dist/a2a/task-store.d.ts.map +1 -1
  6. package/dist/a2a/task-store.js +5 -1
  7. package/dist/client/hooks/useAgentStream.d.ts +84 -1
  8. package/dist/client/hooks/useAgentStream.d.ts.map +1 -1
  9. package/dist/client/hooks/useAgentStream.js +106 -10
  10. package/dist/index.d.ts +4 -6
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -15
  13. package/dist/ingestion/admin-indexer.js +1 -1
  14. package/dist/ingestion/pipeline.d.ts +1 -1
  15. package/dist/ingestion/pipeline.d.ts.map +1 -1
  16. package/dist/llm/client.d.ts +33 -10
  17. package/dist/llm/client.d.ts.map +1 -1
  18. package/dist/llm/client.js +70 -57
  19. package/dist/llm/key-validator.d.ts.map +1 -1
  20. package/dist/llm/key-validator.js +0 -27
  21. package/dist/llm/providers/inference-snaps.d.ts +7 -5
  22. package/dist/llm/providers/inference-snaps.d.ts.map +1 -1
  23. package/dist/llm/providers/inference-snaps.js +7 -5
  24. package/dist/llm/providers/openai-compat.d.ts +1 -1
  25. package/dist/llm/providers/openai-compat.js +1 -1
  26. package/dist/llm/response-cache.d.ts +1 -2
  27. package/dist/llm/response-cache.d.ts.map +1 -1
  28. package/dist/llm/response-cache.js +1 -2
  29. package/dist/llm/server.d.ts +1 -1
  30. package/dist/llm/server.d.ts.map +1 -1
  31. package/dist/llm/server.js +3 -1
  32. package/dist/memory/crdt/lww-register.js +1 -1
  33. package/dist/memory/preferences/user-preferences-manager.d.ts.map +1 -1
  34. package/dist/memory/preferences/user-preferences-manager.js +1 -1
  35. package/dist/memory/services/index.d.ts +2 -0
  36. package/dist/memory/services/index.d.ts.map +1 -1
  37. package/dist/memory/services/index.js +1 -0
  38. package/dist/memory/services/reconciliation-service.d.ts +64 -0
  39. package/dist/memory/services/reconciliation-service.d.ts.map +1 -0
  40. package/dist/memory/services/reconciliation-service.js +127 -0
  41. package/dist/memory/stores/working-memory.d.ts.map +1 -1
  42. package/dist/memory/stores/working-memory.js +1 -1
  43. package/dist/memory/utils/index.d.ts +1 -2
  44. package/dist/memory/utils/index.d.ts.map +1 -1
  45. package/dist/memory/utils/index.js +1 -2
  46. package/dist/memory/utils/sql-helpers.d.ts +21 -66
  47. package/dist/memory/utils/sql-helpers.d.ts.map +1 -1
  48. package/dist/memory/utils/sql-helpers.js +34 -169
  49. package/dist/memory/vector/vector-memory-service.d.ts.map +1 -1
  50. package/dist/memory/vector/vector-memory-service.js +6 -1
  51. package/dist/orchestration/runtime.d.ts +30 -1
  52. package/dist/orchestration/runtime.d.ts.map +1 -1
  53. package/dist/orchestration/runtime.js +20 -3
  54. package/dist/orchestration/streaming-runtime.d.ts +48 -1
  55. package/dist/orchestration/streaming-runtime.d.ts.map +1 -1
  56. package/dist/orchestration/ticket-agent.d.ts +10 -1
  57. package/dist/orchestration/ticket-agent.d.ts.map +1 -1
  58. package/dist/orchestration/ticket-agent.js +11 -2
  59. package/dist/skills/catalog/vercel-catalog.d.ts.map +1 -1
  60. package/dist/skills/catalog/vercel-catalog.js +13 -7
  61. package/dist/tools/mcp-adapter.d.ts +192 -1
  62. package/dist/tools/mcp-adapter.d.ts.map +1 -1
  63. package/dist/tools/mcp-adapter.js +434 -1
  64. package/dist/tools/mcp-elicitation.d.ts +128 -0
  65. package/dist/tools/mcp-elicitation.d.ts.map +1 -0
  66. package/dist/tools/mcp-elicitation.js +121 -0
  67. package/dist/tools/mcp-events.d.ts +270 -0
  68. package/dist/tools/mcp-events.d.ts.map +1 -0
  69. package/dist/tools/mcp-events.js +165 -0
  70. package/dist/tools/mcp-sampling.d.ts +155 -0
  71. package/dist/tools/mcp-sampling.d.ts.map +1 -0
  72. package/dist/tools/mcp-sampling.js +171 -0
  73. package/dist/tools/ticket-tools.d.ts +35 -2
  74. package/dist/tools/ticket-tools.d.ts.map +1 -1
  75. package/dist/tools/ticket-tools.js +32 -2
  76. package/package.json +38 -12
  77. package/dist/llm/providers/vultr.d.ts +0 -18
  78. package/dist/llm/providers/vultr.d.ts.map +0 -1
  79. package/dist/llm/providers/vultr.js +0 -169
  80. package/dist/memory/utils/deep-clone.d.ts +0 -10
  81. package/dist/memory/utils/deep-clone.d.ts.map +0 -1
  82. package/dist/memory/utils/deep-clone.js +0 -9
@@ -0,0 +1,121 @@
1
+ /**
2
+ * MCP Elicitation — route server `elicitation/create` requests through
3
+ * a consumer-provided UI callback (Stage 5.3 of the MCP v1 plan).
4
+ *
5
+ * The MCP spec defines `elicitation/create` as a server-to-client
6
+ * request: a server asks the client to collect structured input from
7
+ * the user mid-flow (form inputs, confirmations, auth prompts). The
8
+ * agent runtime is NOT the right place to decide what UI to show —
9
+ * each consumer (admin inspector, agent execution panel, CLI tool,
10
+ * Slack bot, …) renders in its own idiom. This module exposes
11
+ * `createElicitationHandler({ onElicit })` — a thin factory that
12
+ * wraps the consumer's async UI callback into the structural handler
13
+ * shape that `McpClient` from `@revealui/mcp/client` expects.
14
+ *
15
+ * Safety defaults
16
+ * ---------------
17
+ * - Servers that request **URL mode** (out-of-band consent via a
18
+ * separate browser tab) are auto-declined unless `allowUrlMode: true`
19
+ * is passed. URL mode is a phishing vector when the user can't
20
+ * easily verify what domain they're on — admin inspector (Stage 3.4)
21
+ * takes the same posture; agent runtime matches for consistency.
22
+ * - Optional `timeoutMs` auto-declines after the deadline. Useful in
23
+ * headless automation where no human is at the keyboard.
24
+ * - Thrown errors inside `onElicit` are converted to `{ action:
25
+ * 'cancel' }` rather than propagating — servers should see a clean
26
+ * decline, not a protocol error the UI crashed on.
27
+ *
28
+ * As with the rest of the MCP-adapter surface, this module uses
29
+ * structural typing to stay decoupled from `@revealui/mcp` — the real
30
+ * `ElicitationHandler` from `@revealui/mcp/client` structurally
31
+ * satisfies the `McpElicitationHandler` shape exported here.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * import { McpClient } from '@revealui/mcp/client';
36
+ * import { createElicitationHandler } from '@revealui/ai';
37
+ *
38
+ * const client = new McpClient({
39
+ * clientInfo: { name: 'my-agent', version: '1.0.0' },
40
+ * transport: { kind: 'streamable-http', url: '…' },
41
+ * elicitationHandler: createElicitationHandler({
42
+ * onElicit: async ({ message, requestedSchema }) => {
43
+ * const form = await showFormDialog({ title: message, schema: requestedSchema });
44
+ * if (!form) return { action: 'cancel' };
45
+ * return { action: 'accept', content: form.values };
46
+ * },
47
+ * timeoutMs: 60_000,
48
+ * }),
49
+ * });
50
+ * ```
51
+ */
52
+ import { logger } from '@revealui/core/observability/logger';
53
+ import { emitMcpEvent } from './mcp-events.js';
54
+ export function createElicitationHandler(options) {
55
+ const { onElicit, timeoutMs, allowUrlMode, onElicitationRequest, onEvent, namespace } = options;
56
+ return async (params) => {
57
+ const fieldCount = countSchemaFields(params.requestedSchema);
58
+ const started = Date.now();
59
+ if (params.mode === 'url' && !allowUrlMode) {
60
+ emitMcpEvent(onEvent, {
61
+ kind: 'mcp.elicitation.create',
62
+ ...(namespace !== undefined ? { namespace } : {}),
63
+ action: 'decline',
64
+ fieldCount,
65
+ ...(params.mode !== undefined ? { mode: params.mode } : {}),
66
+ duration_ms: Date.now() - started,
67
+ success: true,
68
+ });
69
+ return { action: 'decline' };
70
+ }
71
+ onElicitationRequest?.({
72
+ message: params.message,
73
+ fieldCount,
74
+ ...(params.mode !== undefined ? { mode: params.mode } : {}),
75
+ });
76
+ let timeoutHandle;
77
+ let result;
78
+ try {
79
+ const userResponse = onElicit(params).catch((error) => {
80
+ // Errors inside the consumer's UI callback → cancel (not decline —
81
+ // the server should see a clean "didn't happen", not a
82
+ // misrepresented "user said no").
83
+ logger.warn('[createElicitationHandler] onElicit threw; returning cancel', {
84
+ error: error instanceof Error ? error.message : String(error),
85
+ });
86
+ return { action: 'cancel' };
87
+ });
88
+ if (timeoutMs === undefined || timeoutMs <= 0) {
89
+ result = await userResponse;
90
+ }
91
+ else {
92
+ const timeoutPromise = new Promise((resolve) => {
93
+ timeoutHandle = setTimeout(() => {
94
+ resolve({ action: 'cancel' });
95
+ }, timeoutMs);
96
+ });
97
+ result = await Promise.race([userResponse, timeoutPromise]);
98
+ }
99
+ }
100
+ finally {
101
+ if (timeoutHandle !== undefined)
102
+ clearTimeout(timeoutHandle);
103
+ }
104
+ emitMcpEvent(onEvent, {
105
+ kind: 'mcp.elicitation.create',
106
+ ...(namespace !== undefined ? { namespace } : {}),
107
+ action: result.action,
108
+ fieldCount,
109
+ ...(params.mode !== undefined ? { mode: params.mode } : {}),
110
+ duration_ms: Date.now() - started,
111
+ success: true,
112
+ });
113
+ return result;
114
+ };
115
+ }
116
+ /** Count the declared fields on a `requestedSchema.properties` record. */
117
+ function countSchemaFields(schema) {
118
+ if (!(schema?.properties && typeof schema.properties === 'object'))
119
+ return 0;
120
+ return Object.keys(schema.properties).length;
121
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * MCP protocol log channel — structured events per server-to-client call
3
+ * (Stage 6.1 of the MCP v1 plan).
4
+ *
5
+ * Stage 5 shipped the full agent-side MCP protocol surface
6
+ * (tools + resources + prompts + sampling + elicitation + progress +
7
+ * cancellation). Stage 6.1 adds observability: every adapter in
8
+ * `@revealui/ai/tools/mcp-*` grows an `onEvent?: McpEventSink` hook that
9
+ * fires once per protocol call with a structured summary. Consumers wire
10
+ * the sink to whatever aggregator they run — typically
11
+ * `@revealui/core/observability/logger` via `createCoreLoggerSink()`.
12
+ *
13
+ * Instrumentation lives at the agent-adapter layer by design. The
14
+ * hypervisor is a separate lane (Stage 6.2 lands `usage_meters` rows
15
+ * there, where `accountId` is in scope). Agent adapters are intentionally
16
+ * tenant-agnostic, so events carry no `accountId` field — the consumer
17
+ * adds tenant context in their sink wrapper if they need it for routing.
18
+ *
19
+ * PII note
20
+ * --------
21
+ * Errors surfaced by the server can legitimately contain user-identifying
22
+ * strings (e.g. "user 'alice@example.com' not found"). `error` is passed
23
+ * through to the sink verbatim. If your deployment's observability layer
24
+ * is subject to PII constraints, wrap the sink with a redactor
25
+ * (`redactLogContext` from `@revealui/security`) before passing it in.
26
+ *
27
+ * Params / args / content bodies are **not** included in events —
28
+ * observability is summary-grained by default to avoid accidentally
29
+ * logging credentials, prompts, or document contents.
30
+ */
31
+ /** Common fields every MCP protocol-log event carries. */
32
+ interface McpEventBase {
33
+ /**
34
+ * Wall-clock duration of the call in milliseconds, measured from
35
+ * handler entry to handler exit (success or failure). Includes the
36
+ * full round-trip across the transport plus any zod validation.
37
+ */
38
+ duration_ms: number;
39
+ /** `true` when the call completed without throwing and without `isError`. */
40
+ success: boolean;
41
+ /**
42
+ * Server-reported error text on failure (from `CallToolResult.content`
43
+ * when `isError: true`, or from `error.message` when the call threw).
44
+ * Omitted when `success: true`. May contain PII — see module docs.
45
+ */
46
+ error?: string;
47
+ }
48
+ export interface McpToolCallEvent extends McpEventBase {
49
+ kind: 'mcp.tool.call';
50
+ /** Server identifier the tool belongs to. */
51
+ namespace: string;
52
+ /** Tool name WITHOUT the `mcp_<namespace>__` adapter prefix. */
53
+ toolName: string;
54
+ }
55
+ export interface McpResourceListEvent extends McpEventBase {
56
+ kind: 'mcp.resource.list';
57
+ namespace: string;
58
+ /** Number of resources returned (omitted on failure). */
59
+ resourceCount?: number;
60
+ }
61
+ export interface McpResourceReadEvent extends McpEventBase {
62
+ kind: 'mcp.resource.read';
63
+ namespace: string;
64
+ /** Resource URI that was read. May be sensitive — see module docs. */
65
+ uri: string;
66
+ }
67
+ export interface McpPromptListEvent extends McpEventBase {
68
+ kind: 'mcp.prompt.list';
69
+ namespace: string;
70
+ /** Number of prompts returned (omitted on failure). */
71
+ promptCount?: number;
72
+ }
73
+ export interface McpPromptGetEvent extends McpEventBase {
74
+ kind: 'mcp.prompt.get';
75
+ namespace: string;
76
+ /** Prompt name that was requested. */
77
+ promptName: string;
78
+ }
79
+ export interface McpSamplingCreateEvent extends McpEventBase {
80
+ kind: 'mcp.sampling.create';
81
+ /**
82
+ * Server identifier when the consumer attached one at handler
83
+ * construction via `createSamplingHandler({ namespace })`. Absent
84
+ * when the handler is wired to a shared `McpClient` that fans out
85
+ * to multiple servers — in that case the consumer's sink wrapper
86
+ * can fill it in from call-site context.
87
+ */
88
+ namespace?: string;
89
+ /** Model label reported back to the server in `result.model`. */
90
+ model: string;
91
+ messageCount: number;
92
+ maxTokens: number;
93
+ }
94
+ export interface McpElicitationCreateEvent extends McpEventBase {
95
+ kind: 'mcp.elicitation.create';
96
+ /** Server identifier when provided to `createElicitationHandler`. */
97
+ namespace?: string;
98
+ /** User's decision — `'accept' | 'decline' | 'cancel'`. */
99
+ action: 'accept' | 'decline' | 'cancel';
100
+ /** Number of fields in the requested schema. */
101
+ fieldCount: number;
102
+ /** Elicitation mode as declared by the server (`'form'`, `'url'`, …). */
103
+ mode?: string;
104
+ }
105
+ export type McpLogEvent = McpToolCallEvent | McpResourceListEvent | McpResourceReadEvent | McpPromptListEvent | McpPromptGetEvent | McpSamplingCreateEvent | McpElicitationCreateEvent;
106
+ /**
107
+ * Consumer-wired observability sink. Called exactly once per protocol
108
+ * call. Must not throw — the adapter swallows sink-thrown errors
109
+ * internally, but a well-behaved sink returns quickly and cleanly.
110
+ */
111
+ export type McpEventSink = (event: McpLogEvent) => void;
112
+ export interface CreateCoreLoggerSinkOptions {
113
+ /**
114
+ * Minimum level for successful events. Default `'info'`. Set to
115
+ * `'debug'` to drop successful-call volume from info-level ingestion.
116
+ * Failed calls are always emitted at `'warn'` regardless.
117
+ */
118
+ successLevel?: 'debug' | 'info';
119
+ }
120
+ /**
121
+ * Build an `McpEventSink` that fans events into the central log
122
+ * aggregator via `@revealui/core/observability/logger`. This is the
123
+ * recommended default for in-process consumers (admin app, API, agent
124
+ * orchestration). Out-of-process consumers (CLI, remote inspector)
125
+ * typically construct a custom sink that forwards to their own transport.
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * import {
130
+ * createToolsFromMcpClient,
131
+ * createCoreLoggerSink,
132
+ * } from '@revealui/ai';
133
+ *
134
+ * const onEvent = createCoreLoggerSink();
135
+ *
136
+ * const tools = await createToolsFromMcpClient(client, {
137
+ * namespace: 'my-server',
138
+ * onEvent,
139
+ * });
140
+ * ```
141
+ */
142
+ export declare function createCoreLoggerSink(options?: CreateCoreLoggerSinkOptions): McpEventSink;
143
+ /**
144
+ * Invoke a sink, swallowing any thrown error. A misbehaving sink must
145
+ * never break the underlying MCP call — observability is best-effort.
146
+ * Logs the sink failure at warn so the regression is still visible.
147
+ *
148
+ * @internal
149
+ */
150
+ export declare function emitMcpEvent(sink: McpEventSink | undefined, event: McpLogEvent): void;
151
+ /**
152
+ * Row shape consumed by the sink's `write` callback. Structurally
153
+ * matches `usage_meters` insert payload in `@revealui/db/schema/accounts`.
154
+ * Kept as a plain interface here to avoid importing Drizzle's inferred
155
+ * insert type and to let tests build rows without a db client.
156
+ */
157
+ export interface McpUsageMeterRow {
158
+ /** Synthetic primary key. Default: `crypto.randomUUID()`. */
159
+ id: string;
160
+ /** Tenant / account fk (NOT NULL on `usage_meters`). */
161
+ accountId: string;
162
+ /** Dot-notation meter name: mirrors `McpLogEvent.kind`. */
163
+ meterName: string;
164
+ /**
165
+ * Event count. v1 uses `1` per call (row-per-call pattern); billing
166
+ * aggregation rolls these up. A larger quantity makes sense only
167
+ * when the meter represents a volume (tokens, bytes) rather than
168
+ * a count of events.
169
+ */
170
+ quantity: number;
171
+ /** Timestamp the metered event occurred. */
172
+ periodStart: Date;
173
+ /** Reserved for aggregation-mode rows; `null` for per-call rows. */
174
+ periodEnd: Date | null;
175
+ /**
176
+ * Origin label. Per the `usage_meters` CHECK constraint one of
177
+ * `'system' | 'user' | 'agent' | 'api'`. Defaults to `'agent'`
178
+ * since this sink consumes agent-runtime protocol events.
179
+ */
180
+ source: 'system' | 'user' | 'agent' | 'api';
181
+ /** Unique key. Backs `uniqueIndex` on `usage_meters`. */
182
+ idempotencyKey: string;
183
+ /**
184
+ * Wall-clock duration of the protocol call in milliseconds, lifted
185
+ * directly from `event.duration_ms`. Optional in the row shape so
186
+ * pre-A.3 consumers writing into rows without a `duration_ms`
187
+ * column don't break — the sink populates it whenever the consumer's
188
+ * schema accepts it. A.3 of the post-v1 MCP arc.
189
+ */
190
+ durationMs?: number;
191
+ /**
192
+ * `true` when the protocol call surfaced an error (`!event.success`).
193
+ * Optional for the same forward-compat reason as `durationMs`.
194
+ * The Usage tab on `/admin/mcp` aggregates these into success-rate
195
+ * counts per `meterName`. A.3.
196
+ */
197
+ errored?: boolean;
198
+ }
199
+ /**
200
+ * Consumer-supplied row writer. Typically:
201
+ * `(row) => db.insert(usageMeters).values(row)`.
202
+ * Can be sync (throws on error) or async. The sink awaits thenable
203
+ * results and logs rejections at warn — write failures never propagate
204
+ * back into the MCP call that produced the event.
205
+ */
206
+ export type McpUsageMeterWriter = (row: McpUsageMeterRow) => void | Promise<void>;
207
+ export interface CreateUsageMeterSinkOptions {
208
+ /**
209
+ * Tenant / account id required by the `usage_meters.accountId NOT
210
+ * NULL fk` constraint. Agent-side adapters are tenant-agnostic by
211
+ * design — consumer binds the correct account when constructing
212
+ * the sink, typically one sink per authenticated call path.
213
+ */
214
+ accountId: string;
215
+ /**
216
+ * Row persistence callback. Structural so consumers can plug in
217
+ * raw Drizzle, a wrapped client, or a test double.
218
+ */
219
+ write: McpUsageMeterWriter;
220
+ /**
221
+ * Origin label per `usage_meters.source` check constraint. Default
222
+ * `'agent'`.
223
+ */
224
+ source?: McpUsageMeterRow['source'];
225
+ /**
226
+ * Custom idempotency-key generator. Default: fresh
227
+ * `crypto.randomUUID()` per event (safe; no retry coalescence).
228
+ * Supply a deterministic generator (e.g. `${requestId}:${e.kind}`)
229
+ * to collapse duplicate retries of one semantic call to a single
230
+ * row.
231
+ */
232
+ idempotencyKey?: (event: McpLogEvent) => string;
233
+ /**
234
+ * Custom row-id generator. Default: fresh UUID. Provided as a hook
235
+ * for consumers integrating with an existing id scheme.
236
+ */
237
+ id?: (event: McpLogEvent) => string;
238
+ }
239
+ /**
240
+ * Build an `McpEventSink` that translates Stage 6.1 protocol log
241
+ * events into `usage_meters` insert rows (Stage 6.2).
242
+ *
243
+ * This is the agent-adapter-lane counterpart to the hypervisor's
244
+ * `setUsageMeterSink()` in `@revealui/mcp`. Both paths land rows in
245
+ * the same `usage_meters` table. The agent-adapter path covers the
246
+ * four boundaries the hypervisor doesn't own
247
+ * (`resource.list/read`, `prompt.list/get`, `sampling.create`,
248
+ * `elicitation.create`) plus the agent-side `tool.call` for
249
+ * `McpClient`-using agents.
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * import { usageMeters } from '@revealui/db';
254
+ * import {
255
+ * createToolsFromMcpClient,
256
+ * createUsageMeterSink,
257
+ * } from '@revealui/ai';
258
+ *
259
+ * const tools = await createToolsFromMcpClient(client, {
260
+ * namespace: 'content',
261
+ * onEvent: createUsageMeterSink({
262
+ * accountId: session.accountId,
263
+ * write: (row) => db.insert(usageMeters).values(row),
264
+ * }),
265
+ * });
266
+ * ```
267
+ */
268
+ export declare function createUsageMeterSink(options: CreateUsageMeterSinkOptions): McpEventSink;
269
+ export {};
270
+ //# sourceMappingURL=mcp-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-events.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-events.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * MCP protocol log channel — structured events per server-to-client call
3
+ * (Stage 6.1 of the MCP v1 plan).
4
+ *
5
+ * Stage 5 shipped the full agent-side MCP protocol surface
6
+ * (tools + resources + prompts + sampling + elicitation + progress +
7
+ * cancellation). Stage 6.1 adds observability: every adapter in
8
+ * `@revealui/ai/tools/mcp-*` grows an `onEvent?: McpEventSink` hook that
9
+ * fires once per protocol call with a structured summary. Consumers wire
10
+ * the sink to whatever aggregator they run — typically
11
+ * `@revealui/core/observability/logger` via `createCoreLoggerSink()`.
12
+ *
13
+ * Instrumentation lives at the agent-adapter layer by design. The
14
+ * hypervisor is a separate lane (Stage 6.2 lands `usage_meters` rows
15
+ * there, where `accountId` is in scope). Agent adapters are intentionally
16
+ * tenant-agnostic, so events carry no `accountId` field — the consumer
17
+ * adds tenant context in their sink wrapper if they need it for routing.
18
+ *
19
+ * PII note
20
+ * --------
21
+ * Errors surfaced by the server can legitimately contain user-identifying
22
+ * strings (e.g. "user 'alice@example.com' not found"). `error` is passed
23
+ * through to the sink verbatim. If your deployment's observability layer
24
+ * is subject to PII constraints, wrap the sink with a redactor
25
+ * (`redactLogContext` from `@revealui/security`) before passing it in.
26
+ *
27
+ * Params / args / content bodies are **not** included in events —
28
+ * observability is summary-grained by default to avoid accidentally
29
+ * logging credentials, prompts, or document contents.
30
+ */
31
+ import { logger } from '@revealui/core/observability/logger';
32
+ /**
33
+ * Build an `McpEventSink` that fans events into the central log
34
+ * aggregator via `@revealui/core/observability/logger`. This is the
35
+ * recommended default for in-process consumers (admin app, API, agent
36
+ * orchestration). Out-of-process consumers (CLI, remote inspector)
37
+ * typically construct a custom sink that forwards to their own transport.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * import {
42
+ * createToolsFromMcpClient,
43
+ * createCoreLoggerSink,
44
+ * } from '@revealui/ai';
45
+ *
46
+ * const onEvent = createCoreLoggerSink();
47
+ *
48
+ * const tools = await createToolsFromMcpClient(client, {
49
+ * namespace: 'my-server',
50
+ * onEvent,
51
+ * });
52
+ * ```
53
+ */
54
+ export function createCoreLoggerSink(options = {}) {
55
+ const successLevel = options.successLevel ?? 'info';
56
+ return (event) => {
57
+ const { kind, ...rest } = event;
58
+ const payload = { event: kind, ...rest };
59
+ if (!event.success) {
60
+ logger.warn(`[mcp] ${kind}`, payload);
61
+ return;
62
+ }
63
+ if (successLevel === 'debug') {
64
+ logger.debug(`[mcp] ${kind}`, payload);
65
+ }
66
+ else {
67
+ logger.info(`[mcp] ${kind}`, payload);
68
+ }
69
+ };
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Internal helper — shared across mcp-adapter / mcp-sampling / mcp-elicitation
73
+ // ---------------------------------------------------------------------------
74
+ /**
75
+ * Invoke a sink, swallowing any thrown error. A misbehaving sink must
76
+ * never break the underlying MCP call — observability is best-effort.
77
+ * Logs the sink failure at warn so the regression is still visible.
78
+ *
79
+ * @internal
80
+ */
81
+ export function emitMcpEvent(sink, event) {
82
+ if (!sink)
83
+ return;
84
+ try {
85
+ sink(event);
86
+ }
87
+ catch (error) {
88
+ logger.warn('[mcp] event sink threw; continuing', {
89
+ event: event.kind,
90
+ error: error instanceof Error ? error.message : String(error),
91
+ });
92
+ }
93
+ }
94
+ /**
95
+ * Map an `McpLogEvent.kind` to a `usage_meters.meterName` string.
96
+ * One-to-one: the dot-notation kind IS the meter name.
97
+ *
98
+ * @internal
99
+ */
100
+ const METER_NAMES = {
101
+ 'mcp.tool.call': 'mcp.tool.call',
102
+ 'mcp.resource.list': 'mcp.resource.list',
103
+ 'mcp.resource.read': 'mcp.resource.read',
104
+ 'mcp.prompt.list': 'mcp.prompt.list',
105
+ 'mcp.prompt.get': 'mcp.prompt.get',
106
+ 'mcp.sampling.create': 'mcp.sampling.create',
107
+ 'mcp.elicitation.create': 'mcp.elicitation.create',
108
+ };
109
+ /**
110
+ * Build an `McpEventSink` that translates Stage 6.1 protocol log
111
+ * events into `usage_meters` insert rows (Stage 6.2).
112
+ *
113
+ * This is the agent-adapter-lane counterpart to the hypervisor's
114
+ * `setUsageMeterSink()` in `@revealui/mcp`. Both paths land rows in
115
+ * the same `usage_meters` table. The agent-adapter path covers the
116
+ * four boundaries the hypervisor doesn't own
117
+ * (`resource.list/read`, `prompt.list/get`, `sampling.create`,
118
+ * `elicitation.create`) plus the agent-side `tool.call` for
119
+ * `McpClient`-using agents.
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * import { usageMeters } from '@revealui/db';
124
+ * import {
125
+ * createToolsFromMcpClient,
126
+ * createUsageMeterSink,
127
+ * } from '@revealui/ai';
128
+ *
129
+ * const tools = await createToolsFromMcpClient(client, {
130
+ * namespace: 'content',
131
+ * onEvent: createUsageMeterSink({
132
+ * accountId: session.accountId,
133
+ * write: (row) => db.insert(usageMeters).values(row),
134
+ * }),
135
+ * });
136
+ * ```
137
+ */
138
+ export function createUsageMeterSink(options) {
139
+ const source = options.source ?? 'agent';
140
+ const genId = options.id ?? (() => crypto.randomUUID());
141
+ const genKey = options.idempotencyKey ?? (() => crypto.randomUUID());
142
+ return (event) => {
143
+ const row = {
144
+ id: genId(event),
145
+ accountId: options.accountId,
146
+ meterName: METER_NAMES[event.kind],
147
+ quantity: 1,
148
+ periodStart: new Date(),
149
+ periodEnd: null,
150
+ source,
151
+ idempotencyKey: genKey(event),
152
+ durationMs: event.duration_ms,
153
+ errored: !event.success,
154
+ };
155
+ const result = options.write(row);
156
+ if (result && typeof result.catch === 'function') {
157
+ result.catch((error) => {
158
+ logger.warn('[mcp] usage meter write rejected; continuing', {
159
+ event: event.kind,
160
+ error: error instanceof Error ? error.message : String(error),
161
+ });
162
+ });
163
+ }
164
+ };
165
+ }