@llui/agent 0.0.51 → 0.0.53
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/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +45 -31
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/client/effect-handler.d.ts +0 -21
- package/dist/client/effect-handler.d.ts.map +1 -1
- package/dist/client/effect-handler.js +0 -26
- package/dist/client/effect-handler.js.map +1 -1
- package/dist/client/effects.d.ts +0 -23
- package/dist/client/effects.d.ts.map +1 -1
- package/dist/client/effects.js.map +1 -1
- package/dist/client/factory.d.ts +0 -9
- package/dist/client/factory.d.ts.map +1 -1
- package/dist/client/factory.js +0 -2
- package/dist/client/factory.js.map +1 -1
- package/dist/client/index.d.ts +0 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +0 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/ws-client.d.ts +0 -9
- package/dist/client/ws-client.d.ts.map +1 -1
- package/dist/client/ws-client.js +0 -32
- package/dist/client/ws-client.js.map +1 -1
- package/dist/mcp/tools.d.ts +19 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +176 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/protocol.d.ts +1 -50
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server/cloudflare/durable-object.d.ts +14 -44
- package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
- package/dist/server/cloudflare/durable-object.js +21 -49
- package/dist/server/cloudflare/durable-object.js.map +1 -1
- package/dist/server/cloudflare/index.d.ts +3 -10
- package/dist/server/cloudflare/index.d.ts.map +1 -1
- package/dist/server/cloudflare/index.js +3 -10
- package/dist/server/cloudflare/index.js.map +1 -1
- package/dist/server/core.d.ts +1 -11
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +0 -1
- package/dist/server/core.js.map +1 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +14 -1
- package/dist/server/factory.js.map +1 -1
- package/dist/server/lap/router.d.ts.map +1 -1
- package/dist/server/lap/router.js +0 -3
- package/dist/server/lap/router.js.map +1 -1
- package/dist/server/mcp/router.d.ts +26 -0
- package/dist/server/mcp/router.d.ts.map +1 -0
- package/dist/server/mcp/router.js +81 -0
- package/dist/server/mcp/router.js.map +1 -0
- package/dist/server/mcp/server.d.ts +26 -0
- package/dist/server/mcp/server.d.ts.map +1 -0
- package/dist/server/mcp/server.js +116 -0
- package/dist/server/mcp/server.js.map +1 -0
- package/dist/server/mcp/session-map.d.ts +18 -0
- package/dist/server/mcp/session-map.d.ts.map +1 -0
- package/dist/server/mcp/session-map.js +13 -0
- package/dist/server/mcp/session-map.js.map +1 -0
- package/dist/server/options.d.ts +13 -0
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js.map +1 -1
- package/dist/server/ws/pairing-registry.d.ts +0 -101
- package/dist/server/ws/pairing-registry.d.ts.map +1 -1
- package/dist/server/ws/pairing-registry.js +0 -160
- package/dist/server/ws/pairing-registry.js.map +1 -1
- package/package.json +8 -2
- package/styles/agent-panel.css +0 -13
|
@@ -1,75 +1,6 @@
|
|
|
1
1
|
import type { ClientFrame, ServerFrame, HelloFrame, LogEntry } from '../../protocol.js';
|
|
2
2
|
import { type RpcOptions, type RpcError } from './rpc.js';
|
|
3
3
|
export type { RpcOptions, RpcError };
|
|
4
|
-
/**
|
|
5
|
-
* Resolution shape for `waitForUserInput`. Mirrors `LapWaitForUserInputResponse`
|
|
6
|
-
* one-for-one — declared here so the interface stays expressible without
|
|
7
|
-
* importing the LAP layer's protocol types upward.
|
|
8
|
-
*/
|
|
9
|
-
export type UserInputResolution = {
|
|
10
|
-
status: 'submitted';
|
|
11
|
-
text: string;
|
|
12
|
-
at: number;
|
|
13
|
-
} | {
|
|
14
|
-
status: 'timeout';
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* Optional persistence adapter for the per-tid user-input buffer.
|
|
18
|
-
*
|
|
19
|
-
* Most runtimes (Node, Bun, Deno, Deno Deploy) don't need this — the
|
|
20
|
-
* in-memory registry survives for the lifetime of the process, which
|
|
21
|
-
* is also the lifetime of the WS pairing. Buffered submissions either
|
|
22
|
-
* get drained by an agent's `wait_for_user_input` call or are
|
|
23
|
-
* irrelevant when the pairing eventually closes.
|
|
24
|
-
*
|
|
25
|
-
* Cloudflare Durable Objects are the motivating case. A DO process
|
|
26
|
-
* can be evicted (deploys, idle eviction, runtime restarts) while a
|
|
27
|
-
* WS pairing is paused mid-conversation; the next request rebuilds
|
|
28
|
-
* a fresh DO with a fresh `InMemoryPairingRegistry`. Wiring this
|
|
29
|
-
* adapter to the DO's `state.storage` makes buffered submissions
|
|
30
|
-
* survive eviction.
|
|
31
|
-
*
|
|
32
|
-
* Parked waiters (Promise resolvers from `waitForUserInput`) CAN'T be
|
|
33
|
-
* persisted — they live in JS memory only. After eviction + wake the
|
|
34
|
-
* agent's LAP client times out on its parked HTTP request and retries
|
|
35
|
-
* via the same long-poll loop; the retry sees the restored buffer.
|
|
36
|
-
*
|
|
37
|
-
* Calls are best-effort: the registry doesn't await them on the hot
|
|
38
|
-
* path. A storage outage causes lost messages on eviction but never
|
|
39
|
-
* wedges a live conversation.
|
|
40
|
-
*/
|
|
41
|
-
export interface UserInputStorage {
|
|
42
|
-
/**
|
|
43
|
-
* Read any persisted buffer for this tid. Called from `register()`
|
|
44
|
-
* when the registry sees a fresh pairing — the returned entries
|
|
45
|
-
* seed the in-memory buffer so subsequent `waitForUserInput` calls
|
|
46
|
-
* find them.
|
|
47
|
-
*
|
|
48
|
-
* Returning an empty array (or rejecting) for an unknown tid is
|
|
49
|
-
* normal — a fresh DO has nothing to restore.
|
|
50
|
-
*/
|
|
51
|
-
read(tid: string): Promise<Array<{
|
|
52
|
-
text: string;
|
|
53
|
-
at: number;
|
|
54
|
-
}>>;
|
|
55
|
-
/**
|
|
56
|
-
* Persist the current buffer for this tid. Called whenever the
|
|
57
|
-
* buffer mutates (push on `user-input-submitted`, shift on
|
|
58
|
-
* `waitForUserInput` drain). Receives the FULL buffer, not just
|
|
59
|
-
* the delta — simpler contract, idempotent writes, and the buffer
|
|
60
|
-
* is small (capped at USER_INPUT_BUFFER_CAP).
|
|
61
|
-
*/
|
|
62
|
-
write(tid: string, buffer: Array<{
|
|
63
|
-
text: string;
|
|
64
|
-
at: number;
|
|
65
|
-
}>): Promise<void>;
|
|
66
|
-
/**
|
|
67
|
-
* Drop persisted buffer for this tid. Called from `handleClose()`
|
|
68
|
-
* so an evicted-then-restarted DO doesn't see stale messages from
|
|
69
|
-
* a session that ended.
|
|
70
|
-
*/
|
|
71
|
-
clear(tid: string): Promise<void>;
|
|
72
|
-
}
|
|
73
4
|
/**
|
|
74
5
|
* Thin abstraction over a single paired WebSocket. Consumed by the
|
|
75
6
|
* registry implementations; runtime-specific adapters (`ws`-lib,
|
|
@@ -133,21 +64,6 @@ export interface PairingRegistry {
|
|
|
133
64
|
* for unknown tids.
|
|
134
65
|
*/
|
|
135
66
|
getRecentLog(tid: string, n: number): LogEntry[];
|
|
136
|
-
/**
|
|
137
|
-
* Long-poll for the next user-input submission from the paired
|
|
138
|
-
* runtime. The registry buffers a small number of submissions
|
|
139
|
-
* received with no waiter parked (so a user typing before Claude
|
|
140
|
-
* reaches the tool call doesn't lose the message); when a waiter
|
|
141
|
-
* parks with a non-empty buffer it resolves immediately with the
|
|
142
|
-
* oldest buffered submission. When the buffer is empty, the waiter
|
|
143
|
-
* sleeps until a `user-input-submitted` frame arrives, the WS
|
|
144
|
-
* pairing closes, or `timeoutMs` elapses.
|
|
145
|
-
*
|
|
146
|
-
* FIFO delivery: each submission is consumed by exactly one waiter.
|
|
147
|
-
* Multiple parked waiters form a queue; submissions are dispatched
|
|
148
|
-
* in arrival order to the head of the waiter queue.
|
|
149
|
-
*/
|
|
150
|
-
waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution>;
|
|
151
67
|
/**
|
|
152
68
|
* Send a typed rpc frame and await its matching reply. See
|
|
153
69
|
* `./rpc.ts::rpc` for the full contract.
|
|
@@ -167,7 +83,6 @@ export interface PairingRegistry {
|
|
|
167
83
|
export declare class InMemoryPairingRegistry implements PairingRegistry {
|
|
168
84
|
private pairings;
|
|
169
85
|
private onLogAppend;
|
|
170
|
-
private userInputStorage;
|
|
171
86
|
/**
|
|
172
87
|
* Per-tid ring buffer of recent log entries. Populated as the
|
|
173
88
|
* registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
|
|
@@ -177,13 +92,6 @@ export declare class InMemoryPairingRegistry implements PairingRegistry {
|
|
|
177
92
|
private recentLog;
|
|
178
93
|
constructor(opts?: {
|
|
179
94
|
onLogAppend?: (tid: string, entry: LogEntry) => void;
|
|
180
|
-
/**
|
|
181
|
-
* Optional adapter for persisting the user-input buffer across
|
|
182
|
-
* runtime restarts (Cloudflare DO eviction, mainly). See
|
|
183
|
-
* `UserInputStorage` for the contract. Omit on Node/Bun/Deno —
|
|
184
|
-
* those runtimes don't need it.
|
|
185
|
-
*/
|
|
186
|
-
userInputStorage?: UserInputStorage;
|
|
187
95
|
});
|
|
188
96
|
/**
|
|
189
97
|
* Read the most recent `n` log entries for a tid, newest-first. Returns
|
|
@@ -209,17 +117,8 @@ export declare class InMemoryPairingRegistry implements PairingRegistry {
|
|
|
209
117
|
status: 'changed' | 'timeout';
|
|
210
118
|
stateAfter: unknown;
|
|
211
119
|
}>;
|
|
212
|
-
waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution>;
|
|
213
120
|
/** @deprecated Use `send(tid, frame)` directly; semantics are identical. */
|
|
214
121
|
notify(tid: string, frame: ServerFrame): void;
|
|
215
|
-
/**
|
|
216
|
-
* Fire-and-forget write-through to the optional storage adapter.
|
|
217
|
-
* Snapshots the buffer (defensive copy — the in-memory array can
|
|
218
|
-
* mutate again before the adapter's write resolves) and ignores
|
|
219
|
-
* rejections. Called on every buffer mutation; cheap when the
|
|
220
|
-
* adapter is unset (no-ops) and best-effort when wired.
|
|
221
|
-
*/
|
|
222
|
-
private persistUserInputBuffer;
|
|
223
122
|
private handleClose;
|
|
224
123
|
}
|
|
225
124
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AACvF,OAAO,EAIL,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,UAAU,CAAA;AAEjB,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAA;AAEpC
|
|
1
|
+
{"version":3,"file":"pairing-registry.d.ts","sourceRoot":"","sources":["../../../src/server/ws/pairing-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,18 +13,9 @@ import { rpc as rpcHelper, waitForConfirm as waitForConfirmHelper, waitForChange
|
|
|
13
13
|
* the buffer currently holds.
|
|
14
14
|
*/
|
|
15
15
|
const RECENT_LOG_CAP = 100;
|
|
16
|
-
/**
|
|
17
|
-
* Per-tid cap on the user-input buffer (submissions received with no
|
|
18
|
-
* waiter parked). Eight messages covers a typical "user types a few
|
|
19
|
-
* follow-ups while Claude is mid-tool-call" gap without leaking memory
|
|
20
|
-
* if no agent ever drains them. Overflow drops oldest (newer messages
|
|
21
|
-
* are more contextually relevant).
|
|
22
|
-
*/
|
|
23
|
-
const USER_INPUT_BUFFER_CAP = 8;
|
|
24
16
|
export class InMemoryPairingRegistry {
|
|
25
17
|
pairings = new Map();
|
|
26
18
|
onLogAppend;
|
|
27
|
-
userInputStorage;
|
|
28
19
|
/**
|
|
29
20
|
* Per-tid ring buffer of recent log entries. Populated as the
|
|
30
21
|
* registry sees `log-append` frames; trimmed to RECENT_LOG_CAP.
|
|
@@ -34,7 +25,6 @@ export class InMemoryPairingRegistry {
|
|
|
34
25
|
recentLog = new Map();
|
|
35
26
|
constructor(opts = {}) {
|
|
36
27
|
this.onLogAppend = opts.onLogAppend ?? null;
|
|
37
|
-
this.userInputStorage = opts.userInputStorage ?? null;
|
|
38
28
|
}
|
|
39
29
|
/**
|
|
40
30
|
* Read the most recent `n` log entries for a tid, newest-first. Returns
|
|
@@ -59,38 +49,10 @@ export class InMemoryPairingRegistry {
|
|
|
59
49
|
subscribers: new Set(),
|
|
60
50
|
closeHandlers: new Set(),
|
|
61
51
|
closed: false,
|
|
62
|
-
userInputBuffer: [],
|
|
63
|
-
userInputWaiters: [],
|
|
64
52
|
};
|
|
65
53
|
this.pairings.set(tid, p);
|
|
66
54
|
conn.onFrame((frame) => this.dispatch(tid, frame));
|
|
67
55
|
conn.onClose(() => this.handleClose(tid));
|
|
68
|
-
// Best-effort restore of any persisted buffer. The promise is NOT
|
|
69
|
-
// awaited so the synchronous register() contract is preserved;
|
|
70
|
-
// submissions arriving in-flight queue up in the in-memory buffer
|
|
71
|
-
// and the restored entries simply prepend (oldest-first) when the
|
|
72
|
-
// read resolves. If the read rejects, we treat it as an empty
|
|
73
|
-
// restore — the conversation continues, just without any messages
|
|
74
|
-
// from before the eviction.
|
|
75
|
-
if (this.userInputStorage) {
|
|
76
|
-
const storage = this.userInputStorage;
|
|
77
|
-
storage.read(tid).then((entries) => {
|
|
78
|
-
if (!entries || entries.length === 0)
|
|
79
|
-
return;
|
|
80
|
-
const live = this.pairings.get(tid);
|
|
81
|
-
if (!live || live !== p || live.closed)
|
|
82
|
-
return; // pairing already closed/replaced
|
|
83
|
-
// Restored entries are older than anything that arrived in
|
|
84
|
-
// the meantime. Prepend, then re-cap to USER_INPUT_BUFFER_CAP.
|
|
85
|
-
live.userInputBuffer.unshift(...entries);
|
|
86
|
-
if (live.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {
|
|
87
|
-
live.userInputBuffer.splice(0, live.userInputBuffer.length - USER_INPUT_BUFFER_CAP);
|
|
88
|
-
}
|
|
89
|
-
}, () => {
|
|
90
|
-
// Storage failure — log and continue. The conversation works
|
|
91
|
-
// without persistence; we just lost any pre-eviction messages.
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
56
|
}
|
|
95
57
|
unregister(tid) {
|
|
96
58
|
this.handleClose(tid);
|
|
@@ -162,25 +124,6 @@ export class InMemoryPairingRegistry {
|
|
|
162
124
|
this.onLogAppend?.(tid, frame.entry);
|
|
163
125
|
return;
|
|
164
126
|
}
|
|
165
|
-
if (frame.t === 'user-input-submitted') {
|
|
166
|
-
// FIFO delivery to a parked waiter, else buffer.
|
|
167
|
-
// Shifting the head keeps "first parked" semantics; the parked
|
|
168
|
-
// promise's cancel tears down its own timer before resolving.
|
|
169
|
-
const waiter = p.userInputWaiters.shift();
|
|
170
|
-
if (waiter) {
|
|
171
|
-
waiter.cancel();
|
|
172
|
-
waiter.resolve({ status: 'submitted', text: frame.text, at: frame.at });
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
p.userInputBuffer.push({ text: frame.text, at: frame.at });
|
|
176
|
-
if (p.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {
|
|
177
|
-
// Drop oldest. Newer messages are more contextually relevant
|
|
178
|
-
// for an agent picking up a stale conversation.
|
|
179
|
-
p.userInputBuffer.splice(0, p.userInputBuffer.length - USER_INPUT_BUFFER_CAP);
|
|
180
|
-
}
|
|
181
|
-
this.persistUserInputBuffer(tid, p.userInputBuffer);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
127
|
// Iterate over a snapshot because subscribers may self-remove
|
|
185
128
|
// mid-iteration by returning true.
|
|
186
129
|
const snapshot = Array.from(p.subscribers);
|
|
@@ -212,118 +155,15 @@ export class InMemoryPairingRegistry {
|
|
|
212
155
|
waitForChange(tid, path, timeoutMs) {
|
|
213
156
|
return waitForChangeHelper(this, tid, path, timeoutMs);
|
|
214
157
|
}
|
|
215
|
-
waitForUserInput(tid, timeoutMs) {
|
|
216
|
-
const p = this.pairings.get(tid);
|
|
217
|
-
// Unknown / closed pairing → resolve as timeout immediately. The
|
|
218
|
-
// LAP layer above us has already gated on `isPaired`, so this is
|
|
219
|
-
// the rare race where the pairing closed between the gate and the
|
|
220
|
-
// wait call. Returning timeout (instead of rejecting) keeps the
|
|
221
|
-
// public response shape simple — agents only need to handle two
|
|
222
|
-
// outcomes.
|
|
223
|
-
if (!p || p.closed) {
|
|
224
|
-
return Promise.resolve({ status: 'timeout' });
|
|
225
|
-
}
|
|
226
|
-
// Buffered submission already waiting → resolve synchronously.
|
|
227
|
-
const buffered = p.userInputBuffer.shift();
|
|
228
|
-
if (buffered) {
|
|
229
|
-
this.persistUserInputBuffer(tid, p.userInputBuffer);
|
|
230
|
-
return Promise.resolve({ status: 'submitted', text: buffered.text, at: buffered.at });
|
|
231
|
-
}
|
|
232
|
-
return new Promise((resolve) => {
|
|
233
|
-
let settled = false;
|
|
234
|
-
const settle = (value) => {
|
|
235
|
-
if (settled)
|
|
236
|
-
return;
|
|
237
|
-
settled = true;
|
|
238
|
-
resolve(value);
|
|
239
|
-
};
|
|
240
|
-
const timer = setTimeout(() => {
|
|
241
|
-
const idx = p.userInputWaiters.findIndex((w) => w === waiter);
|
|
242
|
-
if (idx !== -1)
|
|
243
|
-
p.userInputWaiters.splice(idx, 1);
|
|
244
|
-
settle({ status: 'timeout' });
|
|
245
|
-
}, timeoutMs);
|
|
246
|
-
const waiter = {
|
|
247
|
-
resolve: (value) => {
|
|
248
|
-
// dispatch() / handleClose() both call this. The `settled`
|
|
249
|
-
// guard makes it idempotent — exactly one resolution survives,
|
|
250
|
-
// regardless of arrival order.
|
|
251
|
-
settle(value);
|
|
252
|
-
},
|
|
253
|
-
cancel: () => {
|
|
254
|
-
clearTimeout(timer);
|
|
255
|
-
unsubClose();
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
p.userInputWaiters.push(waiter);
|
|
259
|
-
// Pairing close before resolution: clean up and resolve as
|
|
260
|
-
// timeout. handleClose sweeps the waiter queue and calls
|
|
261
|
-
// `waiter.resolve({ status: 'timeout' })` directly, so this
|
|
262
|
-
// close subscription is belt-and-braces — covers any path where
|
|
263
|
-
// the registry's close cascade doesn't run (e.g. a custom
|
|
264
|
-
// PairingConnection signalling close in an unusual order).
|
|
265
|
-
const unsubClose = this.onClose(tid, () => {
|
|
266
|
-
const idx = p.userInputWaiters.findIndex((w) => w === waiter);
|
|
267
|
-
if (idx !== -1)
|
|
268
|
-
p.userInputWaiters.splice(idx, 1);
|
|
269
|
-
clearTimeout(timer);
|
|
270
|
-
settle({ status: 'timeout' });
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
158
|
/** @deprecated Use `send(tid, frame)` directly; semantics are identical. */
|
|
275
159
|
notify(tid, frame) {
|
|
276
160
|
this.send(tid, frame);
|
|
277
161
|
}
|
|
278
|
-
/**
|
|
279
|
-
* Fire-and-forget write-through to the optional storage adapter.
|
|
280
|
-
* Snapshots the buffer (defensive copy — the in-memory array can
|
|
281
|
-
* mutate again before the adapter's write resolves) and ignores
|
|
282
|
-
* rejections. Called on every buffer mutation; cheap when the
|
|
283
|
-
* adapter is unset (no-ops) and best-effort when wired.
|
|
284
|
-
*/
|
|
285
|
-
persistUserInputBuffer(tid, buffer) {
|
|
286
|
-
if (!this.userInputStorage)
|
|
287
|
-
return;
|
|
288
|
-
// Defensive copy so the persisted snapshot is the buffer at the
|
|
289
|
-
// moment of mutation, not whatever the buffer holds when the
|
|
290
|
-
// storage adapter actually serializes the write.
|
|
291
|
-
const snapshot = buffer.slice();
|
|
292
|
-
void this.userInputStorage.write(tid, snapshot).catch(() => {
|
|
293
|
-
/* best-effort; storage failures don't wedge the conversation */
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
162
|
handleClose(tid) {
|
|
297
163
|
const p = this.pairings.get(tid);
|
|
298
164
|
if (!p || p.closed)
|
|
299
165
|
return;
|
|
300
166
|
p.closed = true;
|
|
301
|
-
// Tear down user-input waiters BEFORE running closeHandlers.
|
|
302
|
-
// Each waiter's `cancel` clears its timer + close-subscription;
|
|
303
|
-
// resolving as `timeout` afterward is idempotent (the waiter's
|
|
304
|
-
// `settled` guard short-circuits if its own close handler already
|
|
305
|
-
// resolved it). Order matters: cancel first so the resolution
|
|
306
|
-
// path can't re-enter handleClose via the close-subscription.
|
|
307
|
-
const waiters = p.userInputWaiters.slice();
|
|
308
|
-
p.userInputWaiters.length = 0;
|
|
309
|
-
for (const w of waiters) {
|
|
310
|
-
try {
|
|
311
|
-
w.cancel();
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
// best-effort; resolution still proceeds
|
|
315
|
-
}
|
|
316
|
-
w.resolve({ status: 'timeout' });
|
|
317
|
-
}
|
|
318
|
-
// Drop buffered inputs — once the pairing is closed, an agent
|
|
319
|
-
// resuming on a fresh tid shouldn't see stale messages from the
|
|
320
|
-
// previous session. Same retention contract as recentLog.
|
|
321
|
-
p.userInputBuffer.length = 0;
|
|
322
|
-
if (this.userInputStorage) {
|
|
323
|
-
void this.userInputStorage.clear(tid).catch(() => {
|
|
324
|
-
/* best-effort; storage failures don't block the close path */
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
167
|
for (const h of Array.from(p.closeHandlers)) {
|
|
328
168
|
try {
|
|
329
169
|
h();
|
|
@@ -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 * Resolution shape for `waitForUserInput`. Mirrors `LapWaitForUserInputResponse`\n * one-for-one — declared here so the interface stays expressible without\n * importing the LAP layer's protocol types upward.\n */\nexport type UserInputResolution =\n | { status: 'submitted'; text: string; at: number }\n | { status: 'timeout' }\n\n/**\n * Optional persistence adapter for the per-tid user-input buffer.\n *\n * Most runtimes (Node, Bun, Deno, Deno Deploy) don't need this — the\n * in-memory registry survives for the lifetime of the process, which\n * is also the lifetime of the WS pairing. Buffered submissions either\n * get drained by an agent's `wait_for_user_input` call or are\n * irrelevant when the pairing eventually closes.\n *\n * Cloudflare Durable Objects are the motivating case. A DO process\n * can be evicted (deploys, idle eviction, runtime restarts) while a\n * WS pairing is paused mid-conversation; the next request rebuilds\n * a fresh DO with a fresh `InMemoryPairingRegistry`. Wiring this\n * adapter to the DO's `state.storage` makes buffered submissions\n * survive eviction.\n *\n * Parked waiters (Promise resolvers from `waitForUserInput`) CAN'T be\n * persisted — they live in JS memory only. After eviction + wake the\n * agent's LAP client times out on its parked HTTP request and retries\n * via the same long-poll loop; the retry sees the restored buffer.\n *\n * Calls are best-effort: the registry doesn't await them on the hot\n * path. A storage outage causes lost messages on eviction but never\n * wedges a live conversation.\n */\nexport interface UserInputStorage {\n /**\n * Read any persisted buffer for this tid. Called from `register()`\n * when the registry sees a fresh pairing — the returned entries\n * seed the in-memory buffer so subsequent `waitForUserInput` calls\n * find them.\n *\n * Returning an empty array (or rejecting) for an unknown tid is\n * normal — a fresh DO has nothing to restore.\n */\n read(tid: string): Promise<Array<{ text: string; at: number }>>\n /**\n * Persist the current buffer for this tid. Called whenever the\n * buffer mutates (push on `user-input-submitted`, shift on\n * `waitForUserInput` drain). Receives the FULL buffer, not just\n * the delta — simpler contract, idempotent writes, and the buffer\n * is small (capped at USER_INPUT_BUFFER_CAP).\n */\n write(tid: string, buffer: Array<{ text: string; at: number }>): Promise<void>\n /**\n * Drop persisted buffer for this tid. Called from `handleClose()`\n * so an evicted-then-restarted DO doesn't see stale messages from\n * a session that ended.\n */\n clear(tid: string): Promise<void>\n}\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 /**\n * Long-poll for the next user-input submission from the paired\n * runtime. The registry buffers a small number of submissions\n * received with no waiter parked (so a user typing before Claude\n * reaches the tool call doesn't lose the message); when a waiter\n * parks with a non-empty buffer it resolves immediately with the\n * oldest buffered submission. When the buffer is empty, the waiter\n * sleeps until a `user-input-submitted` frame arrives, the WS\n * pairing closes, or `timeoutMs` elapses.\n *\n * FIFO delivery: each submission is consumed by exactly one waiter.\n * Multiple parked waiters form a queue; submissions are dispatched\n * in arrival order to the head of the waiter queue.\n */\n waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution>\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 * Buffered user-input submissions awaiting a parked waiter.\n * Bounded to USER_INPUT_BUFFER_CAP to prevent unbounded memory\n * growth if Claude never calls `wait_for_user_input` while the\n * user keeps typing. Buffer overflow drops the OLDEST entry —\n * fresher messages are more likely to be relevant.\n */\n userInputBuffer: Array<{ text: string; at: number }>\n /**\n * Waiters parked on `waitForUserInput`. The shape carries both the\n * outer promise's resolver (typed for the union, so `handleClose`\n * can resolve as `timeout` if the pairing dies) and a `cancel`\n * that tears down the waiter's per-call timer + close subscription.\n */\n userInputWaiters: Array<UserInputWaiter>\n}\n\ntype UserInputWaiter = {\n resolve: (value: UserInputResolution) => void\n cancel: () => void\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\n/**\n * Per-tid cap on the user-input buffer (submissions received with no\n * waiter parked). Eight messages covers a typical \"user types a few\n * follow-ups while Claude is mid-tool-call\" gap without leaking memory\n * if no agent ever drains them. Overflow drops oldest (newer messages\n * are more contextually relevant).\n */\nconst USER_INPUT_BUFFER_CAP = 8\n\nexport class InMemoryPairingRegistry implements PairingRegistry {\n private pairings = new Map<string, Pairing>()\n private onLogAppend: ((tid: string, entry: LogEntry) => void) | null\n private userInputStorage: UserInputStorage | 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 * Optional adapter for persisting the user-input buffer across\n * runtime restarts (Cloudflare DO eviction, mainly). See\n * `UserInputStorage` for the contract. Omit on Node/Bun/Deno —\n * those runtimes don't need it.\n */\n userInputStorage?: UserInputStorage\n } = {},\n ) {\n this.onLogAppend = opts.onLogAppend ?? null\n this.userInputStorage = opts.userInputStorage ?? 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 userInputBuffer: [],\n userInputWaiters: [],\n }\n this.pairings.set(tid, p)\n conn.onFrame((frame) => this.dispatch(tid, frame))\n conn.onClose(() => this.handleClose(tid))\n // Best-effort restore of any persisted buffer. The promise is NOT\n // awaited so the synchronous register() contract is preserved;\n // submissions arriving in-flight queue up in the in-memory buffer\n // and the restored entries simply prepend (oldest-first) when the\n // read resolves. If the read rejects, we treat it as an empty\n // restore — the conversation continues, just without any messages\n // from before the eviction.\n if (this.userInputStorage) {\n const storage = this.userInputStorage\n storage.read(tid).then(\n (entries) => {\n if (!entries || entries.length === 0) return\n const live = this.pairings.get(tid)\n if (!live || live !== p || live.closed) return // pairing already closed/replaced\n // Restored entries are older than anything that arrived in\n // the meantime. Prepend, then re-cap to USER_INPUT_BUFFER_CAP.\n live.userInputBuffer.unshift(...entries)\n if (live.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {\n live.userInputBuffer.splice(0, live.userInputBuffer.length - USER_INPUT_BUFFER_CAP)\n }\n },\n () => {\n // Storage failure — log and continue. The conversation works\n // without persistence; we just lost any pre-eviction messages.\n },\n )\n }\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 if (frame.t === 'user-input-submitted') {\n // FIFO delivery to a parked waiter, else buffer.\n // Shifting the head keeps \"first parked\" semantics; the parked\n // promise's cancel tears down its own timer before resolving.\n const waiter = p.userInputWaiters.shift()\n if (waiter) {\n waiter.cancel()\n waiter.resolve({ status: 'submitted', text: frame.text, at: frame.at })\n return\n }\n p.userInputBuffer.push({ text: frame.text, at: frame.at })\n if (p.userInputBuffer.length > USER_INPUT_BUFFER_CAP) {\n // Drop oldest. Newer messages are more contextually relevant\n // for an agent picking up a stale conversation.\n p.userInputBuffer.splice(0, p.userInputBuffer.length - USER_INPUT_BUFFER_CAP)\n }\n this.persistUserInputBuffer(tid, p.userInputBuffer)\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 waitForUserInput(tid: string, timeoutMs: number): Promise<UserInputResolution> {\n const p = this.pairings.get(tid)\n // Unknown / closed pairing → resolve as timeout immediately. The\n // LAP layer above us has already gated on `isPaired`, so this is\n // the rare race where the pairing closed between the gate and the\n // wait call. Returning timeout (instead of rejecting) keeps the\n // public response shape simple — agents only need to handle two\n // outcomes.\n if (!p || p.closed) {\n return Promise.resolve({ status: 'timeout' })\n }\n\n // Buffered submission already waiting → resolve synchronously.\n const buffered = p.userInputBuffer.shift()\n if (buffered) {\n this.persistUserInputBuffer(tid, p.userInputBuffer)\n return Promise.resolve({ status: 'submitted', text: buffered.text, at: buffered.at })\n }\n\n return new Promise<UserInputResolution>((resolve) => {\n let settled = false\n const settle = (value: UserInputResolution): void => {\n if (settled) return\n settled = true\n resolve(value)\n }\n\n const timer = setTimeout(() => {\n const idx = p.userInputWaiters.findIndex((w) => w === waiter)\n if (idx !== -1) p.userInputWaiters.splice(idx, 1)\n settle({ status: 'timeout' })\n }, timeoutMs)\n\n const waiter: UserInputWaiter = {\n resolve: (value) => {\n // dispatch() / handleClose() both call this. The `settled`\n // guard makes it idempotent — exactly one resolution survives,\n // regardless of arrival order.\n settle(value)\n },\n cancel: () => {\n clearTimeout(timer)\n unsubClose()\n },\n }\n p.userInputWaiters.push(waiter)\n\n // Pairing close before resolution: clean up and resolve as\n // timeout. handleClose sweeps the waiter queue and calls\n // `waiter.resolve({ status: 'timeout' })` directly, so this\n // close subscription is belt-and-braces — covers any path where\n // the registry's close cascade doesn't run (e.g. a custom\n // PairingConnection signalling close in an unusual order).\n const unsubClose = this.onClose(tid, () => {\n const idx = p.userInputWaiters.findIndex((w) => w === waiter)\n if (idx !== -1) p.userInputWaiters.splice(idx, 1)\n clearTimeout(timer)\n settle({ status: 'timeout' })\n })\n })\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 /**\n * Fire-and-forget write-through to the optional storage adapter.\n * Snapshots the buffer (defensive copy — the in-memory array can\n * mutate again before the adapter's write resolves) and ignores\n * rejections. Called on every buffer mutation; cheap when the\n * adapter is unset (no-ops) and best-effort when wired.\n */\n private persistUserInputBuffer(tid: string, buffer: Array<{ text: string; at: number }>): void {\n if (!this.userInputStorage) return\n // Defensive copy so the persisted snapshot is the buffer at the\n // moment of mutation, not whatever the buffer holds when the\n // storage adapter actually serializes the write.\n const snapshot = buffer.slice()\n void this.userInputStorage.write(tid, snapshot).catch(() => {\n /* best-effort; storage failures don't wedge the conversation */\n })\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 // Tear down user-input waiters BEFORE running closeHandlers.\n // Each waiter's `cancel` clears its timer + close-subscription;\n // resolving as `timeout` afterward is idempotent (the waiter's\n // `settled` guard short-circuits if its own close handler already\n // resolved it). Order matters: cancel first so the resolution\n // path can't re-enter handleClose via the close-subscription.\n const waiters = p.userInputWaiters.slice()\n p.userInputWaiters.length = 0\n for (const w of waiters) {\n try {\n w.cancel()\n } catch {\n // best-effort; resolution still proceeds\n }\n w.resolve({ status: 'timeout' })\n }\n // Drop buffered inputs — once the pairing is closed, an agent\n // resuming on a fresh tid shouldn't see stale messages from the\n // previous session. Same retention contract as recentLog.\n p.userInputBuffer.length = 0\n if (this.userInputStorage) {\n void this.userInputStorage.clear(tid).catch(() => {\n /* best-effort; storage failures don't block the close path */\n })\n }\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"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llui/agent",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"exports": {
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"types": "./dist/codecs.d.ts",
|
|
33
33
|
"import": "./dist/codecs.js"
|
|
34
34
|
},
|
|
35
|
+
"./mcp/tools": {
|
|
36
|
+
"types": "./dist/mcp/tools.d.ts",
|
|
37
|
+
"import": "./dist/mcp/tools.js"
|
|
38
|
+
},
|
|
35
39
|
"./styles/agent-panel.css": "./styles/agent-panel.css"
|
|
36
40
|
},
|
|
37
41
|
"files": [
|
|
@@ -39,7 +43,9 @@
|
|
|
39
43
|
"styles"
|
|
40
44
|
],
|
|
41
45
|
"dependencies": {
|
|
42
|
-
"
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
|
+
"ws": "^8.18.0",
|
|
48
|
+
"zod": "^4.0.0"
|
|
43
49
|
},
|
|
44
50
|
"peerDependencies": {
|
|
45
51
|
"@llui/dom": "^0.0.37"
|
package/styles/agent-panel.css
CHANGED
|
@@ -134,20 +134,7 @@
|
|
|
134
134
|
[data-scope='agent-log'] [data-part='entry'][data-kind='read'] {
|
|
135
135
|
border-left-color: rgba(148, 163, 184, 0.5); /* slate: passive */
|
|
136
136
|
}
|
|
137
|
-
[data-scope='agent-log'] [data-part='entry'][data-kind='user-input'] {
|
|
138
|
-
border-left-color: rgba(99, 102, 241, 0.7); /* indigo: user voice */
|
|
139
|
-
}
|
|
140
137
|
[data-scope='agent-log'] [data-part='entry'][data-kind='narrate'] {
|
|
141
138
|
border-left-color: rgba(168, 85, 247, 0.6); /* purple: agent voice */
|
|
142
139
|
font-style: italic;
|
|
143
140
|
}
|
|
144
|
-
|
|
145
|
-
/*
|
|
146
|
-
* Chat composer: subtle disabled state during in-flight submit so the
|
|
147
|
-
* user has a visible cue beyond the input element's own `disabled`
|
|
148
|
-
* styling (which varies by browser).
|
|
149
|
-
*/
|
|
150
|
-
[data-scope='agent-chat'][data-submitting='true'] {
|
|
151
|
-
opacity: 0.7;
|
|
152
|
-
pointer-events: none;
|
|
153
|
-
}
|