@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
@@ -56,6 +56,14 @@ export interface PairingRegistry {
56
56
  * close fire synchronously. Returns an unsubscribe function.
57
57
  */
58
58
  onClose(tid: string, handler: () => void): () => void;
59
+ /**
60
+ * Read the most recent `n` log entries for a tid (newest first).
61
+ * Backed by an in-memory ring buffer populated as the registry
62
+ * sees `log-append` frames; capped per-tid to bound memory across
63
+ * long-lived sessions. Drained on close. Returns an empty array
64
+ * for unknown tids.
65
+ */
66
+ getRecentLog(tid: string, n: number): LogEntry[];
59
67
  /**
60
68
  * Send a typed rpc frame and await its matching reply. See
61
69
  * `./rpc.ts::rpc` for the full contract.
@@ -72,18 +80,26 @@ export interface PairingRegistry {
72
80
  stateAfter: unknown;
73
81
  }>;
74
82
  }
75
- /**
76
- * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno
77
- * Deploy — anywhere the server process can hold a long-lived
78
- * WebSocket. Not suitable for stateless Worker isolates; use the
79
- * Durable Object registry for Cloudflare.
80
- */
81
83
  export declare class InMemoryPairingRegistry implements PairingRegistry {
82
84
  private pairings;
83
85
  private onLogAppend;
86
+ /**
87
+ * Per-tid ring buffer of recent log entries. Populated as the
88
+ * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
89
+ * The agent reads this via `describe_recent_actions` to introspect
90
+ * its own activity history with stateDiffs intact.
91
+ */
92
+ private recentLog;
84
93
  constructor(opts?: {
85
94
  onLogAppend?: (tid: string, entry: LogEntry) => void;
86
95
  });
96
+ /**
97
+ * Read the most recent `n` log entries for a tid, newest-first. Returns
98
+ * an empty array when the tid is unknown or has no recorded activity.
99
+ * Drained from the in-memory ring buffer; entries older than
100
+ * RECENT_LOG_CAP have already been trimmed.
101
+ */
102
+ getRecentLog(tid: string, n: number): LogEntry[];
87
103
  register(tid: string, conn: PairingConnection): void;
88
104
  unregister(tid: string): void;
89
105
  isPaired(tid: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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"}
@@ -5,12 +5,43 @@ import { rpc as rpcHelper, waitForConfirm as waitForConfirmHelper, waitForChange
5
5
  * WebSocket. Not suitable for stateless Worker isolates; use the
6
6
  * Durable Object registry for Cloudflare.
7
7
  */
8
+ /**
9
+ * Per-tid cap on the recent-log ring buffer. Sized to cover a few
10
+ * minutes of agent activity at typical dispatch rates without
11
+ * growing unboundedly for long-lived sessions. Reads via
12
+ * `getRecentLog` clamp to this; agents asking for more get whatever
13
+ * the buffer currently holds.
14
+ */
15
+ const RECENT_LOG_CAP = 100;
8
16
  export class InMemoryPairingRegistry {
9
17
  pairings = new Map();
10
18
  onLogAppend;
19
+ /**
20
+ * Per-tid ring buffer of recent log entries. Populated as the
21
+ * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
22
+ * The agent reads this via `describe_recent_actions` to introspect
23
+ * its own activity history with stateDiffs intact.
24
+ */
25
+ recentLog = new Map();
11
26
  constructor(opts = {}) {
12
27
  this.onLogAppend = opts.onLogAppend ?? null;
13
28
  }
29
+ /**
30
+ * Read the most recent `n` log entries for a tid, newest-first. Returns
31
+ * an empty array when the tid is unknown or has no recorded activity.
32
+ * Drained from the in-memory ring buffer; entries older than
33
+ * RECENT_LOG_CAP have already been trimmed.
34
+ */
35
+ getRecentLog(tid, n) {
36
+ const buf = this.recentLog.get(tid);
37
+ if (!buf || buf.length === 0)
38
+ return [];
39
+ const count = Math.min(Math.max(0, Math.floor(n)), buf.length);
40
+ if (count === 0)
41
+ return [];
42
+ // Buffer is append-order; return the tail reversed so newest is first.
43
+ return buf.slice(-count).reverse();
44
+ }
14
45
  register(tid, conn) {
15
46
  const p = {
16
47
  conn,
@@ -77,6 +108,19 @@ export class InMemoryPairingRegistry {
77
108
  return;
78
109
  }
79
110
  if (frame.t === 'log-append') {
111
+ // Push into the ring buffer for `describe_recent_actions`,
112
+ // capped to RECENT_LOG_CAP. The audit-sink callback runs
113
+ // alongside; both are independent observers of the same
114
+ // log-append stream.
115
+ let buf = this.recentLog.get(tid);
116
+ if (!buf) {
117
+ buf = [];
118
+ this.recentLog.set(tid, buf);
119
+ }
120
+ buf.push(frame.entry);
121
+ if (buf.length > RECENT_LOG_CAP) {
122
+ buf.splice(0, buf.length - RECENT_LOG_CAP);
123
+ }
80
124
  this.onLogAppend?.(tid, frame.entry);
81
125
  return;
82
126
  }
@@ -131,6 +175,11 @@ export class InMemoryPairingRegistry {
131
175
  p.closeHandlers.clear();
132
176
  p.subscribers.clear();
133
177
  this.pairings.delete(tid);
178
+ // Drop the recent-log ring buffer — once the pairing is gone,
179
+ // `describe_recent_actions` will reject anyway (paused/revoked
180
+ // gates run before the registry lookup), but holding the entries
181
+ // would leak memory across reconnects.
182
+ this.recentLog.delete(tid);
134
183
  }
135
184
  }
136
185
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"pairing-registry.js","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js'\nimport {\n rpc as rpcHelper,\n waitForConfirm as waitForConfirmHelper,\n waitForChange as waitForChangeHelper,\n type RpcOptions,\n type RpcError,\n} from './rpc.js'\n\nexport type { RpcOptions, RpcError }\n\n/**\n * Thin abstraction over a single paired WebSocket. Consumed by the\n * registry implementations; runtime-specific adapters (`ws`-lib,\n * `WebSocketPair`, `Deno.upgradeWebSocket`, `Bun.serve` upgrade) build\n * one of these and pass it to `registry.register()`.\n */\nexport interface PairingConnection {\n send(frame: ServerFrame): void\n onFrame(handler: (f: ClientFrame) => void): void\n onClose(handler: () => void): void\n close(): void\n}\n\n/**\n * A per-call frame subscriber. Return `true` to remove this\n * subscriber (one-shot), or `false` to keep receiving. The registry\n * dispatches every inbound `ClientFrame` to every active subscriber\n * for the given `tid`; subscribers filter by `frame.t` + identifiers\n * (correlation id, confirm id, state path) to find the one that\n * belongs to their request.\n */\nexport type FrameSubscriber = (frame: ClientFrame) => boolean\n\n/**\n * Registry of live browser pairings. Pure routing + hello cache —\n * request-lifecycle state (in-flight RPC promises, confirm waits,\n * long-polls) lives in the LAP handlers that need it, not here.\n *\n * Two implementations ship today:\n * - `InMemoryPairingRegistry` for long-lived server processes\n * (Node, Bun, Deno, Deno Deploy).\n * - A Cloudflare Durable Object implementation (see\n * `server/cloudflare`) for stateless Worker runtimes.\n *\n * Other runtimes can implement this interface the same way; the\n * contract is intentionally small.\n */\nexport interface PairingRegistry {\n // ── Routing primitives ─────────────────────────────────────────\n register(tid: string, conn: PairingConnection): void\n unregister(tid: string): void\n isPaired(tid: string): boolean\n getHello(tid: string): HelloFrame | null\n /** Send a frame. No-op when the pairing is absent or closed. */\n send(tid: string, frame: ServerFrame): void\n /**\n * Subscribe to frames from the paired browser. Returns an\n * unsubscribe function. A subscriber can remove itself mid-dispatch\n * by returning `true` from its callback — useful for one-shot\n * request/response correlation.\n */\n subscribe(tid: string, handler: FrameSubscriber): () => void\n /**\n * Observe the pairing closing (WebSocket drop, `unregister`, etc.).\n * Handlers registered before close fire; handlers registered after\n * close fire synchronously. Returns an unsubscribe function.\n */\n onClose(tid: string, handler: () => void): () => void\n\n // ── Request/response helpers ───────────────────────────────────\n // These are part of the contract (LAP handlers call them directly)\n // but implementations almost always delegate to the free helpers in\n // `./rpc.ts`, which are built on the routing primitives above. The\n // Cloudflare Durable Object registry uses the same helpers; the\n // split exists so the routing surface is small enough to implement\n // across stateful boundaries (DO storage, WebSocket hibernation),\n // while the correlation logic lives once in a runtime-neutral file.\n\n /**\n * Send a typed rpc frame and await its matching reply. See\n * `./rpc.ts::rpc` for the full contract.\n */\n rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>\n /** See `./rpc.ts::waitForConfirm`. */\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }>\n /** See `./rpc.ts::waitForChange`. */\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }>\n}\n\ntype Pairing = {\n conn: PairingConnection\n hello: HelloFrame | null\n subscribers: Set<FrameSubscriber>\n closeHandlers: Set<() => void>\n closed: boolean\n}\n\n/**\n * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno\n * Deploy — anywhere the server process can hold a long-lived\n * WebSocket. Not suitable for stateless Worker isolates; use the\n * Durable Object registry for Cloudflare.\n */\nexport class InMemoryPairingRegistry implements PairingRegistry {\n private pairings = new Map<string, Pairing>()\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n\n constructor(\n opts: {\n onLogAppend?: (tid: string, entry: LogEntry) => void\n } = {},\n ) {\n this.onLogAppend = opts.onLogAppend ?? null\n }\n\n register(tid: string, conn: PairingConnection): void {\n const p: Pairing = {\n conn,\n hello: null,\n subscribers: new Set(),\n closeHandlers: new Set(),\n closed: false,\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.dispatch(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n }\n\n unregister(tid: string): void {\n this.handleClose(tid)\n }\n\n isPaired(tid: string): boolean {\n const p = this.pairings.get(tid)\n return !!p && !p.closed\n }\n\n getHello(tid: string): HelloFrame | null {\n return this.pairings.get(tid)?.hello ?? null\n }\n\n send(tid: string, frame: ServerFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n try {\n p.conn.send(frame)\n } catch {\n // Connection may have dropped between isPaired() and send(); no-op.\n }\n }\n\n subscribe(tid: string, handler: FrameSubscriber): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return () => {}\n p.subscribers.add(handler)\n return () => {\n p.subscribers.delete(handler)\n }\n }\n\n onClose(tid: string, handler: () => void): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n // Already closed — fire synchronously so callers don't hang\n // waiting for a close that already happened.\n queueMicrotask(handler)\n return () => {}\n }\n p.closeHandlers.add(handler)\n return () => {\n p.closeHandlers.delete(handler)\n }\n }\n\n private dispatch(tid: string, frame: ClientFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n // hello and log-append are registry-owned side effects — handled\n // here so no per-call subscriber has to pick them up.\n if (frame.t === 'hello') {\n p.hello = frame\n return\n }\n if (frame.t === 'log-append') {\n this.onLogAppend?.(tid, frame.entry)\n return\n }\n // Iterate over a snapshot because subscribers may self-remove\n // mid-iteration by returning true.\n const snapshot = Array.from(p.subscribers)\n for (const sub of snapshot) {\n try {\n if (sub(frame)) p.subscribers.delete(sub)\n } catch {\n // One bad subscriber shouldn't break the others.\n p.subscribers.delete(sub)\n }\n }\n }\n\n // ── Convenience wrappers ───────────────────────────────────────\n // The following methods delegate to the free-function helpers in\n // `./rpc.ts`. They're here so the in-memory registry remains a\n // one-stop testing surface (spy on `registry.rpc`, etc.) without\n // couping the `PairingRegistry` interface to request-lifecycle\n // details. External implementations (e.g. the Cloudflare Durable\n // Object registry) are NOT required to provide these; the LAP\n // handlers always go through the free helpers.\n\n rpc(tid: string, tool: string, args: unknown, opts: RpcOptions = {}): Promise<unknown> {\n return rpcHelper(this, tid, tool, args, opts)\n }\n\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }> {\n return waitForConfirmHelper(this, tid, confirmId, timeoutMs)\n }\n\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }> {\n return waitForChangeHelper(this, tid, path, timeoutMs)\n }\n\n /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */\n notify(tid: string, frame: ServerFrame): void {\n this.send(tid, frame)\n }\n\n private handleClose(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n p.closed = true\n for (const h of Array.from(p.closeHandlers)) {\n try {\n h()\n } catch {\n // Swallow — handlers run best-effort.\n }\n }\n p.closeHandlers.clear()\n p.subscribers.clear()\n this.pairings.delete(tid)\n }\n}\n\n/**\n * Back-compat alias for the prior class name. New code should use\n * `InMemoryPairingRegistry`. Removed in a future major.\n *\n * @deprecated Use `InMemoryPairingRegistry` directly.\n */\nexport const WsPairingRegistry = InMemoryPairingRegistry\nexport type WsPairingRegistry = InMemoryPairingRegistry\n"]}
1
+ {"version":3,"file":"pairing-registry.js","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js'\nimport {\n rpc as rpcHelper,\n waitForConfirm as waitForConfirmHelper,\n waitForChange as waitForChangeHelper,\n type RpcOptions,\n type RpcError,\n} from './rpc.js'\n\nexport type { RpcOptions, RpcError }\n\n/**\n * Thin abstraction over a single paired WebSocket. Consumed by the\n * registry implementations; runtime-specific adapters (`ws`-lib,\n * `WebSocketPair`, `Deno.upgradeWebSocket`, `Bun.serve` upgrade) build\n * one of these and pass it to `registry.register()`.\n */\nexport interface PairingConnection {\n send(frame: ServerFrame): void\n onFrame(handler: (f: ClientFrame) => void): void\n onClose(handler: () => void): void\n close(): void\n}\n\n/**\n * A per-call frame subscriber. Return `true` to remove this\n * subscriber (one-shot), or `false` to keep receiving. The registry\n * dispatches every inbound `ClientFrame` to every active subscriber\n * for the given `tid`; subscribers filter by `frame.t` + identifiers\n * (correlation id, confirm id, state path) to find the one that\n * belongs to their request.\n */\nexport type FrameSubscriber = (frame: ClientFrame) => boolean\n\n/**\n * Registry of live browser pairings. Pure routing + hello cache —\n * request-lifecycle state (in-flight RPC promises, confirm waits,\n * long-polls) lives in the LAP handlers that need it, not here.\n *\n * Two implementations ship today:\n * - `InMemoryPairingRegistry` for long-lived server processes\n * (Node, Bun, Deno, Deno Deploy).\n * - A Cloudflare Durable Object implementation (see\n * `server/cloudflare`) for stateless Worker runtimes.\n *\n * Other runtimes can implement this interface the same way; the\n * contract is intentionally small.\n */\nexport interface PairingRegistry {\n // ── Routing primitives ─────────────────────────────────────────\n register(tid: string, conn: PairingConnection): void\n unregister(tid: string): void\n isPaired(tid: string): boolean\n getHello(tid: string): HelloFrame | null\n /** Send a frame. No-op when the pairing is absent or closed. */\n send(tid: string, frame: ServerFrame): void\n /**\n * Subscribe to frames from the paired browser. Returns an\n * unsubscribe function. A subscriber can remove itself mid-dispatch\n * by returning `true` from its callback — useful for one-shot\n * request/response correlation.\n */\n subscribe(tid: string, handler: FrameSubscriber): () => void\n /**\n * Observe the pairing closing (WebSocket drop, `unregister`, etc.).\n * Handlers registered before close fire; handlers registered after\n * close fire synchronously. Returns an unsubscribe function.\n */\n onClose(tid: string, handler: () => void): () => void\n\n /**\n * Read the most recent `n` log entries for a tid (newest first).\n * Backed by an in-memory ring buffer populated as the registry\n * sees `log-append` frames; capped per-tid to bound memory across\n * long-lived sessions. Drained on close. Returns an empty array\n * for unknown tids.\n */\n getRecentLog(tid: string, n: number): LogEntry[]\n\n // ── Request/response helpers ───────────────────────────────────\n // These are part of the contract (LAP handlers call them directly)\n // but implementations almost always delegate to the free helpers in\n // `./rpc.ts`, which are built on the routing primitives above. The\n // Cloudflare Durable Object registry uses the same helpers; the\n // split exists so the routing surface is small enough to implement\n // across stateful boundaries (DO storage, WebSocket hibernation),\n // while the correlation logic lives once in a runtime-neutral file.\n\n /**\n * Send a typed rpc frame and await its matching reply. See\n * `./rpc.ts::rpc` for the full contract.\n */\n rpc(tid: string, tool: string, args: unknown, opts?: RpcOptions): Promise<unknown>\n /** See `./rpc.ts::waitForConfirm`. */\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }>\n /** See `./rpc.ts::waitForChange`. */\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }>\n}\n\ntype Pairing = {\n conn: PairingConnection\n hello: HelloFrame | null\n subscribers: Set<FrameSubscriber>\n closeHandlers: Set<() => void>\n closed: boolean\n}\n\n/**\n * Single-process in-memory registry. Correct for Node/Bun/Deno/Deno\n * Deploy — anywhere the server process can hold a long-lived\n * WebSocket. Not suitable for stateless Worker isolates; use the\n * Durable Object registry for Cloudflare.\n */\n/**\n * Per-tid cap on the recent-log ring buffer. Sized to cover a few\n * minutes of agent activity at typical dispatch rates without\n * growing unboundedly for long-lived sessions. Reads via\n * `getRecentLog` clamp to this; agents asking for more get whatever\n * the buffer currently holds.\n */\nconst RECENT_LOG_CAP = 100\n\nexport class InMemoryPairingRegistry implements PairingRegistry {\n private pairings = new Map<string, Pairing>()\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n /**\n * Per-tid ring buffer of recent log entries. Populated as the\n * registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.\n * The agent reads this via `describe_recent_actions` to introspect\n * its own activity history with stateDiffs intact.\n */\n private recentLog = new Map<string, LogEntry[]>()\n\n constructor(\n opts: {\n onLogAppend?: (tid: string, entry: LogEntry) => void\n } = {},\n ) {\n this.onLogAppend = opts.onLogAppend ?? null\n }\n\n /**\n * Read the most recent `n` log entries for a tid, newest-first. Returns\n * an empty array when the tid is unknown or has no recorded activity.\n * Drained from the in-memory ring buffer; entries older than\n * RECENT_LOG_CAP have already been trimmed.\n */\n getRecentLog(tid: string, n: number): LogEntry[] {\n const buf = this.recentLog.get(tid)\n if (!buf || buf.length === 0) return []\n const count = Math.min(Math.max(0, Math.floor(n)), buf.length)\n if (count === 0) return []\n // Buffer is append-order; return the tail reversed so newest is first.\n return buf.slice(-count).reverse()\n }\n\n register(tid: string, conn: PairingConnection): void {\n const p: Pairing = {\n conn,\n hello: null,\n subscribers: new Set(),\n closeHandlers: new Set(),\n closed: false,\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.dispatch(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n }\n\n unregister(tid: string): void {\n this.handleClose(tid)\n }\n\n isPaired(tid: string): boolean {\n const p = this.pairings.get(tid)\n return !!p && !p.closed\n }\n\n getHello(tid: string): HelloFrame | null {\n return this.pairings.get(tid)?.hello ?? null\n }\n\n send(tid: string, frame: ServerFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n try {\n p.conn.send(frame)\n } catch {\n // Connection may have dropped between isPaired() and send(); no-op.\n }\n }\n\n subscribe(tid: string, handler: FrameSubscriber): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return () => {}\n p.subscribers.add(handler)\n return () => {\n p.subscribers.delete(handler)\n }\n }\n\n onClose(tid: string, handler: () => void): () => void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) {\n // Already closed — fire synchronously so callers don't hang\n // waiting for a close that already happened.\n queueMicrotask(handler)\n return () => {}\n }\n p.closeHandlers.add(handler)\n return () => {\n p.closeHandlers.delete(handler)\n }\n }\n\n private dispatch(tid: string, frame: ClientFrame): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n // hello and log-append are registry-owned side effects — handled\n // here so no per-call subscriber has to pick them up.\n if (frame.t === 'hello') {\n p.hello = frame\n return\n }\n if (frame.t === 'log-append') {\n // Push into the ring buffer for `describe_recent_actions`,\n // capped to RECENT_LOG_CAP. The audit-sink callback runs\n // alongside; both are independent observers of the same\n // log-append stream.\n let buf = this.recentLog.get(tid)\n if (!buf) {\n buf = []\n this.recentLog.set(tid, buf)\n }\n buf.push(frame.entry)\n if (buf.length > RECENT_LOG_CAP) {\n buf.splice(0, buf.length - RECENT_LOG_CAP)\n }\n this.onLogAppend?.(tid, frame.entry)\n return\n }\n // Iterate over a snapshot because subscribers may self-remove\n // mid-iteration by returning true.\n const snapshot = Array.from(p.subscribers)\n for (const sub of snapshot) {\n try {\n if (sub(frame)) p.subscribers.delete(sub)\n } catch {\n // One bad subscriber shouldn't break the others.\n p.subscribers.delete(sub)\n }\n }\n }\n\n // ── Convenience wrappers ───────────────────────────────────────\n // The following methods delegate to the free-function helpers in\n // `./rpc.ts`. They're here so the in-memory registry remains a\n // one-stop testing surface (spy on `registry.rpc`, etc.) without\n // couping the `PairingRegistry` interface to request-lifecycle\n // details. External implementations (e.g. the Cloudflare Durable\n // Object registry) are NOT required to provide these; the LAP\n // handlers always go through the free helpers.\n\n rpc(tid: string, tool: string, args: unknown, opts: RpcOptions = {}): Promise<unknown> {\n return rpcHelper(this, tid, tool, args, opts)\n }\n\n waitForConfirm(\n tid: string,\n confirmId: string,\n timeoutMs: number,\n ): Promise<{ outcome: 'confirmed' | 'user-cancelled'; stateAfter?: unknown }> {\n return waitForConfirmHelper(this, tid, confirmId, timeoutMs)\n }\n\n waitForChange(\n tid: string,\n path: string | undefined,\n timeoutMs: number,\n ): Promise<{ status: 'changed' | 'timeout'; stateAfter: unknown }> {\n return waitForChangeHelper(this, tid, path, timeoutMs)\n }\n\n /** @deprecated Use `send(tid, frame)` directly; semantics are identical. */\n notify(tid: string, frame: ServerFrame): void {\n this.send(tid, frame)\n }\n\n private handleClose(tid: string): void {\n const p = this.pairings.get(tid)\n if (!p || p.closed) return\n p.closed = true\n for (const h of Array.from(p.closeHandlers)) {\n try {\n h()\n } catch {\n // Swallow — handlers run best-effort.\n }\n }\n p.closeHandlers.clear()\n p.subscribers.clear()\n this.pairings.delete(tid)\n // Drop the recent-log ring buffer — once the pairing is gone,\n // `describe_recent_actions` will reject anyway (paused/revoked\n // gates run before the registry lookup), but holding the entries\n // would leak memory across reconnects.\n this.recentLog.delete(tid)\n }\n}\n\n/**\n * Back-compat alias for the prior class name. New code should use\n * `InMemoryPairingRegistry`. Removed in a future major.\n *\n * @deprecated Use `InMemoryPairingRegistry` directly.\n */\nexport const WsPairingRegistry = InMemoryPairingRegistry\nexport type WsPairingRegistry = InMemoryPairingRegistry\n"]}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Compute a structural diff between two state snapshots and return it
3
+ * in JSON-Patch-shaped form (RFC 6902 subset: `add`, `remove`,
4
+ * `replace`).
5
+ *
6
+ * Why JSON Patch shape: LLMs see this exact format in their training
7
+ * data — it's the standard for describing object mutations on the
8
+ * wire. The agent learns the schema implicitly and can answer "what
9
+ * changed?" in a sentence by reading the ops.
10
+ *
11
+ * Why not unified-diff or per-binding dirty masks: the dirty mask
12
+ * tracks what bindings need re-rendering, which is a layout concern.
13
+ * The agent wants to know what *values* changed, which is a state
14
+ * concern. Dirty masks miss field-level resolution; per-path JSON
15
+ * Patch gives it.
16
+ *
17
+ * Cost is O(state size) per dispatch. For typical app states (a few
18
+ * KB) that's microseconds. Apps with very large states (collections
19
+ * of thousands of items) should subscribe to specific slices via
20
+ * `query_state` / `wait_for_change` instead of reading full diffs.
21
+ *
22
+ * Path escaping follows JSON Pointer (RFC 6901): `/` becomes `~1`,
23
+ * `~` becomes `~0`. The escape happens per-segment.
24
+ */
25
+ export type JsonPatchOp = {
26
+ op: 'add';
27
+ path: string;
28
+ value: unknown;
29
+ } | {
30
+ op: 'remove';
31
+ path: string;
32
+ } | {
33
+ op: 'replace';
34
+ path: string;
35
+ value: unknown;
36
+ };
37
+ export type StateDiff = JsonPatchOp[];
38
+ /**
39
+ * Compute the diff. Order of operations: removes first, then adds,
40
+ * then replaces. This is RFC 6902's recommended order — the receiver
41
+ * can apply ops sequentially without ambiguity.
42
+ *
43
+ * The implementation is a simple recursive walk; collection diffs
44
+ * are positional (index-based for arrays, key-based for objects)
45
+ * rather than structural (no LCS). Apps that pass identity-stable
46
+ * collections (`[...prev, item]`-style appends) get clean diffs;
47
+ * apps that rebuild arrays from scratch get noisy ones — same
48
+ * tradeoff a React reconciler makes, and the same fix (stable keys
49
+ * + push-don't-rebuild updates) applies.
50
+ */
51
+ export declare function computeStateDiff(prev: unknown, next: unknown): StateDiff;
52
+ //# sourceMappingURL=state-diff.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-diff.d.ts","sourceRoot":"","sources":["../src/state-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,MAAM,WAAW,GACnB;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC3C;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAA;AAEnD,MAAM,MAAM,SAAS,GAAG,WAAW,EAAE,CAAA;AAErC;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,SAAS,CAIxE"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Compute a structural diff between two state snapshots and return it
3
+ * in JSON-Patch-shaped form (RFC 6902 subset: `add`, `remove`,
4
+ * `replace`).
5
+ *
6
+ * Why JSON Patch shape: LLMs see this exact format in their training
7
+ * data — it's the standard for describing object mutations on the
8
+ * wire. The agent learns the schema implicitly and can answer "what
9
+ * changed?" in a sentence by reading the ops.
10
+ *
11
+ * Why not unified-diff or per-binding dirty masks: the dirty mask
12
+ * tracks what bindings need re-rendering, which is a layout concern.
13
+ * The agent wants to know what *values* changed, which is a state
14
+ * concern. Dirty masks miss field-level resolution; per-path JSON
15
+ * Patch gives it.
16
+ *
17
+ * Cost is O(state size) per dispatch. For typical app states (a few
18
+ * KB) that's microseconds. Apps with very large states (collections
19
+ * of thousands of items) should subscribe to specific slices via
20
+ * `query_state` / `wait_for_change` instead of reading full diffs.
21
+ *
22
+ * Path escaping follows JSON Pointer (RFC 6901): `/` becomes `~1`,
23
+ * `~` becomes `~0`. The escape happens per-segment.
24
+ */
25
+ /**
26
+ * Compute the diff. Order of operations: removes first, then adds,
27
+ * then replaces. This is RFC 6902's recommended order — the receiver
28
+ * can apply ops sequentially without ambiguity.
29
+ *
30
+ * The implementation is a simple recursive walk; collection diffs
31
+ * are positional (index-based for arrays, key-based for objects)
32
+ * rather than structural (no LCS). Apps that pass identity-stable
33
+ * collections (`[...prev, item]`-style appends) get clean diffs;
34
+ * apps that rebuild arrays from scratch get noisy ones — same
35
+ * tradeoff a React reconciler makes, and the same fix (stable keys
36
+ * + push-don't-rebuild updates) applies.
37
+ */
38
+ export function computeStateDiff(prev, next) {
39
+ const ops = [];
40
+ diffInto(prev, next, '', ops);
41
+ return ops;
42
+ }
43
+ function diffInto(prev, next, basePath, ops) {
44
+ if (Object.is(prev, next))
45
+ return;
46
+ // Either side null/undefined or non-object → straight replace at this path.
47
+ // (`null` is `typeof === 'object'`, so we test it explicitly.)
48
+ if (prev === null ||
49
+ next === null ||
50
+ prev === undefined ||
51
+ next === undefined ||
52
+ typeof prev !== 'object' ||
53
+ typeof next !== 'object') {
54
+ ops.push({ op: 'replace', path: basePath, value: next });
55
+ return;
56
+ }
57
+ const prevIsArr = Array.isArray(prev);
58
+ const nextIsArr = Array.isArray(next);
59
+ // Type change (object↔array) — single replace at the path. Recursing
60
+ // into mismatched containers would emit a wall of incoherent ops.
61
+ if (prevIsArr !== nextIsArr) {
62
+ ops.push({ op: 'replace', path: basePath, value: next });
63
+ return;
64
+ }
65
+ if (prevIsArr && nextIsArr) {
66
+ diffArray(prev, next, basePath, ops);
67
+ return;
68
+ }
69
+ diffObject(prev, next, basePath, ops);
70
+ }
71
+ function diffArray(prev, next, basePath, ops) {
72
+ const minLen = Math.min(prev.length, next.length);
73
+ // Recurse into shared indices.
74
+ for (let i = 0; i < minLen; i++) {
75
+ diffInto(prev[i], next[i], `${basePath}/${i}`, ops);
76
+ }
77
+ // Excess elements: remove from the end (descending order so each
78
+ // index is still valid as we apply).
79
+ if (prev.length > next.length) {
80
+ for (let i = prev.length - 1; i >= next.length; i--) {
81
+ ops.push({ op: 'remove', path: `${basePath}/${i}` });
82
+ }
83
+ }
84
+ // New elements: add at the position they end up in. RFC 6902 allows
85
+ // a `-` index for "append" semantics, but explicit indices are easier
86
+ // for the LLM to reason about ("alternative at index 8 was added").
87
+ if (next.length > prev.length) {
88
+ for (let i = prev.length; i < next.length; i++) {
89
+ ops.push({ op: 'add', path: `${basePath}/${i}`, value: next[i] });
90
+ }
91
+ }
92
+ }
93
+ function diffObject(prev, next, basePath, ops) {
94
+ // Removes: keys that exist in prev but not next.
95
+ for (const k in prev) {
96
+ if (!(k in next)) {
97
+ ops.push({ op: 'remove', path: `${basePath}/${escapeSegment(k)}` });
98
+ }
99
+ }
100
+ // Adds + replaces: keys in next.
101
+ for (const k in next) {
102
+ const path = `${basePath}/${escapeSegment(k)}`;
103
+ if (!(k in prev)) {
104
+ ops.push({ op: 'add', path, value: next[k] });
105
+ }
106
+ else {
107
+ diffInto(prev[k], next[k], path, ops);
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Escape a single path segment per RFC 6901: `~` → `~0`, then `/` →
113
+ * `~1`. Order matters; do `~` first so `/` substitution doesn't
114
+ * double-escape an already-escaped tilde.
115
+ */
116
+ function escapeSegment(seg) {
117
+ return seg.replace(/~/g, '~0').replace(/\//g, '~1');
118
+ }
119
+ //# sourceMappingURL=state-diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-diff.js","sourceRoot":"","sources":["../src/state-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Compute a structural diff between two state snapshots and return it\n * in JSON-Patch-shaped form (RFC 6902 subset: `add`, `remove`,\n * `replace`).\n *\n * Why JSON Patch shape: LLMs see this exact format in their training\n * data — it's the standard for describing object mutations on the\n * wire. The agent learns the schema implicitly and can answer \"what\n * changed?\" in a sentence by reading the ops.\n *\n * Why not unified-diff or per-binding dirty masks: the dirty mask\n * tracks what bindings need re-rendering, which is a layout concern.\n * The agent wants to know what *values* changed, which is a state\n * concern. Dirty masks miss field-level resolution; per-path JSON\n * Patch gives it.\n *\n * Cost is O(state size) per dispatch. For typical app states (a few\n * KB) that's microseconds. Apps with very large states (collections\n * of thousands of items) should subscribe to specific slices via\n * `query_state` / `wait_for_change` instead of reading full diffs.\n *\n * Path escaping follows JSON Pointer (RFC 6901): `/` becomes `~1`,\n * `~` becomes `~0`. The escape happens per-segment.\n */\n\nexport type JsonPatchOp =\n | { op: 'add'; path: string; value: unknown }\n | { op: 'remove'; path: string }\n | { op: 'replace'; path: string; value: unknown }\n\nexport type StateDiff = JsonPatchOp[]\n\n/**\n * Compute the diff. Order of operations: removes first, then adds,\n * then replaces. This is RFC 6902's recommended order — the receiver\n * can apply ops sequentially without ambiguity.\n *\n * The implementation is a simple recursive walk; collection diffs\n * are positional (index-based for arrays, key-based for objects)\n * rather than structural (no LCS). Apps that pass identity-stable\n * collections (`[...prev, item]`-style appends) get clean diffs;\n * apps that rebuild arrays from scratch get noisy ones — same\n * tradeoff a React reconciler makes, and the same fix (stable keys\n * + push-don't-rebuild updates) applies.\n */\nexport function computeStateDiff(prev: unknown, next: unknown): StateDiff {\n const ops: JsonPatchOp[] = []\n diffInto(prev, next, '', ops)\n return ops\n}\n\nfunction diffInto(prev: unknown, next: unknown, basePath: string, ops: JsonPatchOp[]): void {\n if (Object.is(prev, next)) return\n\n // Either side null/undefined or non-object → straight replace at this path.\n // (`null` is `typeof === 'object'`, so we test it explicitly.)\n if (\n prev === null ||\n next === null ||\n prev === undefined ||\n next === undefined ||\n typeof prev !== 'object' ||\n typeof next !== 'object'\n ) {\n ops.push({ op: 'replace', path: basePath, value: next })\n return\n }\n\n const prevIsArr = Array.isArray(prev)\n const nextIsArr = Array.isArray(next)\n\n // Type change (object↔array) — single replace at the path. Recursing\n // into mismatched containers would emit a wall of incoherent ops.\n if (prevIsArr !== nextIsArr) {\n ops.push({ op: 'replace', path: basePath, value: next })\n return\n }\n\n if (prevIsArr && nextIsArr) {\n diffArray(prev, next, basePath, ops)\n return\n }\n\n diffObject(prev as Record<string, unknown>, next as Record<string, unknown>, basePath, ops)\n}\n\nfunction diffArray(\n prev: readonly unknown[],\n next: readonly unknown[],\n basePath: string,\n ops: JsonPatchOp[],\n): void {\n const minLen = Math.min(prev.length, next.length)\n // Recurse into shared indices.\n for (let i = 0; i < minLen; i++) {\n diffInto(prev[i], next[i], `${basePath}/${i}`, ops)\n }\n // Excess elements: remove from the end (descending order so each\n // index is still valid as we apply).\n if (prev.length > next.length) {\n for (let i = prev.length - 1; i >= next.length; i--) {\n ops.push({ op: 'remove', path: `${basePath}/${i}` })\n }\n }\n // New elements: add at the position they end up in. RFC 6902 allows\n // a `-` index for \"append\" semantics, but explicit indices are easier\n // for the LLM to reason about (\"alternative at index 8 was added\").\n if (next.length > prev.length) {\n for (let i = prev.length; i < next.length; i++) {\n ops.push({ op: 'add', path: `${basePath}/${i}`, value: next[i] })\n }\n }\n}\n\nfunction diffObject(\n prev: Record<string, unknown>,\n next: Record<string, unknown>,\n basePath: string,\n ops: JsonPatchOp[],\n): void {\n // Removes: keys that exist in prev but not next.\n for (const k in prev) {\n if (!(k in next)) {\n ops.push({ op: 'remove', path: `${basePath}/${escapeSegment(k)}` })\n }\n }\n // Adds + replaces: keys in next.\n for (const k in next) {\n const path = `${basePath}/${escapeSegment(k)}`\n if (!(k in prev)) {\n ops.push({ op: 'add', path, value: next[k] })\n } else {\n diffInto(prev[k], next[k], path, ops)\n }\n }\n}\n\n/**\n * Escape a single path segment per RFC 6901: `~` → `~0`, then `/` →\n * `~1`. Order matters; do `~` first so `/` substitution doesn't\n * double-escape an already-escaped tilde.\n */\nfunction escapeSegment(seg: string): string {\n return seg.replace(/~/g, '~0').replace(/\\//g, '~1')\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llui/agent",
3
- "version": "0.0.31",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "exports": {
@@ -27,19 +27,25 @@
27
27
  "./protocol": {
28
28
  "types": "./dist/protocol.d.ts",
29
29
  "import": "./dist/protocol.js"
30
+ },
31
+ "./codecs": {
32
+ "types": "./dist/codecs.d.ts",
33
+ "import": "./dist/codecs.js"
30
34
  }
31
35
  },
32
36
  "files": [
33
37
  "dist"
34
38
  ],
35
39
  "dependencies": {
36
- "ws": "^8.18.0",
37
- "@llui/dom": "0.0.30",
38
- "@llui/effects": "0.0.9"
40
+ "ws": "^8.18.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@llui/dom": "^0.0.32"
39
44
  },
40
45
  "devDependencies": {
41
46
  "@types/node": "^22.0.0",
42
- "@types/ws": "^8.5.13"
47
+ "@types/ws": "^8.5.13",
48
+ "@llui/dom": "0.0.32"
43
49
  },
44
50
  "description": "LLui Agent — LAP server + browser client runtime for driving LLui apps from LLM clients",
45
51
  "keywords": [