@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.
Files changed (72) hide show
  1. package/README.md +82 -1
  2. package/dist/client/agentConfirm.d.ts +48 -18
  3. package/dist/client/agentConfirm.d.ts.map +1 -1
  4. package/dist/client/agentConfirm.js +28 -25
  5. package/dist/client/agentConfirm.js.map +1 -1
  6. package/dist/client/agentConnect.d.ts +95 -34
  7. package/dist/client/agentConnect.d.ts.map +1 -1
  8. package/dist/client/agentConnect.js +81 -47
  9. package/dist/client/agentConnect.js.map +1 -1
  10. package/dist/client/agentLog.d.ts +31 -14
  11. package/dist/client/agentLog.d.ts.map +1 -1
  12. package/dist/client/agentLog.js +39 -20
  13. package/dist/client/agentLog.js.map +1 -1
  14. package/dist/client/effect-handler.d.ts +23 -0
  15. package/dist/client/effect-handler.d.ts.map +1 -1
  16. package/dist/client/effect-handler.js +185 -126
  17. package/dist/client/effect-handler.js.map +1 -1
  18. package/dist/client/effects.d.ts +13 -2
  19. package/dist/client/effects.d.ts.map +1 -1
  20. package/dist/client/effects.js.map +1 -1
  21. package/dist/client/factory.d.ts +55 -3
  22. package/dist/client/factory.d.ts.map +1 -1
  23. package/dist/client/factory.js +30 -5
  24. package/dist/client/factory.js.map +1 -1
  25. package/dist/client/rpc/describe-visible-content.d.ts +18 -5
  26. package/dist/client/rpc/describe-visible-content.d.ts.map +1 -1
  27. package/dist/client/rpc/describe-visible-content.js +112 -7
  28. package/dist/client/rpc/describe-visible-content.js.map +1 -1
  29. package/dist/client/rpc/list-actions.d.ts +52 -2
  30. package/dist/client/rpc/list-actions.d.ts.map +1 -1
  31. package/dist/client/rpc/list-actions.js +187 -5
  32. package/dist/client/rpc/list-actions.js.map +1 -1
  33. package/dist/client/rpc/query-state.d.ts +32 -0
  34. package/dist/client/rpc/query-state.d.ts.map +1 -0
  35. package/dist/client/rpc/query-state.js +82 -0
  36. package/dist/client/rpc/query-state.js.map +1 -0
  37. package/dist/client/rpc/send-message.d.ts +2 -0
  38. package/dist/client/rpc/send-message.d.ts.map +1 -1
  39. package/dist/client/rpc/send-message.js +119 -9
  40. package/dist/client/rpc/send-message.js.map +1 -1
  41. package/dist/client/rpc/would-dispatch.d.ts +66 -0
  42. package/dist/client/rpc/would-dispatch.d.ts.map +1 -0
  43. package/dist/client/rpc/would-dispatch.js +21 -0
  44. package/dist/client/rpc/would-dispatch.js.map +1 -0
  45. package/dist/client/ws-client.d.ts +3 -1
  46. package/dist/client/ws-client.d.ts.map +1 -1
  47. package/dist/client/ws-client.js +29 -0
  48. package/dist/client/ws-client.js.map +1 -1
  49. package/dist/codecs.d.ts +107 -0
  50. package/dist/codecs.d.ts.map +1 -0
  51. package/dist/codecs.js +166 -0
  52. package/dist/codecs.js.map +1 -0
  53. package/dist/protocol.d.ts +155 -6
  54. package/dist/protocol.d.ts.map +1 -1
  55. package/dist/protocol.js +7 -1
  56. package/dist/protocol.js.map +1 -1
  57. package/dist/server/lap/forward.d.ts +13 -0
  58. package/dist/server/lap/forward.d.ts.map +1 -1
  59. package/dist/server/lap/forward.js +74 -0
  60. package/dist/server/lap/forward.js.map +1 -1
  61. package/dist/server/lap/router.d.ts.map +1 -1
  62. package/dist/server/lap/router.js +7 -1
  63. package/dist/server/lap/router.js.map +1 -1
  64. package/dist/server/ws/pairing-registry.d.ts +22 -6
  65. package/dist/server/ws/pairing-registry.d.ts.map +1 -1
  66. package/dist/server/ws/pairing-registry.js +49 -0
  67. package/dist/server/ws/pairing-registry.js.map +1 -1
  68. package/dist/state-diff.d.ts +52 -0
  69. package/dist/state-diff.d.ts.map +1 -0
  70. package/dist/state-diff.js +119 -0
  71. package/dist/state-diff.js.map +1 -0
  72. package/package.json +11 -5
@@ -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"]}
@@ -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"]}