@itookit/dsht 0.3.8 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +33 -12
- package/README.zh.md +35 -14
- package/dist/catalog/controller.d.ts +26 -6
- package/dist/catalog/controller.js +73 -45
- package/dist/catalog/index.d.ts +1 -0
- package/dist/cli/dsht.js +206 -18
- package/dist/cli/startup.d.ts +40 -0
- package/dist/cli/startup.js +314 -0
- package/dist/cli/trace-summary.d.ts +78 -0
- package/dist/cli/trace-summary.js +241 -0
- package/dist/cli/verifier.d.ts +64 -0
- package/dist/cli/verifier.js +265 -0
- package/dist/contracts.d.ts +359 -0
- package/dist/contracts.js +1 -0
- package/dist/controller/commands.d.ts +47 -0
- package/dist/controller/commands.js +322 -0
- package/dist/controller/connection-streams.d.ts +22 -0
- package/dist/controller/connection-streams.js +105 -0
- package/dist/controller/connection.d.ts +24 -31
- package/dist/controller/connection.js +48 -111
- package/dist/controller/controller.d.ts +412 -178
- package/dist/controller/controller.js +713 -167
- package/dist/controller/foreground.d.ts +44 -0
- package/dist/controller/foreground.js +79 -0
- package/dist/controller/index.d.ts +8 -1
- package/dist/controller/index.js +5 -0
- package/dist/controller/loop-contract.d.ts +136 -0
- package/dist/controller/loop-contract.js +308 -0
- package/dist/controller/loop-coordinator.d.ts +48 -0
- package/dist/controller/loop-coordinator.js +647 -0
- package/dist/controller/loop-prompts-schema.d.ts +56 -0
- package/dist/controller/loop-prompts-schema.js +144 -0
- package/dist/controller/loop-prompts.d.ts +55 -0
- package/dist/controller/loop-prompts.generated.d.ts +104 -0
- package/dist/controller/loop-prompts.generated.js +185 -0
- package/dist/controller/loop-prompts.js +104 -0
- package/dist/controller/loop-protocols.d.ts +39 -0
- package/dist/controller/loop-protocols.js +115 -0
- package/dist/controller/loop.d.ts +275 -0
- package/dist/controller/loop.js +378 -0
- package/dist/controller/prompts.d.ts +54 -0
- package/dist/controller/prompts.js +162 -0
- package/dist/controller/trace-log.d.ts +45 -0
- package/dist/controller/trace-log.js +144 -0
- package/dist/controller/verifier.d.ts +130 -0
- package/dist/controller/verifier.js +75 -0
- package/dist/cost/controller.d.ts +1 -1
- package/dist/cost/controller.js +12 -5
- package/dist/cost/index.d.ts +1 -1
- package/dist/cost/index.js +1 -1
- package/dist/cost/ledger.d.ts +0 -1
- package/dist/cost/ledger.js +0 -1
- package/dist/cost/scanner.js +1 -0
- package/dist/json.d.ts +18 -0
- package/dist/json.js +19 -0
- package/dist/references.d.ts +25 -0
- package/dist/references.js +26 -0
- package/dist/session/connection-view.d.ts +2 -11
- package/dist/session/controller.d.ts +94 -105
- package/dist/session/controller.js +262 -536
- package/dist/session/history-reader.d.ts +32 -0
- package/dist/session/history-reader.js +170 -0
- package/dist/session/history.d.ts +6 -18
- package/dist/session/history.js +1 -24
- package/dist/session/index.d.ts +9 -4
- package/dist/session/index.js +7 -3
- package/dist/session/info.d.ts +20 -82
- package/dist/session/info.js +52 -25
- package/dist/session/interactions.d.ts +26 -0
- package/dist/session/interactions.js +75 -0
- package/dist/session/markdown.js +1 -1
- package/dist/session/math.js +1 -1
- package/dist/session/mutation-gate.d.ts +51 -0
- package/dist/session/mutation-gate.js +73 -0
- package/dist/session/navigation.d.ts +2 -89
- package/dist/session/navigation.js +2 -129
- package/dist/session/navigator.d.ts +47 -0
- package/dist/session/navigator.js +158 -0
- package/dist/session/peek.d.ts +38 -0
- package/dist/session/peek.js +103 -0
- package/dist/session/prompt-backfill.d.ts +23 -0
- package/dist/session/prompt-backfill.js +88 -0
- package/dist/session/references.d.ts +2 -20
- package/dist/session/references.js +1 -26
- package/dist/session/runtime.d.ts +26 -0
- package/dist/session/runtime.js +28 -0
- package/dist/session/state.d.ts +20 -0
- package/dist/session/state.js +1 -0
- package/dist/session/telemetry.d.ts +25 -17
- package/dist/session/telemetry.js +66 -60
- package/dist/session/transcript.d.ts +5 -7
- package/dist/session/transcript.js +2 -15
- package/dist/session/types.d.ts +25 -0
- package/dist/session/types.js +0 -1
- package/dist/session-title.d.ts +9 -0
- package/dist/session-title.js +21 -0
- package/dist/shell/controller.d.ts +31 -1
- package/dist/shell/controller.js +34 -2
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/index.js +2 -2
- package/dist/shell/runner.d.ts +10 -0
- package/dist/shell/runner.js +48 -9
- package/dist/slash/index.d.ts +10 -0
- package/dist/slash/index.js +7 -0
- package/dist/slash/parse.d.ts +42 -0
- package/dist/slash/parse.js +259 -0
- package/dist/slash/pipeline.d.ts +140 -0
- package/dist/slash/pipeline.js +115 -0
- package/dist/slash/registry.d.ts +88 -0
- package/dist/slash/registry.js +177 -0
- package/dist/slash/types.d.ts +126 -0
- package/dist/slash/types.js +1 -0
- package/dist/state.d.ts +16 -18
- package/dist/state.js +4 -3
- package/dist/text.d.ts +28 -0
- package/dist/text.js +55 -0
- package/dist/transport/client.d.ts +4 -3
- package/dist/transport/client.js +71 -25
- package/dist/transport/events.d.ts +104 -0
- package/dist/transport/events.js +149 -0
- package/dist/transport/wire.d.ts +9 -17
- package/dist/transport/wire.js +2 -27
- package/dist/ui/app.js +750 -550
- package/dist/ui/chat/header.js +1 -1
- package/dist/ui/chat/history-view.d.ts +1 -1
- package/dist/ui/chat/loop-status.d.ts +11 -0
- package/dist/ui/chat/loop-status.js +28 -0
- package/dist/ui/chat/navigation-model.d.ts +86 -0
- package/dist/ui/chat/navigation-model.js +107 -0
- package/dist/ui/chat/shell-view.d.ts +17 -2
- package/dist/ui/chat/shell-view.js +45 -3
- package/dist/ui/chat/status.d.ts +47 -3
- package/dist/ui/chat/status.js +65 -50
- package/dist/ui/chat/use-history-view.d.ts +69 -0
- package/dist/ui/chat/use-history-view.js +123 -0
- package/dist/ui/chat/viewport.d.ts +1 -1
- package/dist/ui/dialogs/cost.d.ts +21 -4
- package/dist/ui/dialogs/cost.js +7 -12
- package/dist/ui/dialogs/index.d.ts +22 -5
- package/dist/ui/dialogs/index.js +19 -3
- package/dist/ui/dialogs/loop.d.ts +43 -0
- package/dist/ui/dialogs/loop.js +224 -0
- package/dist/ui/dialogs/peek.d.ts +25 -0
- package/dist/ui/dialogs/peek.js +35 -0
- package/dist/ui/dialogs/picker.d.ts +2 -0
- package/dist/ui/dialogs/picker.js +4 -2
- package/dist/ui/dialogs/use-panels.d.ts +53 -0
- package/dist/ui/dialogs/use-panels.js +51 -0
- package/dist/ui/input/mouse.d.ts +12 -2
- package/dist/ui/input/mouse.js +20 -7
- package/dist/ui/input/references.d.ts +1 -1
- package/dist/ui/input/use-composer.d.ts +35 -0
- package/dist/ui/input/use-composer.js +109 -0
- package/dist/ui/input/use-deferred-lines.d.ts +16 -0
- package/dist/ui/input/use-deferred-lines.js +54 -0
- package/dist/ui/input/use-history-recall.d.ts +20 -0
- package/dist/ui/input/use-history-recall.js +47 -0
- package/dist/ui/status/model.d.ts +7 -0
- package/dist/ui/status/model.js +5 -0
- package/dist/ui/theme/index.d.ts +1 -1
- package/package.json +6 -4
- package/dist/ui/commands/parse.d.ts +0 -104
- package/dist/ui/commands/parse.js +0 -135
- package/dist/ui/commands/registry.d.ts +0 -33
- package/dist/ui/commands/registry.js +0 -73
package/dist/transport/client.js
CHANGED
|
@@ -22,7 +22,7 @@ export class RemoteError extends Error {
|
|
|
22
22
|
this.details = error.details;
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/** One client lifetime. Peer disconnects allow reconnect; close permanently ends the lifetime. */
|
|
26
26
|
export class Client {
|
|
27
27
|
timeoutMs;
|
|
28
28
|
base;
|
|
@@ -31,6 +31,7 @@ export class Client {
|
|
|
31
31
|
socket;
|
|
32
32
|
listeners = new Map();
|
|
33
33
|
lifetime = new AbortController();
|
|
34
|
+
closeTask;
|
|
34
35
|
constructor(base, timeoutMs = 15_000) {
|
|
35
36
|
this.timeoutMs = timeoutMs;
|
|
36
37
|
this.base = new URL(base);
|
|
@@ -117,13 +118,17 @@ export class Client {
|
|
|
117
118
|
}
|
|
118
119
|
/** Connect the physical mux. A disconnected instance may reconnect with its cookie. */
|
|
119
120
|
async connect() {
|
|
121
|
+
this.lifetime.signal.throwIfAborted();
|
|
120
122
|
if (this.socket)
|
|
121
123
|
throw new Error('Mux is already connected');
|
|
122
124
|
const url = new URL('/api/remote.mux', this.base);
|
|
123
125
|
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
124
126
|
const socket = new WebSocket(url, { headers: { cookie: this.cookie }, handshakeTimeout: this.timeoutMs });
|
|
125
127
|
this.socket = socket;
|
|
128
|
+
const closed = new Promise(resolve => socket.once('close', () => resolve()));
|
|
126
129
|
socket.on('message', (raw, binary) => {
|
|
130
|
+
if (this.socket !== socket || this.lifetime.signal.aborted)
|
|
131
|
+
return;
|
|
127
132
|
try {
|
|
128
133
|
if (binary)
|
|
129
134
|
throw new Error('Unexpected binary mux frame');
|
|
@@ -154,22 +159,40 @@ export class Client {
|
|
|
154
159
|
socket.terminate();
|
|
155
160
|
}
|
|
156
161
|
});
|
|
157
|
-
socket.on('error', error => this.
|
|
162
|
+
socket.on('error', error => { if (this.socket === socket)
|
|
163
|
+
this.fail(error); });
|
|
158
164
|
socket.on('close', () => {
|
|
159
|
-
if (this.socket
|
|
160
|
-
|
|
165
|
+
if (this.socket !== socket)
|
|
166
|
+
return;
|
|
167
|
+
this.socket = undefined;
|
|
161
168
|
this.fail(new Error('Connection closed'));
|
|
162
169
|
});
|
|
163
|
-
|
|
170
|
+
try {
|
|
171
|
+
await once(socket, 'open', { signal: this.lifetime.signal });
|
|
172
|
+
this.lifetime.signal.throwIfAborted();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (socket.readyState !== WebSocket.CLOSED)
|
|
176
|
+
socket.terminate();
|
|
177
|
+
await closed;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
164
180
|
}
|
|
165
181
|
/** Subscribe on the existing mux; each subscription has a fresh stream identity. */
|
|
166
182
|
subscribe(endpoint, args, listener) {
|
|
183
|
+
this.lifetime.signal.throwIfAborted();
|
|
167
184
|
const socket = this.socket;
|
|
168
185
|
if (socket?.readyState !== WebSocket.OPEN)
|
|
169
186
|
throw new Error('Mux is not connected');
|
|
170
187
|
const streamId = randomUUID();
|
|
171
188
|
this.listeners.set(streamId, listener);
|
|
172
|
-
|
|
189
|
+
try {
|
|
190
|
+
socket.send(JSON.stringify({ type: 'open', streamId, endpoint, payload: { args } }));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
this.listeners.delete(streamId);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
173
196
|
return { cancel: () => {
|
|
174
197
|
if (!this.listeners.delete(streamId))
|
|
175
198
|
return;
|
|
@@ -180,57 +203,80 @@ export class Client {
|
|
|
180
203
|
/** Archived session IDs from the latest authoritative workspace baseline. */
|
|
181
204
|
archivedSessionIds = new Set();
|
|
182
205
|
/** List workspaces by consuming and cancelling the authoritative opening baseline. */
|
|
183
|
-
async listWorkspaces() {
|
|
206
|
+
async listWorkspaces(signal) {
|
|
207
|
+
signal?.throwIfAborted();
|
|
184
208
|
return new Promise((resolve, reject) => {
|
|
185
209
|
let sub;
|
|
186
|
-
|
|
210
|
+
let settled = false;
|
|
211
|
+
const finish = (error, items) => {
|
|
212
|
+
if (settled)
|
|
213
|
+
return;
|
|
214
|
+
settled = true;
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
signal?.removeEventListener('abort', cancel);
|
|
217
|
+
sub?.cancel();
|
|
218
|
+
if (error !== undefined)
|
|
219
|
+
reject(error);
|
|
220
|
+
else
|
|
221
|
+
resolve(items);
|
|
222
|
+
};
|
|
223
|
+
const cancel = () => finish(signal.reason);
|
|
224
|
+
const timer = setTimeout(() => finish(new Error('Workspace baseline timed out')), this.timeoutMs);
|
|
225
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
187
226
|
try {
|
|
188
227
|
sub = this.subscribe('workspace/follow', {}, {
|
|
189
228
|
item: value => {
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
if (settled)
|
|
230
|
+
return;
|
|
192
231
|
try {
|
|
193
232
|
const frame = object(value);
|
|
194
233
|
if (frame.type !== 'baseline')
|
|
195
234
|
throw new Error('Workspace stream omitted its baseline');
|
|
196
235
|
this.archivedSessionIds = new Set(array(object(frame.value).archivedSessionIds).map(string));
|
|
197
|
-
|
|
236
|
+
finish(undefined, array(object(frame.value).items).map(object));
|
|
198
237
|
}
|
|
199
238
|
catch (error) {
|
|
200
|
-
|
|
239
|
+
finish(error);
|
|
201
240
|
}
|
|
202
241
|
},
|
|
203
|
-
end: error =>
|
|
242
|
+
end: error => finish(error ?? new Error('Workspace stream ended before baseline')),
|
|
204
243
|
});
|
|
244
|
+
if (settled)
|
|
245
|
+
sub.cancel();
|
|
205
246
|
}
|
|
206
247
|
catch (error) {
|
|
207
|
-
|
|
208
|
-
reject(error);
|
|
248
|
+
finish(error);
|
|
209
249
|
}
|
|
210
250
|
});
|
|
211
251
|
}
|
|
212
252
|
/** List visible sessions, optionally filtering by the workspace's accounted IDs. */
|
|
213
|
-
async listSessions(workspaceId) {
|
|
214
|
-
const sessions = array(object(await this.call('session/list', { _request: {} })).items).map(object);
|
|
253
|
+
async listSessions(workspaceId, signal) {
|
|
254
|
+
const sessions = array(object(await this.call('session/list', { _request: {} }, signal)).items).map(object);
|
|
215
255
|
if (!workspaceId)
|
|
216
256
|
return sessions;
|
|
217
|
-
const workspace = (await this.listWorkspaces()).find(item => item.workspaceId === workspaceId);
|
|
257
|
+
const workspace = (await this.listWorkspaces(signal)).find(item => item.workspaceId === workspaceId);
|
|
218
258
|
if (!workspace)
|
|
219
259
|
throw new Error(`Workspace not found: ${workspaceId}`);
|
|
220
260
|
const ids = new Set(array(workspace.sessionIds).map(string));
|
|
221
261
|
return sessions.filter(item => ids.has(string(item.sessionId)));
|
|
222
262
|
}
|
|
223
263
|
/** Close all streams, abort in-flight HTTP, and await the physical socket's closure. */
|
|
224
|
-
|
|
225
|
-
this.
|
|
264
|
+
close() {
|
|
265
|
+
if (this.closeTask)
|
|
266
|
+
return this.closeTask;
|
|
226
267
|
const socket = this.socket;
|
|
227
268
|
this.socket = undefined;
|
|
269
|
+
// Reserve before publishing cancellation: termination callbacks may themselves call close.
|
|
270
|
+
this.closeTask = Promise.resolve().then(async () => {
|
|
271
|
+
if (socket && socket.readyState !== WebSocket.CLOSED) {
|
|
272
|
+
const closed = new Promise(resolve => socket.once('close', () => resolve()));
|
|
273
|
+
socket.terminate();
|
|
274
|
+
await closed;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
this.lifetime.abort();
|
|
228
278
|
this.fail(new Error('Client closed'));
|
|
229
|
-
|
|
230
|
-
const closed = new Promise(resolve => socket.once('close', () => resolve()));
|
|
231
|
-
socket.terminate();
|
|
232
|
-
await closed;
|
|
233
|
-
}
|
|
279
|
+
return this.closeTask;
|
|
234
280
|
}
|
|
235
281
|
signal(timeoutMs = this.timeoutMs) {
|
|
236
282
|
return timeoutMs === null ? this.lifetime.signal : AbortSignal.any([this.lifetime.signal, AbortSignal.timeout(timeoutMs)]);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** One place that reads DSH event field names and turns them into semantic host events.
|
|
2
|
+
*
|
|
3
|
+
* Raw wire stops here: no `Json`/`ObjectValue` payload survives into a `HostEvent`, so `connection/`,
|
|
4
|
+
* `session/` and the UI never learn how the host names its fields. Replying `{kind:'next'}` to an
|
|
5
|
+
* unrecognized waterfall is a wire-level obligation of the host's event chain, so it is modelled as
|
|
6
|
+
* `waterfall-delegate` rather than silently dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { type Json, type ObjectValue } from './wire.ts';
|
|
9
|
+
/** One selectable answer of a host question, already flattened out of the raw request. */
|
|
10
|
+
export interface QuestionOption {
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
}
|
|
14
|
+
/** One question of an `user-questions/request` waterfall. */
|
|
15
|
+
export interface QuestionItem {
|
|
16
|
+
id: string;
|
|
17
|
+
header?: string;
|
|
18
|
+
question: string;
|
|
19
|
+
detail?: string;
|
|
20
|
+
multiSelect: boolean;
|
|
21
|
+
options: readonly QuestionOption[];
|
|
22
|
+
}
|
|
23
|
+
/** A decoded host event with protocol field names already resolved into domain meaning. */
|
|
24
|
+
export type HostEvent = {
|
|
25
|
+
kind: 'approval-request';
|
|
26
|
+
eventId: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
description: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'question-request';
|
|
31
|
+
eventId: string;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
questions: readonly QuestionItem[];
|
|
34
|
+
}
|
|
35
|
+
/** A waterfall this client does not answer; the caller must reply `next` to keep the host chain moving. */
|
|
36
|
+
| {
|
|
37
|
+
kind: 'waterfall-delegate';
|
|
38
|
+
eventId: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: 'cancel';
|
|
41
|
+
eventId: string;
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'agent-status';
|
|
44
|
+
sessionId: string;
|
|
45
|
+
running: boolean;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'catalog-invalidated';
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'session-error';
|
|
50
|
+
sessionId: string;
|
|
51
|
+
error: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'control';
|
|
54
|
+
frame: ControlFrame;
|
|
55
|
+
};
|
|
56
|
+
/** One host-owned pending input, flattened to the text the UI shows. */
|
|
57
|
+
export interface QueuedInput {
|
|
58
|
+
id: string;
|
|
59
|
+
placement: 'queued' | 'steering' | 'context';
|
|
60
|
+
text: string;
|
|
61
|
+
}
|
|
62
|
+
/** A capability value the host owns. Individual keys are typed incrementally as they are consumed. */
|
|
63
|
+
export type ProjectionValue = Json;
|
|
64
|
+
/** One session's projection values and the sequence they describe. */
|
|
65
|
+
export interface ProjectionSnapshot {
|
|
66
|
+
asOfSeq: number;
|
|
67
|
+
values: Readonly<Record<string, ProjectionValue>>;
|
|
68
|
+
}
|
|
69
|
+
/** One decoded `session/control` frame with protocol field names already resolved. */
|
|
70
|
+
export type ControlFrame = {
|
|
71
|
+
kind: 'baseline';
|
|
72
|
+
projections: ReadonlyMap<string, ProjectionSnapshot>;
|
|
73
|
+
queues: ReadonlyMap<string, readonly QueuedInput[]>;
|
|
74
|
+
jobs: ReadonlyMap<string, number>;
|
|
75
|
+
} | {
|
|
76
|
+
kind: 'projection';
|
|
77
|
+
sessionId: string;
|
|
78
|
+
key: string;
|
|
79
|
+
seq: number;
|
|
80
|
+
value: ProjectionValue;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'queue';
|
|
83
|
+
sessionId: string;
|
|
84
|
+
items: readonly QueuedInput[];
|
|
85
|
+
} | {
|
|
86
|
+
kind: 'jobs';
|
|
87
|
+
sessionId: string;
|
|
88
|
+
count: number;
|
|
89
|
+
};
|
|
90
|
+
/** Decode one projection baseline, as carried by a `session/control` baseline or a follow snapshot. */
|
|
91
|
+
export declare function projectionSnapshot(value: unknown): ProjectionSnapshot | undefined;
|
|
92
|
+
/** Turn one decoded `session/control` frame into a semantic control frame.
|
|
93
|
+
* @param value - One decoded `session/control` frame.
|
|
94
|
+
* @returns The normalized frame.
|
|
95
|
+
*/
|
|
96
|
+
export declare function controlFrame(value: unknown): ControlFrame;
|
|
97
|
+
/** Turn one decoded `$events` frame into a semantic host event.
|
|
98
|
+
*
|
|
99
|
+
* Returns `undefined` for anything this client does not act on, including the `ready` handshake,
|
|
100
|
+
* which belongs to the connection lifecycle rather than to a domain.
|
|
101
|
+
* @param frame - One decoded `$events` frame.
|
|
102
|
+
* @returns The normalized event, or undefined when it is not one this client consumes.
|
|
103
|
+
*/
|
|
104
|
+
export declare function hostEvent(frame: ObjectValue): HostEvent | undefined;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** One place that reads DSH event field names and turns them into semantic host events.
|
|
2
|
+
*
|
|
3
|
+
* Raw wire stops here: no `Json`/`ObjectValue` payload survives into a `HostEvent`, so `connection/`,
|
|
4
|
+
* `session/` and the UI never learn how the host names its fields. Replying `{kind:'next'}` to an
|
|
5
|
+
* unrecognized waterfall is a wire-level obligation of the host's event chain, so it is modelled as
|
|
6
|
+
* `waterfall-delegate` rather than silently dropped.
|
|
7
|
+
*/
|
|
8
|
+
import { array, errorText, object, string } from "./wire.js";
|
|
9
|
+
/** Host notifications that invalidate the model catalog. */
|
|
10
|
+
const CATALOG_EVENTS = new Set(['llm/adapters-updated', 'settings/document-updated', 'credentials/reference-updated']);
|
|
11
|
+
/** Require a projection watermark rather than accepting a rolled-back value. */
|
|
12
|
+
function sequence(value) {
|
|
13
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < -1)
|
|
14
|
+
throw new Error('Invalid projection watermark');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
/** Flatten one session's pending-input list. */
|
|
18
|
+
function queuedInputs(value) {
|
|
19
|
+
return array(value).map(raw => {
|
|
20
|
+
const item = object(raw);
|
|
21
|
+
if (!['queued', 'steering', 'context'].includes(string(item.placement)))
|
|
22
|
+
throw new Error('Invalid queue placement');
|
|
23
|
+
return { id: string(item.id), placement: item.placement,
|
|
24
|
+
text: array(object(item.message).content).map(object)
|
|
25
|
+
.map(block => block.type === 'text' ? string(block.text) : `[${string(block.type)}]`).join(' ') };
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** Count the jobs the host still considers active. */
|
|
29
|
+
function activeJobs(value) {
|
|
30
|
+
return array(value).filter(item => ['running', 'stopping'].includes(string(object(item).status))).length;
|
|
31
|
+
}
|
|
32
|
+
/** Decode one projection baseline, as carried by a `session/control` baseline or a follow snapshot. */
|
|
33
|
+
export function projectionSnapshot(value) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
const baseline = object(value);
|
|
37
|
+
return { asOfSeq: sequence(baseline.asOfSeq), values: object(baseline.values) };
|
|
38
|
+
}
|
|
39
|
+
/** Turn one decoded `session/control` frame into a semantic control frame.
|
|
40
|
+
* @param value - One decoded `session/control` frame.
|
|
41
|
+
* @returns The normalized frame.
|
|
42
|
+
*/
|
|
43
|
+
export function controlFrame(value) {
|
|
44
|
+
const frame = object(value);
|
|
45
|
+
if (frame.type === 'baseline') {
|
|
46
|
+
const baseline = object(frame.value);
|
|
47
|
+
const projections = Object.entries(object(baseline.projections))
|
|
48
|
+
.map(([id, snapshot]) => [id, projectionSnapshot(snapshot)]);
|
|
49
|
+
const queues = Object.entries(object(baseline.queues)).map(([id, items]) => [id, queuedInputs(items)]);
|
|
50
|
+
const jobs = Object.entries(object(baseline.jobs)).map(([id, items]) => [id, activeJobs(items)]);
|
|
51
|
+
return { kind: 'baseline', projections: new Map(projections), queues: new Map(queues), jobs: new Map(jobs) };
|
|
52
|
+
}
|
|
53
|
+
if (frame.type === 'projection' || frame.type === 'queue' || frame.type === 'jobs') {
|
|
54
|
+
const sessionId = string(frame.sessionId);
|
|
55
|
+
if (frame.type === 'projection') {
|
|
56
|
+
if (frame.value === undefined)
|
|
57
|
+
throw new Error('Missing projection value');
|
|
58
|
+
return { kind: 'projection', sessionId, key: string(frame.key), seq: sequence(frame.seq), value: frame.value };
|
|
59
|
+
}
|
|
60
|
+
if (frame.type === 'queue')
|
|
61
|
+
return { kind: 'queue', sessionId, items: queuedInputs(frame.items) };
|
|
62
|
+
// The host names the job rows `jobs`; the loopback fixtures used `items`, so accept either.
|
|
63
|
+
return { kind: 'jobs', sessionId, count: activeJobs(frame.jobs ?? frame.items) };
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Unknown session control frame');
|
|
66
|
+
}
|
|
67
|
+
/** Read a possibly-absent string field without turning protocol drift into a throw. */
|
|
68
|
+
function optionalString(value) {
|
|
69
|
+
return typeof value === 'string' ? value : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Flatten one raw question into the semantic shape the session domain retains. */
|
|
72
|
+
function questionItem(value) {
|
|
73
|
+
const item = object(value);
|
|
74
|
+
const options = array(item.options ?? []).map(raw => {
|
|
75
|
+
const option = object(raw);
|
|
76
|
+
const description = optionalString(option.description);
|
|
77
|
+
return { label: string(option.label), ...(description === undefined ? {} : { description }) };
|
|
78
|
+
});
|
|
79
|
+
const header = optionalString(item.header);
|
|
80
|
+
const detail = optionalString(item.detail);
|
|
81
|
+
return {
|
|
82
|
+
id: string(item.id),
|
|
83
|
+
...(header === undefined ? {} : { header }),
|
|
84
|
+
question: string(item.question),
|
|
85
|
+
...(detail === undefined ? {} : { detail }),
|
|
86
|
+
multiSelect: item.multiSelect === true,
|
|
87
|
+
options,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Decode one `$events` waterfall frame. */
|
|
91
|
+
function waterfallEvent(frame) {
|
|
92
|
+
const eventId = string(frame.eventId);
|
|
93
|
+
const sessionId = optionalString(frame.agentId) ?? '';
|
|
94
|
+
if (frame.event === 'approval/request') {
|
|
95
|
+
// The approval body is host-defined and currently rendered verbatim; formatting it here keeps
|
|
96
|
+
// the terminal output byte-identical while removing the raw object from the boundary.
|
|
97
|
+
return { kind: 'approval-request', eventId, sessionId, description: JSON.stringify(frame.request ?? null, null, 2) };
|
|
98
|
+
}
|
|
99
|
+
if (frame.event === 'user-questions/request') {
|
|
100
|
+
try {
|
|
101
|
+
const request = object(frame.request);
|
|
102
|
+
return { kind: 'question-request', eventId, sessionId, questions: array(request.questions ?? []).map(questionItem) };
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// A malformed question still has to be settled, or the host's event chain blocks on it.
|
|
106
|
+
return { kind: 'waterfall-delegate', eventId };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'waterfall-delegate', eventId };
|
|
110
|
+
}
|
|
111
|
+
/** Decode one `$events` emit frame this client reacts to. */
|
|
112
|
+
function emitEvent(frame) {
|
|
113
|
+
const name = frame.event;
|
|
114
|
+
if (name === 'api-session/status') {
|
|
115
|
+
const args = array(frame.args);
|
|
116
|
+
if (typeof args[1] !== 'boolean')
|
|
117
|
+
throw new Error('Invalid session running state');
|
|
118
|
+
return { kind: 'agent-status', sessionId: string(args[0]), running: args[1] };
|
|
119
|
+
}
|
|
120
|
+
if (name === 'api-session/error') {
|
|
121
|
+
const args = array(frame.args);
|
|
122
|
+
return { kind: 'session-error', sessionId: optionalString(args[0]) ?? '', error: errorText(args[1]) };
|
|
123
|
+
}
|
|
124
|
+
if (typeof name === 'string' && CATALOG_EVENTS.has(name))
|
|
125
|
+
return { kind: 'catalog-invalidated' };
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Turn one decoded `$events` frame into a semantic host event.
|
|
129
|
+
*
|
|
130
|
+
* Returns `undefined` for anything this client does not act on, including the `ready` handshake,
|
|
131
|
+
* which belongs to the connection lifecycle rather than to a domain.
|
|
132
|
+
* @param frame - One decoded `$events` frame.
|
|
133
|
+
* @returns The normalized event, or undefined when it is not one this client consumes.
|
|
134
|
+
*/
|
|
135
|
+
export function hostEvent(frame) {
|
|
136
|
+
try {
|
|
137
|
+
if (frame.type === 'waterfall')
|
|
138
|
+
return waterfallEvent(frame);
|
|
139
|
+
if (frame.type === 'cancel')
|
|
140
|
+
return { kind: 'cancel', eventId: string(frame.eventId) };
|
|
141
|
+
if (frame.type === 'emit')
|
|
142
|
+
return emitEvent(frame);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// A frame this client cannot decode is not allowed to fail the whole subscription.
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
package/dist/transport/wire.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
/** Require a string field rather than silently accepting protocol drift. */
|
|
11
|
-
export declare function string(value: unknown): string;
|
|
12
|
-
/** Require an array field from the server. */
|
|
13
|
-
export declare function array(value: unknown): Json[];
|
|
14
|
-
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
15
|
-
export declare function safeText(value: string): string;
|
|
16
|
-
/** Return a displayable error without serializing request headers or credentials. */
|
|
17
|
-
export declare function errorText(error: unknown): string;
|
|
1
|
+
/** The wire's decoding surface.
|
|
2
|
+
*
|
|
3
|
+
* The JSON shapes, their strict readers and the displayable error text are dependency-free leaves
|
|
4
|
+
* (`src/json.ts`, `src/text.ts`), so the UI can name the same types without importing the transport
|
|
5
|
+
* domain. This module re-exports them as the wire-facing API the features already use.
|
|
6
|
+
*/
|
|
7
|
+
export type { Json, ObjectValue } from '../json.ts';
|
|
8
|
+
export { array, object, string } from '../json.ts';
|
|
9
|
+
export { errorText, safeText } from '../text.ts';
|
package/dist/transport/wire.js
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
4
|
-
throw new Error('Expected a JSON object from the server');
|
|
5
|
-
}
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
/** Require a string field rather than silently accepting protocol drift. */
|
|
9
|
-
export function string(value) {
|
|
10
|
-
if (typeof value !== 'string')
|
|
11
|
-
throw new Error('Expected a string from the server');
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
/** Require an array field from the server. */
|
|
15
|
-
export function array(value) {
|
|
16
|
-
if (!Array.isArray(value))
|
|
17
|
-
throw new Error('Expected an array from the server');
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
|
-
/** Remove terminal controls from remote text while retaining line breaks and tabs. */
|
|
21
|
-
export function safeText(value) {
|
|
22
|
-
return value.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, '');
|
|
23
|
-
}
|
|
24
|
-
/** Return a displayable error without serializing request headers or credentials. */
|
|
25
|
-
export function errorText(error) {
|
|
26
|
-
return safeText(error instanceof Error ? error.message : String(error));
|
|
27
|
-
}
|
|
1
|
+
export { array, object, string } from "../json.js";
|
|
2
|
+
export { errorText, safeText } from "../text.js";
|