@llui/agent 0.0.31 → 0.0.34
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 +82 -1
- package/dist/client/agentConfirm.d.ts +48 -18
- package/dist/client/agentConfirm.d.ts.map +1 -1
- package/dist/client/agentConfirm.js +28 -25
- package/dist/client/agentConfirm.js.map +1 -1
- package/dist/client/agentConnect.d.ts +95 -34
- package/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +81 -47
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/client/agentLog.d.ts +31 -14
- package/dist/client/agentLog.d.ts.map +1 -1
- package/dist/client/agentLog.js +39 -20
- package/dist/client/agentLog.js.map +1 -1
- package/dist/client/effect-handler.d.ts +23 -0
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +185 -126
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +13 -2
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +55 -3
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +30 -5
- package/dist/client/factory.js.map +1 -1
- package/dist/client/rpc/describe-visible-content.d.ts +18 -5
- package/dist/client/rpc/describe-visible-content.d.ts.map +1 -1
- package/dist/client/rpc/describe-visible-content.js +112 -7
- package/dist/client/rpc/describe-visible-content.js.map +1 -1
- package/dist/client/rpc/list-actions.d.ts +52 -2
- package/dist/client/rpc/list-actions.d.ts.map +1 -1
- package/dist/client/rpc/list-actions.js +187 -5
- package/dist/client/rpc/list-actions.js.map +1 -1
- package/dist/client/rpc/query-state.d.ts +32 -0
- package/dist/client/rpc/query-state.d.ts.map +1 -0
- package/dist/client/rpc/query-state.js +82 -0
- package/dist/client/rpc/query-state.js.map +1 -0
- package/dist/client/rpc/send-message.d.ts +2 -0
- package/dist/client/rpc/send-message.d.ts.map +1 -1
- package/dist/client/rpc/send-message.js +119 -9
- package/dist/client/rpc/send-message.js.map +1 -1
- package/dist/client/rpc/would-dispatch.d.ts +66 -0
- package/dist/client/rpc/would-dispatch.d.ts.map +1 -0
- package/dist/client/rpc/would-dispatch.js +21 -0
- package/dist/client/rpc/would-dispatch.js.map +1 -0
- package/dist/client/ws-client.d.ts +3 -1
- package/dist/client/ws-client.d.ts.map +1 -1
- package/dist/client/ws-client.js +29 -0
- package/dist/client/ws-client.js.map +1 -1
- package/dist/codecs.d.ts +107 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +166 -0
- package/dist/codecs.js.map +1 -0
- package/dist/protocol.d.ts +155 -6
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server/lap/forward.d.ts +13 -0
- package/dist/server/lap/forward.d.ts.map +1 -1
- package/dist/server/lap/forward.js +74 -0
- package/dist/server/lap/forward.js.map +1 -1
- package/dist/server/lap/router.d.ts.map +1 -1
- package/dist/server/lap/router.js +7 -1
- package/dist/server/lap/router.js.map +1 -1
- package/dist/server/ws/pairing-registry.d.ts +22 -6
- package/dist/server/ws/pairing-registry.d.ts.map +1 -1
- package/dist/server/ws/pairing-registry.js +49 -0
- package/dist/server/ws/pairing-registry.js.map +1 -1
- package/dist/state-diff.d.ts +52 -0
- package/dist/state-diff.d.ts.map +1 -0
- package/dist/state-diff.js +119 -0
- package/dist/state-diff.js.map +1 -0
- package/package.json +11 -5
package/dist/client/ws-client.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { handleGetState } from './rpc/get-state.js';
|
|
2
|
+
import { handleQueryState } from './rpc/query-state.js';
|
|
3
|
+
import { handleWouldDispatch } from './rpc/would-dispatch.js';
|
|
2
4
|
import { handleSendMessage } from './rpc/send-message.js';
|
|
3
5
|
import { handleListActions } from './rpc/list-actions.js';
|
|
4
6
|
import { handleQueryDom } from './rpc/query-dom.js';
|
|
@@ -70,6 +72,14 @@ export function attachWsClient(ws, rpc, hello, opts = {}) {
|
|
|
70
72
|
variant: extractVariant(frame.tool, frame.args),
|
|
71
73
|
intent: buildIntent(frame.tool, frame.args, rpc.getMsgAnnotations()),
|
|
72
74
|
};
|
|
75
|
+
// For successful send_message dispatches, the stateDiff is part
|
|
76
|
+
// of the response. Lifting it into the log entry means the agent
|
|
77
|
+
// can read its own past actions with full "what changed" detail
|
|
78
|
+
// without re-querying state — essential for self-correcting
|
|
79
|
+
// behavior over multi-step flows.
|
|
80
|
+
if (frame.tool === 'send_message' && rpcErr === null && isDispatchedResult(result)) {
|
|
81
|
+
logEntry.stateDiff = result.stateDiff;
|
|
82
|
+
}
|
|
73
83
|
opts.onLogEntry?.(logEntry);
|
|
74
84
|
ws.send(JSON.stringify({ t: 'log-append', entry: logEntry }));
|
|
75
85
|
});
|
|
@@ -100,6 +110,8 @@ async function dispatch(tool, args, rpc) {
|
|
|
100
110
|
switch (tool) {
|
|
101
111
|
case 'get_state':
|
|
102
112
|
return handleGetState(rpc, (args ?? {}));
|
|
113
|
+
case 'query_state':
|
|
114
|
+
return handleQueryState(rpc, (args ?? {}));
|
|
103
115
|
case 'list_actions':
|
|
104
116
|
return handleListActions(rpc);
|
|
105
117
|
case 'send_message':
|
|
@@ -112,17 +124,21 @@ async function dispatch(tool, args, rpc) {
|
|
|
112
124
|
return handleDescribeContext(rpc);
|
|
113
125
|
case 'observe':
|
|
114
126
|
return handleObserve(rpc);
|
|
127
|
+
case 'would_dispatch':
|
|
128
|
+
return handleWouldDispatch(rpc, args);
|
|
115
129
|
default:
|
|
116
130
|
throw { code: 'invalid', detail: `unknown tool: ${tool}` };
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
const READ_TOOLS = new Set([
|
|
120
134
|
'get_state',
|
|
135
|
+
'query_state',
|
|
121
136
|
'list_actions',
|
|
122
137
|
'describe_context',
|
|
123
138
|
'query_dom',
|
|
124
139
|
'describe_visible_content',
|
|
125
140
|
'observe',
|
|
141
|
+
'would_dispatch',
|
|
126
142
|
]);
|
|
127
143
|
function getLogKindForTool(tool, result, err) {
|
|
128
144
|
if (err !== null)
|
|
@@ -142,6 +158,17 @@ function getLogKindForTool(tool, result, err) {
|
|
|
142
158
|
return 'read';
|
|
143
159
|
return 'read';
|
|
144
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Type guard for the `dispatched` shape of `LapMessageResponse`.
|
|
163
|
+
* Used to lift the stateDiff into the log entry without polluting
|
|
164
|
+
* the type chain with cross-cutting imports.
|
|
165
|
+
*/
|
|
166
|
+
function isDispatchedResult(result) {
|
|
167
|
+
return (result !== null &&
|
|
168
|
+
typeof result === 'object' &&
|
|
169
|
+
result.status === 'dispatched' &&
|
|
170
|
+
Array.isArray(result.stateDiff));
|
|
171
|
+
}
|
|
145
172
|
function extractVariant(tool, args) {
|
|
146
173
|
if (tool === 'send_message') {
|
|
147
174
|
const a = args;
|
|
@@ -165,6 +192,8 @@ function buildIntent(tool, args, annotations) {
|
|
|
165
192
|
}
|
|
166
193
|
if (tool === 'get_state')
|
|
167
194
|
return 'Read app state';
|
|
195
|
+
if (tool === 'query_state')
|
|
196
|
+
return 'Read state slice';
|
|
168
197
|
if (tool === 'list_actions')
|
|
169
198
|
return 'List available actions';
|
|
170
199
|
if (tool === 'describe_context')
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../../src/client/ws-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ClientFrame,\n ServerFrame,\n HelloFrame,\n LogEntry,\n LogKind,\n MessageAnnotations,\n} from '../protocol.js'\nimport { handleGetState, type GetStateHost } from './rpc/get-state.js'\nimport { handleSendMessage, type SendMessageHost } from './rpc/send-message.js'\nimport { handleListActions, type ListActionsHost } from './rpc/list-actions.js'\nimport { handleQueryDom, type QueryDomHost } from './rpc/query-dom.js'\nimport {\n handleDescribeVisibleContent,\n type DescribeVisibleHost,\n} from './rpc/describe-visible-content.js'\nimport { handleDescribeContext, type DescribeContextHost } from './rpc/describe-context.js'\nimport { handleObserve, type ObserveHost } from './rpc/observe.js'\n\nexport interface WsLike {\n send(data: string): void\n close(): void\n addEventListener(event: 'message', h: (e: { data: string | ArrayBuffer }) => void): void\n addEventListener(event: 'open' | 'close', h: () => void): void\n}\n\nexport type RpcHosts = GetStateHost &\n SendMessageHost &\n ListActionsHost &\n QueryDomHost &\n DescribeVisibleHost &\n DescribeContextHost &\n ObserveHost\n\nexport type HelloBuilder = () => HelloFrame\n\nexport type WsClient = {\n /** Resolve a pending confirmation; emits confirm-resolved frame to the server. */\n resolveConfirm(\n confirmId: string,\n outcome: 'confirmed' | 'user-cancelled',\n stateAfter?: unknown,\n ): void\n /** Emit a state-update frame so the server can resolve waitForChange promises. */\n emitStateUpdate(path: string, stateAfter: unknown): void\n /** Emit a log-append frame so the server can mirror client-observed actions to the audit sink. */\n emitLogAppend(entry: LogEntry): void\n /** Close the socket cleanly. */\n close(): void\n}\n\nexport type WsClientOpts = {\n /** Called once when the server sends an `{t: 'active'}` frame. Idempotent. */\n onActivated?: () => void\n /**\n * Called with every LogEntry emitted by the ws-client (one per rpc\n * dispatched or errored). Used by the factory to mirror the entries\n * into the app's local `agent.log` slice so the UI can show activity.\n * The ws-client still sends the outbound `log-append` frame to the\n * server regardless.\n */\n onLogEntry?: (entry: LogEntry) => void\n}\n\n/**\n * Wires up a WebSocket to serve rpc requests from the server. See spec §9.4.\n */\nexport function attachWsClient(\n ws: WsLike,\n rpc: RpcHosts,\n hello: HelloBuilder,\n opts: WsClientOpts = {},\n): WsClient {\n let activated = false\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(hello()))\n })\n ws.addEventListener('message', async (ev) => {\n let frame: ServerFrame\n try {\n const raw = typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data)\n frame = JSON.parse(raw) as ServerFrame\n } catch {\n return\n }\n if (frame.t === 'revoked') {\n ws.close()\n return\n }\n if (frame.t === 'active') {\n if (!activated) {\n activated = true\n opts.onActivated?.()\n }\n return\n }\n if (frame.t !== 'rpc') return\n let result: unknown\n let rpcErr: { code?: string; detail?: string } | null = null\n try {\n result = await dispatch(frame.tool, frame.args, rpc)\n const reply: ClientFrame = { t: 'rpc-reply', id: frame.id, result }\n ws.send(JSON.stringify(reply))\n } catch (e: unknown) {\n rpcErr = e as { code?: string; detail?: string }\n // When a plain JS exception bubbles up (TypeError, RangeError, etc.),\n // rpcErr has no .code/.detail. Enrich the detail with the actual\n // message + stack so the server/Claude can see the real cause.\n const detail =\n rpcErr.detail ??\n (e instanceof Error\n ? `${e.name}: ${e.message}${e.stack ? '\\n' + e.stack.split('\\n').slice(0, 5).join('\\n') : ''}`\n : undefined)\n const errFrame: ClientFrame = {\n t: 'rpc-error',\n id: frame.id,\n code: rpcErr.code ?? 'internal',\n detail,\n }\n ws.send(JSON.stringify(errFrame))\n // Also log to the browser console so operators see the real cause even\n // when the server/Claude just show \"internal\".\n console.error(`[llui-agent] rpc handler threw for ${frame.tool}:`, e)\n }\n const kind = getLogKindForTool(frame.tool, result, rpcErr)\n const logEntry: LogEntry = {\n id: frame.id,\n at: Date.now(),\n kind,\n variant: extractVariant(frame.tool, frame.args),\n intent: buildIntent(frame.tool, frame.args, rpc.getMsgAnnotations()),\n }\n opts.onLogEntry?.(logEntry)\n ws.send(JSON.stringify({ t: 'log-append', entry: logEntry } satisfies ClientFrame))\n })\n\n return {\n resolveConfirm(confirmId, outcome, stateAfter) {\n const frame: ClientFrame = {\n t: 'confirm-resolved',\n confirmId,\n outcome,\n stateAfter,\n }\n ws.send(JSON.stringify(frame))\n },\n emitStateUpdate(path, stateAfter) {\n const frame: ClientFrame = { t: 'state-update', path, stateAfter }\n ws.send(JSON.stringify(frame))\n },\n emitLogAppend(entry) {\n const frame: ClientFrame = { t: 'log-append', entry }\n ws.send(JSON.stringify(frame))\n },\n close() {\n ws.close()\n },\n }\n}\n\nasync function dispatch(tool: string, args: unknown, rpc: RpcHosts): Promise<unknown> {\n switch (tool) {\n case 'get_state':\n return handleGetState(rpc, (args ?? {}) as { path?: string })\n case 'list_actions':\n return handleListActions(rpc)\n case 'send_message':\n return handleSendMessage(rpc, args as never)\n case 'query_dom':\n return handleQueryDom(rpc, args as never)\n case 'describe_visible_content':\n return handleDescribeVisibleContent(rpc)\n case 'describe_context':\n return handleDescribeContext(rpc)\n case 'observe':\n return handleObserve(rpc)\n default:\n throw { code: 'invalid', detail: `unknown tool: ${tool}` }\n }\n}\n\nconst READ_TOOLS = new Set([\n 'get_state',\n 'list_actions',\n 'describe_context',\n 'query_dom',\n 'describe_visible_content',\n 'observe',\n])\n\nfunction getLogKindForTool(\n tool: string,\n result: unknown,\n err: { code?: string; detail?: string } | null,\n): LogKind {\n if (err !== null) return 'error'\n if (tool === 'send_message') {\n const r = result as { status?: string } | null\n const status = r?.status\n if (status === 'dispatched' || status === 'confirmed') return 'dispatched'\n if (status === 'pending-confirmation') return 'proposed'\n if (status === 'rejected') return 'blocked'\n return 'dispatched'\n }\n if (READ_TOOLS.has(tool)) return 'read'\n return 'read'\n}\n\nfunction extractVariant(tool: string, args: unknown): string | undefined {\n if (tool === 'send_message') {\n const a = args as { msg?: { type?: string } } | null\n const t = a?.msg?.type\n return typeof t === 'string' ? t : undefined\n }\n return undefined\n}\n\n// Human-readable label for each rpc. For send_message, prefer the @intent\n// annotation authored on the Msg union; fall back to the raw variant name.\n// For read tools, return a short fixed label so the activity feed doesn't\n// show opaque tool ids like \"describe_visible_content\".\nfunction buildIntent(\n tool: string,\n args: unknown,\n annotations: Record<string, MessageAnnotations> | null,\n): string {\n if (tool === 'send_message') {\n const a = args as { msg?: { type?: string } } | null\n const variant = typeof a?.msg?.type === 'string' ? a.msg.type : undefined\n const annotated = variant ? annotations?.[variant]?.intent : null\n if (annotated) return annotated\n return variant ?? 'Send message'\n }\n if (tool === 'get_state') return 'Read app state'\n if (tool === 'list_actions') return 'List available actions'\n if (tool === 'describe_context') return 'Read current context'\n if (tool === 'describe_visible_content') return 'Read visible content'\n if (tool === 'query_dom') {\n const a = args as { name?: string } | null\n return a?.name ? `Query DOM: ${a.name}` : 'Query DOM'\n }\n return tool\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ws-client.js","sourceRoot":"","sources":["../../src/client/ws-client.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ClientFrame,\n ServerFrame,\n HelloFrame,\n LogEntry,\n LogKind,\n MessageAnnotations,\n} from '../protocol.js'\nimport { handleGetState, type GetStateHost } from './rpc/get-state.js'\nimport { handleQueryState, type QueryStateHost } from './rpc/query-state.js'\nimport { handleWouldDispatch, type WouldDispatchHost } from './rpc/would-dispatch.js'\nimport { handleSendMessage, type SendMessageHost } from './rpc/send-message.js'\nimport { handleListActions, type ListActionsHost } from './rpc/list-actions.js'\nimport { handleQueryDom, type QueryDomHost } from './rpc/query-dom.js'\nimport {\n handleDescribeVisibleContent,\n type DescribeVisibleHost,\n} from './rpc/describe-visible-content.js'\nimport { handleDescribeContext, type DescribeContextHost } from './rpc/describe-context.js'\nimport { handleObserve, type ObserveHost } from './rpc/observe.js'\n\nexport interface WsLike {\n send(data: string): void\n close(): void\n addEventListener(event: 'message', h: (e: { data: string | ArrayBuffer }) => void): void\n addEventListener(event: 'open' | 'close', h: () => void): void\n}\n\nexport type RpcHosts = GetStateHost &\n QueryStateHost &\n SendMessageHost &\n ListActionsHost &\n QueryDomHost &\n DescribeVisibleHost &\n DescribeContextHost &\n ObserveHost &\n WouldDispatchHost\n\nexport type HelloBuilder = () => HelloFrame\n\nexport type WsClient = {\n /** Resolve a pending confirmation; emits confirm-resolved frame to the server. */\n resolveConfirm(\n confirmId: string,\n outcome: 'confirmed' | 'user-cancelled',\n stateAfter?: unknown,\n ): void\n /** Emit a state-update frame so the server can resolve waitForChange promises. */\n emitStateUpdate(path: string, stateAfter: unknown): void\n /** Emit a log-append frame so the server can mirror client-observed actions to the audit sink. */\n emitLogAppend(entry: LogEntry): void\n /** Close the socket cleanly. */\n close(): void\n}\n\nexport type WsClientOpts = {\n /** Called once when the server sends an `{t: 'active'}` frame. Idempotent. */\n onActivated?: () => void\n /**\n * Called with every LogEntry emitted by the ws-client (one per rpc\n * dispatched or errored). Used by the factory to mirror the entries\n * into the app's local `agent.log` slice so the UI can show activity.\n * The ws-client still sends the outbound `log-append` frame to the\n * server regardless.\n */\n onLogEntry?: (entry: LogEntry) => void\n}\n\n/**\n * Wires up a WebSocket to serve rpc requests from the server. See spec §9.4.\n */\nexport function attachWsClient(\n ws: WsLike,\n rpc: RpcHosts,\n hello: HelloBuilder,\n opts: WsClientOpts = {},\n): WsClient {\n let activated = false\n ws.addEventListener('open', () => {\n ws.send(JSON.stringify(hello()))\n })\n ws.addEventListener('message', async (ev) => {\n let frame: ServerFrame\n try {\n const raw = typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data)\n frame = JSON.parse(raw) as ServerFrame\n } catch {\n return\n }\n if (frame.t === 'revoked') {\n ws.close()\n return\n }\n if (frame.t === 'active') {\n if (!activated) {\n activated = true\n opts.onActivated?.()\n }\n return\n }\n if (frame.t !== 'rpc') return\n let result: unknown\n let rpcErr: { code?: string; detail?: string } | null = null\n try {\n result = await dispatch(frame.tool, frame.args, rpc)\n const reply: ClientFrame = { t: 'rpc-reply', id: frame.id, result }\n ws.send(JSON.stringify(reply))\n } catch (e: unknown) {\n rpcErr = e as { code?: string; detail?: string }\n // When a plain JS exception bubbles up (TypeError, RangeError, etc.),\n // rpcErr has no .code/.detail. Enrich the detail with the actual\n // message + stack so the server/Claude can see the real cause.\n const detail =\n rpcErr.detail ??\n (e instanceof Error\n ? `${e.name}: ${e.message}${e.stack ? '\\n' + e.stack.split('\\n').slice(0, 5).join('\\n') : ''}`\n : undefined)\n const errFrame: ClientFrame = {\n t: 'rpc-error',\n id: frame.id,\n code: rpcErr.code ?? 'internal',\n detail,\n }\n ws.send(JSON.stringify(errFrame))\n // Also log to the browser console so operators see the real cause even\n // when the server/Claude just show \"internal\".\n console.error(`[llui-agent] rpc handler threw for ${frame.tool}:`, e)\n }\n const kind = getLogKindForTool(frame.tool, result, rpcErr)\n const logEntry: LogEntry = {\n id: frame.id,\n at: Date.now(),\n kind,\n variant: extractVariant(frame.tool, frame.args),\n intent: buildIntent(frame.tool, frame.args, rpc.getMsgAnnotations()),\n }\n // For successful send_message dispatches, the stateDiff is part\n // of the response. Lifting it into the log entry means the agent\n // can read its own past actions with full \"what changed\" detail\n // without re-querying state — essential for self-correcting\n // behavior over multi-step flows.\n if (frame.tool === 'send_message' && rpcErr === null && isDispatchedResult(result)) {\n logEntry.stateDiff = result.stateDiff\n }\n opts.onLogEntry?.(logEntry)\n ws.send(JSON.stringify({ t: 'log-append', entry: logEntry } satisfies ClientFrame))\n })\n\n return {\n resolveConfirm(confirmId, outcome, stateAfter) {\n const frame: ClientFrame = {\n t: 'confirm-resolved',\n confirmId,\n outcome,\n stateAfter,\n }\n ws.send(JSON.stringify(frame))\n },\n emitStateUpdate(path, stateAfter) {\n const frame: ClientFrame = { t: 'state-update', path, stateAfter }\n ws.send(JSON.stringify(frame))\n },\n emitLogAppend(entry) {\n const frame: ClientFrame = { t: 'log-append', entry }\n ws.send(JSON.stringify(frame))\n },\n close() {\n ws.close()\n },\n }\n}\n\nasync function dispatch(tool: string, args: unknown, rpc: RpcHosts): Promise<unknown> {\n switch (tool) {\n case 'get_state':\n return handleGetState(rpc, (args ?? {}) as { path?: string })\n case 'query_state':\n return handleQueryState(rpc, (args ?? {}) as { path: string })\n case 'list_actions':\n return handleListActions(rpc)\n case 'send_message':\n return handleSendMessage(rpc, args as never)\n case 'query_dom':\n return handleQueryDom(rpc, args as never)\n case 'describe_visible_content':\n return handleDescribeVisibleContent(rpc)\n case 'describe_context':\n return handleDescribeContext(rpc)\n case 'observe':\n return handleObserve(rpc)\n case 'would_dispatch':\n return handleWouldDispatch(rpc, args as never)\n default:\n throw { code: 'invalid', detail: `unknown tool: ${tool}` }\n }\n}\n\nconst READ_TOOLS = new Set([\n 'get_state',\n 'query_state',\n 'list_actions',\n 'describe_context',\n 'query_dom',\n 'describe_visible_content',\n 'observe',\n 'would_dispatch',\n])\n\nfunction getLogKindForTool(\n tool: string,\n result: unknown,\n err: { code?: string; detail?: string } | null,\n): LogKind {\n if (err !== null) return 'error'\n if (tool === 'send_message') {\n const r = result as { status?: string } | null\n const status = r?.status\n if (status === 'dispatched' || status === 'confirmed') return 'dispatched'\n if (status === 'pending-confirmation') return 'proposed'\n if (status === 'rejected') return 'blocked'\n return 'dispatched'\n }\n if (READ_TOOLS.has(tool)) return 'read'\n return 'read'\n}\n\n/**\n * Type guard for the `dispatched` shape of `LapMessageResponse`.\n * Used to lift the stateDiff into the log entry without polluting\n * the type chain with cross-cutting imports.\n */\nfunction isDispatchedResult(\n result: unknown,\n): result is { status: 'dispatched'; stateDiff: import('../state-diff.js').StateDiff } {\n return (\n result !== null &&\n typeof result === 'object' &&\n (result as { status?: unknown }).status === 'dispatched' &&\n Array.isArray((result as { stateDiff?: unknown }).stateDiff)\n )\n}\n\nfunction extractVariant(tool: string, args: unknown): string | undefined {\n if (tool === 'send_message') {\n const a = args as { msg?: { type?: string } } | null\n const t = a?.msg?.type\n return typeof t === 'string' ? t : undefined\n }\n return undefined\n}\n\n// Human-readable label for each rpc. For send_message, prefer the @intent\n// annotation authored on the Msg union; fall back to the raw variant name.\n// For read tools, return a short fixed label so the activity feed doesn't\n// show opaque tool ids like \"describe_visible_content\".\nfunction buildIntent(\n tool: string,\n args: unknown,\n annotations: Record<string, MessageAnnotations> | null,\n): string {\n if (tool === 'send_message') {\n const a = args as { msg?: { type?: string } } | null\n const variant = typeof a?.msg?.type === 'string' ? a.msg.type : undefined\n const annotated = variant ? annotations?.[variant]?.intent : null\n if (annotated) return annotated\n return variant ?? 'Send message'\n }\n if (tool === 'get_state') return 'Read app state'\n if (tool === 'query_state') return 'Read state slice'\n if (tool === 'list_actions') return 'List available actions'\n if (tool === 'describe_context') return 'Read current context'\n if (tool === 'describe_visible_content') return 'Read visible content'\n if (tool === 'query_dom') {\n const a = args as { name?: string } | null\n return a?.name ? `Query DOM: ${a.name}` : 'Query DOM'\n }\n return tool\n}\n"]}
|
package/dist/codecs.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-format codecs for non-JSON-safe values flowing across the LAP
|
|
3
|
+
* boundary.
|
|
4
|
+
*
|
|
5
|
+
* JSON natively supports `string | number | boolean | null | array |
|
|
6
|
+
* object`. Component messages and state often carry values that don't
|
|
7
|
+
* round-trip through JSON: `Date`, `Blob`, `File`, `Map`, `Set`,
|
|
8
|
+
* `BigInt`, `ArrayBuffer`. A codec is the convention that lets these
|
|
9
|
+
* cross the wire without forcing every component author to invent
|
|
10
|
+
* their own envelope.
|
|
11
|
+
*
|
|
12
|
+
* **Wire convention.** A non-JSON-safe runtime value travels as a
|
|
13
|
+
* tagged object:
|
|
14
|
+
*
|
|
15
|
+
* { __codec: '<name>', wire: <encoded form> }
|
|
16
|
+
*
|
|
17
|
+
* The runtime walks every value crossing the LAP boundary and applies
|
|
18
|
+
* the codec registry symmetrically:
|
|
19
|
+
*
|
|
20
|
+
* - **Outgoing** (component → agent, e.g. `stateAfter`): the encoder
|
|
21
|
+
* looks up a codec whose `matchesRuntime` returns true and replaces
|
|
22
|
+
* the value with its tagged shape.
|
|
23
|
+
* - **Incoming** (agent → component, e.g. dispatched `msg`): the
|
|
24
|
+
* decoder detects the tagged shape, calls the codec's `decode`,
|
|
25
|
+
* and substitutes the runtime value before `update()` runs.
|
|
26
|
+
*
|
|
27
|
+
* Component code never observes the tagged form. By the time a
|
|
28
|
+
* reducer sees `msg.value`, a real `Date` (or whatever) is in place;
|
|
29
|
+
* by the time the agent reads `stateAfter`, every `Date` has been
|
|
30
|
+
* encoded.
|
|
31
|
+
*
|
|
32
|
+
* **Authoring.** When a Msg variant carries a non-JSON-safe field,
|
|
33
|
+
* tag the variant's JSDoc with both `@intent` and `@codec("<name>")`.
|
|
34
|
+
* For example, a date-input message:
|
|
35
|
+
*
|
|
36
|
+
* @intent("Set the parsed date")
|
|
37
|
+
* @codec("iso-date")
|
|
38
|
+
* | { type: 'setValue'; value: Date | null }
|
|
39
|
+
*
|
|
40
|
+
* The `@codec` tag is documentation for human readers and the
|
|
41
|
+
* eventual schema generator that publishes the message catalogue to
|
|
42
|
+
* the agent client. The runtime encode/decode is registry-driven and
|
|
43
|
+
* doesn't need per-field metadata.
|
|
44
|
+
*
|
|
45
|
+
* **Defaults.** `makeDefaultCodecs()` ships with `iso-date` (Date ↔
|
|
46
|
+
* ISO 8601 string) and `epoch-millis` (Date ↔ number). The
|
|
47
|
+
* `epoch-millis` codec is registered but its `matchesRuntime` returns
|
|
48
|
+
* `false` by default — it's available for explicit decode but doesn't
|
|
49
|
+
* shadow `iso-date` on the encode side. Consumers who prefer epoch
|
|
50
|
+
* millis can construct a registry that lists `epoch-millis` first.
|
|
51
|
+
*
|
|
52
|
+
* **File / Blob.** Not in the default registry. File/Blob handling is
|
|
53
|
+
* environment-specific (browser File API vs. Node Buffer vs. workers)
|
|
54
|
+
* and the encoded form is large enough that consumers should opt in
|
|
55
|
+
* deliberately. Provide your own codec via `registry.register({...})`
|
|
56
|
+
* when a component needs it.
|
|
57
|
+
*/
|
|
58
|
+
export declare const WIRE_TAG = "__codec";
|
|
59
|
+
export declare const WIRE_VALUE = "wire";
|
|
60
|
+
export interface AgentCodec<TWire = unknown, TRuntime = unknown> {
|
|
61
|
+
/** Stable identifier used as the value of the `__codec` tag. */
|
|
62
|
+
readonly name: string;
|
|
63
|
+
/** Convert a runtime value to its wire representation. */
|
|
64
|
+
encode(value: TRuntime): TWire;
|
|
65
|
+
/** Convert a wire representation back to the runtime value. */
|
|
66
|
+
decode(wire: TWire): TRuntime;
|
|
67
|
+
/**
|
|
68
|
+
* Predicate identifying runtime values this codec should handle. The
|
|
69
|
+
* universal encoder calls this on every value it walks; the first
|
|
70
|
+
* codec to return `true` claims the value.
|
|
71
|
+
*/
|
|
72
|
+
matchesRuntime(value: unknown): boolean;
|
|
73
|
+
}
|
|
74
|
+
export declare class CodecRegistry {
|
|
75
|
+
private byName;
|
|
76
|
+
private inOrder;
|
|
77
|
+
register(codec: AgentCodec): void;
|
|
78
|
+
get(name: string): AgentCodec | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* First codec whose `matchesRuntime` returns true for `value`, or
|
|
81
|
+
* `undefined`. Used by the encoder to decide how to wrap arbitrary
|
|
82
|
+
* runtime values.
|
|
83
|
+
*/
|
|
84
|
+
matchRuntime(value: unknown): AgentCodec | undefined;
|
|
85
|
+
clone(): CodecRegistry;
|
|
86
|
+
}
|
|
87
|
+
export declare const isoDateCodec: AgentCodec<string, Date>;
|
|
88
|
+
export declare const epochMillisCodec: AgentCodec<number, Date>;
|
|
89
|
+
export declare function makeDefaultCodecs(): CodecRegistry;
|
|
90
|
+
/**
|
|
91
|
+
* Recursively walk `value`. For any node a codec claims via
|
|
92
|
+
* `matchesRuntime`, replace it with `{ __codec, wire }`. Returns a
|
|
93
|
+
* fresh structure — never mutates the input.
|
|
94
|
+
*
|
|
95
|
+
* The codec match takes precedence over object/array recursion: a
|
|
96
|
+
* `Date` is technically `typeof === 'object'`, but the iso-date codec
|
|
97
|
+
* should claim it before the generic walker tries to enumerate keys.
|
|
98
|
+
*/
|
|
99
|
+
export declare function encodeForWire(value: unknown, registry: CodecRegistry): unknown;
|
|
100
|
+
/**
|
|
101
|
+
* Recursively walk `value`. For any tagged shape `{ __codec, wire }`,
|
|
102
|
+
* look up the codec by name and replace with the decoded runtime
|
|
103
|
+
* value. Tagged shapes whose codec name is unknown pass through
|
|
104
|
+
* untouched so the consumer can inspect them directly.
|
|
105
|
+
*/
|
|
106
|
+
export declare function decodeFromWire(value: unknown, registry: CodecRegistry): unknown;
|
|
107
|
+
//# sourceMappingURL=codecs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../src/codecs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAEH,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,UAAU,SAAS,CAAA;AAEhC,MAAM,WAAW,UAAU,CAAC,KAAK,GAAG,OAAO,EAAE,QAAQ,GAAG,OAAO;IAC7D,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0DAA0D;IAC1D,MAAM,CAAC,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAA;IAC9B,+DAA+D;IAC/D,MAAM,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAA;IAC7B;;;;OAIG;IACH,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAA;CACxC;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,OAAO,CAAmB;IAElC,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAOjC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIzC;;;;OAIG;IACH,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,GAAG,SAAS;IAKpD,KAAK,IAAI,aAAa;CAKvB;AAED,eAAO,MAAM,YAAY,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CAKjD,CAAA;AAED,eAAO,MAAM,gBAAgB,EAAE,UAAU,CAAC,MAAM,EAAE,IAAI,CASrD,CAAA;AAED,wBAAgB,iBAAiB,IAAI,aAAa,CAKjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAa9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAe/E"}
|
package/dist/codecs.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-format codecs for non-JSON-safe values flowing across the LAP
|
|
3
|
+
* boundary.
|
|
4
|
+
*
|
|
5
|
+
* JSON natively supports `string | number | boolean | null | array |
|
|
6
|
+
* object`. Component messages and state often carry values that don't
|
|
7
|
+
* round-trip through JSON: `Date`, `Blob`, `File`, `Map`, `Set`,
|
|
8
|
+
* `BigInt`, `ArrayBuffer`. A codec is the convention that lets these
|
|
9
|
+
* cross the wire without forcing every component author to invent
|
|
10
|
+
* their own envelope.
|
|
11
|
+
*
|
|
12
|
+
* **Wire convention.** A non-JSON-safe runtime value travels as a
|
|
13
|
+
* tagged object:
|
|
14
|
+
*
|
|
15
|
+
* { __codec: '<name>', wire: <encoded form> }
|
|
16
|
+
*
|
|
17
|
+
* The runtime walks every value crossing the LAP boundary and applies
|
|
18
|
+
* the codec registry symmetrically:
|
|
19
|
+
*
|
|
20
|
+
* - **Outgoing** (component → agent, e.g. `stateAfter`): the encoder
|
|
21
|
+
* looks up a codec whose `matchesRuntime` returns true and replaces
|
|
22
|
+
* the value with its tagged shape.
|
|
23
|
+
* - **Incoming** (agent → component, e.g. dispatched `msg`): the
|
|
24
|
+
* decoder detects the tagged shape, calls the codec's `decode`,
|
|
25
|
+
* and substitutes the runtime value before `update()` runs.
|
|
26
|
+
*
|
|
27
|
+
* Component code never observes the tagged form. By the time a
|
|
28
|
+
* reducer sees `msg.value`, a real `Date` (or whatever) is in place;
|
|
29
|
+
* by the time the agent reads `stateAfter`, every `Date` has been
|
|
30
|
+
* encoded.
|
|
31
|
+
*
|
|
32
|
+
* **Authoring.** When a Msg variant carries a non-JSON-safe field,
|
|
33
|
+
* tag the variant's JSDoc with both `@intent` and `@codec("<name>")`.
|
|
34
|
+
* For example, a date-input message:
|
|
35
|
+
*
|
|
36
|
+
* @intent("Set the parsed date")
|
|
37
|
+
* @codec("iso-date")
|
|
38
|
+
* | { type: 'setValue'; value: Date | null }
|
|
39
|
+
*
|
|
40
|
+
* The `@codec` tag is documentation for human readers and the
|
|
41
|
+
* eventual schema generator that publishes the message catalogue to
|
|
42
|
+
* the agent client. The runtime encode/decode is registry-driven and
|
|
43
|
+
* doesn't need per-field metadata.
|
|
44
|
+
*
|
|
45
|
+
* **Defaults.** `makeDefaultCodecs()` ships with `iso-date` (Date ↔
|
|
46
|
+
* ISO 8601 string) and `epoch-millis` (Date ↔ number). The
|
|
47
|
+
* `epoch-millis` codec is registered but its `matchesRuntime` returns
|
|
48
|
+
* `false` by default — it's available for explicit decode but doesn't
|
|
49
|
+
* shadow `iso-date` on the encode side. Consumers who prefer epoch
|
|
50
|
+
* millis can construct a registry that lists `epoch-millis` first.
|
|
51
|
+
*
|
|
52
|
+
* **File / Blob.** Not in the default registry. File/Blob handling is
|
|
53
|
+
* environment-specific (browser File API vs. Node Buffer vs. workers)
|
|
54
|
+
* and the encoded form is large enough that consumers should opt in
|
|
55
|
+
* deliberately. Provide your own codec via `registry.register({...})`
|
|
56
|
+
* when a component needs it.
|
|
57
|
+
*/
|
|
58
|
+
export const WIRE_TAG = '__codec';
|
|
59
|
+
export const WIRE_VALUE = 'wire';
|
|
60
|
+
export class CodecRegistry {
|
|
61
|
+
byName = new Map();
|
|
62
|
+
inOrder = [];
|
|
63
|
+
register(codec) {
|
|
64
|
+
this.byName.set(codec.name, codec);
|
|
65
|
+
const idx = this.inOrder.findIndex((c) => c.name === codec.name);
|
|
66
|
+
if (idx >= 0)
|
|
67
|
+
this.inOrder[idx] = codec;
|
|
68
|
+
else
|
|
69
|
+
this.inOrder.push(codec);
|
|
70
|
+
}
|
|
71
|
+
get(name) {
|
|
72
|
+
return this.byName.get(name);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* First codec whose `matchesRuntime` returns true for `value`, or
|
|
76
|
+
* `undefined`. Used by the encoder to decide how to wrap arbitrary
|
|
77
|
+
* runtime values.
|
|
78
|
+
*/
|
|
79
|
+
matchRuntime(value) {
|
|
80
|
+
for (const c of this.inOrder)
|
|
81
|
+
if (c.matchesRuntime(value))
|
|
82
|
+
return c;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
clone() {
|
|
86
|
+
const r = new CodecRegistry();
|
|
87
|
+
for (const c of this.inOrder)
|
|
88
|
+
r.register(c);
|
|
89
|
+
return r;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export const isoDateCodec = {
|
|
93
|
+
name: 'iso-date',
|
|
94
|
+
matchesRuntime: (v) => v instanceof Date && !Number.isNaN(v.getTime()),
|
|
95
|
+
encode: (d) => d.toISOString(),
|
|
96
|
+
decode: (s) => new Date(s),
|
|
97
|
+
};
|
|
98
|
+
export const epochMillisCodec = {
|
|
99
|
+
name: 'epoch-millis',
|
|
100
|
+
// Returns `false` by default so `iso-date` claims Date values when
|
|
101
|
+
// both are registered. Consumers who prefer epoch millis register
|
|
102
|
+
// an instance with `matchesRuntime: (v) => v instanceof Date` to
|
|
103
|
+
// shadow `iso-date` on the encode side.
|
|
104
|
+
matchesRuntime: () => false,
|
|
105
|
+
encode: (d) => d.getTime(),
|
|
106
|
+
decode: (n) => new Date(n),
|
|
107
|
+
};
|
|
108
|
+
export function makeDefaultCodecs() {
|
|
109
|
+
const r = new CodecRegistry();
|
|
110
|
+
r.register(isoDateCodec);
|
|
111
|
+
r.register(epochMillisCodec);
|
|
112
|
+
return r;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Recursively walk `value`. For any node a codec claims via
|
|
116
|
+
* `matchesRuntime`, replace it with `{ __codec, wire }`. Returns a
|
|
117
|
+
* fresh structure — never mutates the input.
|
|
118
|
+
*
|
|
119
|
+
* The codec match takes precedence over object/array recursion: a
|
|
120
|
+
* `Date` is technically `typeof === 'object'`, but the iso-date codec
|
|
121
|
+
* should claim it before the generic walker tries to enumerate keys.
|
|
122
|
+
*/
|
|
123
|
+
export function encodeForWire(value, registry) {
|
|
124
|
+
if (value === null || value === undefined)
|
|
125
|
+
return value;
|
|
126
|
+
const codec = registry.matchRuntime(value);
|
|
127
|
+
if (codec)
|
|
128
|
+
return { [WIRE_TAG]: codec.name, [WIRE_VALUE]: codec.encode(value) };
|
|
129
|
+
if (Array.isArray(value))
|
|
130
|
+
return value.map((v) => encodeForWire(v, registry));
|
|
131
|
+
if (typeof value === 'object') {
|
|
132
|
+
const out = {};
|
|
133
|
+
for (const [k, v] of Object.entries(value)) {
|
|
134
|
+
out[k] = encodeForWire(v, registry);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Recursively walk `value`. For any tagged shape `{ __codec, wire }`,
|
|
142
|
+
* look up the codec by name and replace with the decoded runtime
|
|
143
|
+
* value. Tagged shapes whose codec name is unknown pass through
|
|
144
|
+
* untouched so the consumer can inspect them directly.
|
|
145
|
+
*/
|
|
146
|
+
export function decodeFromWire(value, registry) {
|
|
147
|
+
if (value === null || value === undefined)
|
|
148
|
+
return value;
|
|
149
|
+
if (Array.isArray(value))
|
|
150
|
+
return value.map((v) => decodeFromWire(v, registry));
|
|
151
|
+
if (typeof value !== 'object')
|
|
152
|
+
return value;
|
|
153
|
+
const obj = value;
|
|
154
|
+
if (typeof obj[WIRE_TAG] === 'string' && WIRE_VALUE in obj) {
|
|
155
|
+
const codec = registry.get(obj[WIRE_TAG]);
|
|
156
|
+
if (codec)
|
|
157
|
+
return codec.decode(obj[WIRE_VALUE]);
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
const out = {};
|
|
161
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
162
|
+
out[k] = decodeFromWire(v, registry);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=codecs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codecs.js","sourceRoot":"","sources":["../src/codecs.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Wire-format codecs for non-JSON-safe values flowing across the LAP\n * boundary.\n *\n * JSON natively supports `string | number | boolean | null | array |\n * object`. Component messages and state often carry values that don't\n * round-trip through JSON: `Date`, `Blob`, `File`, `Map`, `Set`,\n * `BigInt`, `ArrayBuffer`. A codec is the convention that lets these\n * cross the wire without forcing every component author to invent\n * their own envelope.\n *\n * **Wire convention.** A non-JSON-safe runtime value travels as a\n * tagged object:\n *\n * { __codec: '<name>', wire: <encoded form> }\n *\n * The runtime walks every value crossing the LAP boundary and applies\n * the codec registry symmetrically:\n *\n * - **Outgoing** (component → agent, e.g. `stateAfter`): the encoder\n * looks up a codec whose `matchesRuntime` returns true and replaces\n * the value with its tagged shape.\n * - **Incoming** (agent → component, e.g. dispatched `msg`): the\n * decoder detects the tagged shape, calls the codec's `decode`,\n * and substitutes the runtime value before `update()` runs.\n *\n * Component code never observes the tagged form. By the time a\n * reducer sees `msg.value`, a real `Date` (or whatever) is in place;\n * by the time the agent reads `stateAfter`, every `Date` has been\n * encoded.\n *\n * **Authoring.** When a Msg variant carries a non-JSON-safe field,\n * tag the variant's JSDoc with both `@intent` and `@codec(\"<name>\")`.\n * For example, a date-input message:\n *\n * @intent(\"Set the parsed date\")\n * @codec(\"iso-date\")\n * | { type: 'setValue'; value: Date | null }\n *\n * The `@codec` tag is documentation for human readers and the\n * eventual schema generator that publishes the message catalogue to\n * the agent client. The runtime encode/decode is registry-driven and\n * doesn't need per-field metadata.\n *\n * **Defaults.** `makeDefaultCodecs()` ships with `iso-date` (Date ↔\n * ISO 8601 string) and `epoch-millis` (Date ↔ number). The\n * `epoch-millis` codec is registered but its `matchesRuntime` returns\n * `false` by default — it's available for explicit decode but doesn't\n * shadow `iso-date` on the encode side. Consumers who prefer epoch\n * millis can construct a registry that lists `epoch-millis` first.\n *\n * **File / Blob.** Not in the default registry. File/Blob handling is\n * environment-specific (browser File API vs. Node Buffer vs. workers)\n * and the encoded form is large enough that consumers should opt in\n * deliberately. Provide your own codec via `registry.register({...})`\n * when a component needs it.\n */\n\nexport const WIRE_TAG = '__codec'\nexport const WIRE_VALUE = 'wire'\n\nexport interface AgentCodec<TWire = unknown, TRuntime = unknown> {\n /** Stable identifier used as the value of the `__codec` tag. */\n readonly name: string\n /** Convert a runtime value to its wire representation. */\n encode(value: TRuntime): TWire\n /** Convert a wire representation back to the runtime value. */\n decode(wire: TWire): TRuntime\n /**\n * Predicate identifying runtime values this codec should handle. The\n * universal encoder calls this on every value it walks; the first\n * codec to return `true` claims the value.\n */\n matchesRuntime(value: unknown): boolean\n}\n\nexport class CodecRegistry {\n private byName = new Map<string, AgentCodec>()\n private inOrder: AgentCodec[] = []\n\n register(codec: AgentCodec): void {\n this.byName.set(codec.name, codec)\n const idx = this.inOrder.findIndex((c) => c.name === codec.name)\n if (idx >= 0) this.inOrder[idx] = codec\n else this.inOrder.push(codec)\n }\n\n get(name: string): AgentCodec | undefined {\n return this.byName.get(name)\n }\n\n /**\n * First codec whose `matchesRuntime` returns true for `value`, or\n * `undefined`. Used by the encoder to decide how to wrap arbitrary\n * runtime values.\n */\n matchRuntime(value: unknown): AgentCodec | undefined {\n for (const c of this.inOrder) if (c.matchesRuntime(value)) return c\n return undefined\n }\n\n clone(): CodecRegistry {\n const r = new CodecRegistry()\n for (const c of this.inOrder) r.register(c)\n return r\n }\n}\n\nexport const isoDateCodec: AgentCodec<string, Date> = {\n name: 'iso-date',\n matchesRuntime: (v) => v instanceof Date && !Number.isNaN(v.getTime()),\n encode: (d) => d.toISOString(),\n decode: (s) => new Date(s),\n}\n\nexport const epochMillisCodec: AgentCodec<number, Date> = {\n name: 'epoch-millis',\n // Returns `false` by default so `iso-date` claims Date values when\n // both are registered. Consumers who prefer epoch millis register\n // an instance with `matchesRuntime: (v) => v instanceof Date` to\n // shadow `iso-date` on the encode side.\n matchesRuntime: () => false,\n encode: (d) => d.getTime(),\n decode: (n) => new Date(n),\n}\n\nexport function makeDefaultCodecs(): CodecRegistry {\n const r = new CodecRegistry()\n r.register(isoDateCodec)\n r.register(epochMillisCodec)\n return r\n}\n\n/**\n * Recursively walk `value`. For any node a codec claims via\n * `matchesRuntime`, replace it with `{ __codec, wire }`. Returns a\n * fresh structure — never mutates the input.\n *\n * The codec match takes precedence over object/array recursion: a\n * `Date` is technically `typeof === 'object'`, but the iso-date codec\n * should claim it before the generic walker tries to enumerate keys.\n */\nexport function encodeForWire(value: unknown, registry: CodecRegistry): unknown {\n if (value === null || value === undefined) return value\n const codec = registry.matchRuntime(value)\n if (codec) return { [WIRE_TAG]: codec.name, [WIRE_VALUE]: codec.encode(value) }\n if (Array.isArray(value)) return value.map((v) => encodeForWire(v, registry))\n if (typeof value === 'object') {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(value as object)) {\n out[k] = encodeForWire(v, registry)\n }\n return out\n }\n return value\n}\n\n/**\n * Recursively walk `value`. For any tagged shape `{ __codec, wire }`,\n * look up the codec by name and replace with the decoded runtime\n * value. Tagged shapes whose codec name is unknown pass through\n * untouched so the consumer can inspect them directly.\n */\nexport function decodeFromWire(value: unknown, registry: CodecRegistry): unknown {\n if (value === null || value === undefined) return value\n if (Array.isArray(value)) return value.map((v) => decodeFromWire(v, registry))\n if (typeof value !== 'object') return value\n const obj = value as Record<string, unknown>\n if (typeof obj[WIRE_TAG] === 'string' && WIRE_VALUE in obj) {\n const codec = registry.get(obj[WIRE_TAG] as string)\n if (codec) return codec.decode(obj[WIRE_VALUE])\n return value\n }\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj)) {\n out[k] = decodeFromWire(v, registry)\n }\n return out\n}\n"]}
|