@ours.network/fleet 0.15.1 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -13
- package/dist/application/fleet-query-service.js +3 -0
- package/dist/application/model-catalog.d.ts +20 -0
- package/dist/application/model-catalog.js +57 -0
- package/dist/application/role-creation-service.d.ts +7 -0
- package/dist/application/role-creation-service.js +21 -4
- package/dist/application/role-removal-service.d.ts +32 -0
- package/dist/application/role-removal-service.js +87 -0
- package/dist/application/role-repository.js +13 -1
- package/dist/application/session-control.d.ts +74 -0
- package/dist/application/session-control.js +66 -1
- package/dist/application/types.d.ts +18 -0
- package/dist/briefing.js +21 -1
- package/dist/cli.js +39 -7
- package/dist/config.d.ts +4 -1
- package/dist/config.js +3 -2
- package/dist/creation.d.ts +6 -3
- package/dist/creation.js +5 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +47 -11
- package/dist/fleet-proxy.d.ts +25 -0
- package/dist/fleet-proxy.js +38 -0
- package/dist/harness/claude-code.js +20 -3
- package/dist/harness/codex.js +14 -2
- package/dist/harness/types.d.ts +6 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/owner-channel/channel.d.ts +13 -0
- package/dist/owner-channel/channel.js +191 -9
- package/dist/owner-channel/state.d.ts +7 -1
- package/dist/owner-channel/state.js +41 -4
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.js +7 -0
- package/dist/runner.d.ts +2 -0
- package/dist/runner.js +86 -2
- package/dist/session/acp.d.ts +61 -1
- package/dist/session/acp.js +398 -20
- package/dist/session/arbiter.d.ts +10 -1
- package/dist/session/arbiter.js +24 -0
- package/dist/session/control.d.ts +33 -2
- package/dist/session/control.js +158 -5
- package/dist/session/conversation-normalizer.d.ts +34 -0
- package/dist/session/conversation-normalizer.js +356 -0
- package/dist/session/conversation-store.d.ts +88 -0
- package/dist/session/conversation-store.js +347 -0
- package/dist/session/conversation-types.d.ts +274 -0
- package/dist/session/conversation-types.js +1 -0
- package/dist/session/types.d.ts +40 -0
- package/dist/spawn.d.ts +6 -1
- package/dist/spawn.js +23 -16
- package/dist/web/auth.d.ts +1 -1
- package/dist/web/fleet-config-service.d.ts +47 -0
- package/dist/web/fleet-config-service.js +204 -0
- package/dist/web/runtime.js +14 -1
- package/dist/web/server.d.ts +6 -0
- package/dist/web/server.js +181 -9
- package/dist/web/topology.d.ts +31 -0
- package/dist/web/topology.js +61 -0
- package/dist/web-app/assets/{TerminalView-DMoT8udI.js → TerminalView-hZpyUFY_.js} +1 -1
- package/dist/web-app/assets/index-COg4Azq1.css +1 -0
- package/dist/web-app/assets/index-Cde9auW0.js +10 -0
- package/dist/web-app/index.html +2 -2
- package/package.json +1 -1
- package/dist/web-app/assets/index-B-jtLAkp.css +0 -1
- package/dist/web-app/assets/index-B6T8JLSd.js +0 -9
package/dist/session/arbiter.js
CHANGED
|
@@ -58,12 +58,36 @@ export class RoleTurnArbiter {
|
|
|
58
58
|
stopScheduledAdmission() { this.stopping = true; }
|
|
59
59
|
isAlive() { return this.session.isAlive(); }
|
|
60
60
|
snapshot() { return this.session.snapshot(); }
|
|
61
|
+
conversationPage(request = {}) {
|
|
62
|
+
if (!this.session.conversationPage)
|
|
63
|
+
throw new Error('conversation ledger is unavailable');
|
|
64
|
+
return this.session.conversationPage(request);
|
|
65
|
+
}
|
|
66
|
+
conversationSnapshot() {
|
|
67
|
+
if (!this.session.conversationSnapshot)
|
|
68
|
+
throw new Error('conversation snapshot is unavailable');
|
|
69
|
+
return this.session.conversationSnapshot();
|
|
70
|
+
}
|
|
71
|
+
subscribeConversation(listener) {
|
|
72
|
+
if (!this.session.subscribeConversation)
|
|
73
|
+
throw new Error('conversation subscription is unavailable');
|
|
74
|
+
return this.session.subscribeConversation(listener);
|
|
75
|
+
}
|
|
76
|
+
submitPromptBrowser(command) {
|
|
77
|
+
if (!this.session.submitPromptBrowser)
|
|
78
|
+
return Promise.reject(new Error('browser prompt admission is unavailable'));
|
|
79
|
+
return this.exclusive(() => this.session.submitPromptBrowser(command));
|
|
80
|
+
}
|
|
61
81
|
interrupt(source = 'local-console') {
|
|
62
82
|
return this.exclusive(() => this.session.interrupt(source));
|
|
63
83
|
}
|
|
64
84
|
respondPermission(permissionId, optionId) {
|
|
65
85
|
return this.session.respondPermission(permissionId, optionId);
|
|
66
86
|
}
|
|
87
|
+
respondPermissionV2(permissionId, optionId, sessionGeneration) {
|
|
88
|
+
return this.session.respondPermissionV2?.(permissionId, optionId, sessionGeneration)
|
|
89
|
+
?? 'stale';
|
|
90
|
+
}
|
|
67
91
|
eventsSince(seq) { return this.session.eventsSince(seq); }
|
|
68
92
|
subscribe(listener) { return this.session.subscribe(listener); }
|
|
69
93
|
setControllerAttached(attached) { this.session.setControllerAttached(attached); }
|
|
@@ -2,11 +2,13 @@ import { type Socket } from 'node:net';
|
|
|
2
2
|
import type { ControlFailureKind, SessionHandle } from './types.js';
|
|
3
3
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
4
4
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
5
|
+
import type { SpawnOpts } from '../spawn.js';
|
|
6
|
+
import type { ManagedFleetSpawnResult } from '../fleet-proxy.js';
|
|
5
7
|
export interface ControlRequest {
|
|
6
|
-
version: 1 | 2;
|
|
8
|
+
version: 1 | 2 | 3;
|
|
7
9
|
id: string;
|
|
8
10
|
token: string;
|
|
9
|
-
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config';
|
|
11
|
+
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn';
|
|
10
12
|
text?: string;
|
|
11
13
|
permissionId?: string;
|
|
12
14
|
optionId?: string;
|
|
@@ -15,6 +17,19 @@ export interface ControlRequest {
|
|
|
15
17
|
controller?: boolean;
|
|
16
18
|
ownerChannel?: OwnerChannelManagementRequest;
|
|
17
19
|
loop?: string;
|
|
20
|
+
/** Conversation cursor: replay strictly after this durable seq. */
|
|
21
|
+
after?: string;
|
|
22
|
+
limit?: number;
|
|
23
|
+
/** Idempotency key for v3 mutations. */
|
|
24
|
+
commandId?: string;
|
|
25
|
+
/** Browser-session digest recorded as the acting principal. */
|
|
26
|
+
actor?: string;
|
|
27
|
+
/** Server-stamped provenance. User-controlled text cannot set this field. */
|
|
28
|
+
source?: 'owner_admin_console';
|
|
29
|
+
/** Runner generation shown with a permission card. */
|
|
30
|
+
sessionGeneration?: string;
|
|
31
|
+
/** Typed managed-spawn request accepted only over the authenticated role control plane. */
|
|
32
|
+
spawn?: SpawnOpts;
|
|
18
33
|
}
|
|
19
34
|
export interface ControlResponse {
|
|
20
35
|
version: 1;
|
|
@@ -75,9 +90,12 @@ export declare class RoleControlServer {
|
|
|
75
90
|
private readonly socketPath;
|
|
76
91
|
private readonly token;
|
|
77
92
|
private readonly sockets;
|
|
93
|
+
/** Interrupt idempotency: same command id returns the first receipt. */
|
|
94
|
+
private readonly interruptCommands;
|
|
78
95
|
private ownerChannel?;
|
|
79
96
|
private loopManager?;
|
|
80
97
|
private reloadConfig?;
|
|
98
|
+
private fleetSpawner?;
|
|
81
99
|
constructor(stateDir: string, session: SessionHandle, log: (line: string) => void);
|
|
82
100
|
start(): Promise<void>;
|
|
83
101
|
close(): Promise<void>;
|
|
@@ -85,8 +103,11 @@ export declare class RoleControlServer {
|
|
|
85
103
|
setOwnerChannel(ownerChannel: OwnerChannelHandle | undefined): void;
|
|
86
104
|
setLoopManager(loopManager: ScheduledLoopManagerHandle | undefined): void;
|
|
87
105
|
setConfigReloader(reloadConfig: (() => Promise<unknown>) | undefined): void;
|
|
106
|
+
setFleetSpawner(fleetSpawner: ((options: SpawnOpts) => Promise<ManagedFleetSpawnResult>) | undefined): void;
|
|
88
107
|
private accept;
|
|
89
108
|
private handle;
|
|
109
|
+
/** Version and capability gate shared by every conversation v3 command. */
|
|
110
|
+
private requireConversation;
|
|
90
111
|
private write;
|
|
91
112
|
}
|
|
92
113
|
/**
|
|
@@ -100,3 +121,13 @@ export declare function followControl(stateDir: string, onMessage: (message: Rec
|
|
|
100
121
|
socket: Socket;
|
|
101
122
|
send(request: Omit<ControlRequest, 'version' | 'id' | 'token'>): void;
|
|
102
123
|
}>;
|
|
124
|
+
/**
|
|
125
|
+
* Open a live conversation follow on the role's private control socket. The
|
|
126
|
+
* first message is the initial page + snapshot; every subsequent message is
|
|
127
|
+
* `{ version, conversationEvent }`. Closing the socket detaches the
|
|
128
|
+
* controller-presence this connection contributed.
|
|
129
|
+
*/
|
|
130
|
+
export declare function followConversation(stateDir: string, after: string | undefined, onMessage: (message: Record<string, unknown>) => void): Promise<{
|
|
131
|
+
socket: Socket;
|
|
132
|
+
close(): void;
|
|
133
|
+
}>;
|
package/dist/session/control.js
CHANGED
|
@@ -4,6 +4,11 @@ import { createConnection, createServer } from 'node:net';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { SessionControlError } from './types.js';
|
|
6
6
|
const MAX_LINE_BYTES = 64 * 1024;
|
|
7
|
+
/** Commands that require protocol version 3. */
|
|
8
|
+
const V3_COMMANDS = new Set([
|
|
9
|
+
'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'interrupt_v2',
|
|
10
|
+
'respond_permission_v2',
|
|
11
|
+
]);
|
|
7
12
|
/**
|
|
8
13
|
* One line saying what a control failure does — and does not — prove about the
|
|
9
14
|
* agent. Only `offline` is evidence that it is gone; every other kind used to
|
|
@@ -95,9 +100,12 @@ export class RoleControlServer {
|
|
|
95
100
|
socketPath;
|
|
96
101
|
token;
|
|
97
102
|
sockets = new Set();
|
|
103
|
+
/** Interrupt idempotency: same command id returns the first receipt. */
|
|
104
|
+
interruptCommands = new Map();
|
|
98
105
|
ownerChannel;
|
|
99
106
|
loopManager;
|
|
100
107
|
reloadConfig;
|
|
108
|
+
fleetSpawner;
|
|
101
109
|
constructor(stateDir, session, log) {
|
|
102
110
|
this.session = session;
|
|
103
111
|
this.log = log;
|
|
@@ -140,6 +148,9 @@ export class RoleControlServer {
|
|
|
140
148
|
setConfigReloader(reloadConfig) {
|
|
141
149
|
this.reloadConfig = reloadConfig;
|
|
142
150
|
}
|
|
151
|
+
setFleetSpawner(fleetSpawner) {
|
|
152
|
+
this.fleetSpawner = fleetSpawner;
|
|
153
|
+
}
|
|
143
154
|
accept(socket) {
|
|
144
155
|
this.sockets.add(socket);
|
|
145
156
|
socket.setEncoding('utf8');
|
|
@@ -192,7 +203,7 @@ export class RoleControlServer {
|
|
|
192
203
|
this.write(socket, { version: 1, id: '?', ok: false, error: 'invalid JSON' });
|
|
193
204
|
return;
|
|
194
205
|
}
|
|
195
|
-
if (
|
|
206
|
+
if (![1, 2, 3].includes(request.version) || typeof request.id !== 'string'
|
|
196
207
|
|| !sameToken(this.token, request.token ?? '')) {
|
|
197
208
|
this.write(socket, { version: 1, id: request.id ?? '?', ok: false, error: 'unauthorized' });
|
|
198
209
|
return;
|
|
@@ -204,8 +215,12 @@ export class RoleControlServer {
|
|
|
204
215
|
this.write(socket, {
|
|
205
216
|
version: 1, id: request.id, ok: true,
|
|
206
217
|
result: {
|
|
207
|
-
...this.session.snapshot(),
|
|
208
|
-
|
|
218
|
+
...this.session.snapshot(),
|
|
219
|
+
protocolVersion: this.session.conversationPage ? 3 : 2,
|
|
220
|
+
features: [
|
|
221
|
+
'events_since', 'observer_follow', 'retained_range',
|
|
222
|
+
...(this.session.conversationPage ? ['conversation_v3'] : []),
|
|
223
|
+
],
|
|
209
224
|
},
|
|
210
225
|
});
|
|
211
226
|
return;
|
|
@@ -271,6 +286,15 @@ export class RoleControlServer {
|
|
|
271
286
|
});
|
|
272
287
|
return;
|
|
273
288
|
}
|
|
289
|
+
case 'fleet_spawn': {
|
|
290
|
+
if (request.version !== 2 || !request.spawn || typeof request.spawn.name !== 'string')
|
|
291
|
+
throw new SessionControlError('rejected', 'version 2 and typed spawn options are required');
|
|
292
|
+
if (!this.fleetSpawner)
|
|
293
|
+
throw new SessionControlError('rejected', 'managed fleet spawning is unavailable for this role');
|
|
294
|
+
const result = await this.fleetSpawner(request.spawn);
|
|
295
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
274
298
|
case 'owner_channel_manage': {
|
|
275
299
|
if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
|
|
276
300
|
throw new SessionControlError('rejected', 'owner-channel management action is required');
|
|
@@ -314,6 +338,90 @@ export class RoleControlServer {
|
|
|
314
338
|
socket.write(JSON.stringify({ version: 1, event }) + '\n');
|
|
315
339
|
}) };
|
|
316
340
|
}
|
|
341
|
+
case 'conversation_page': {
|
|
342
|
+
this.requireConversation(request);
|
|
343
|
+
this.write(socket, {
|
|
344
|
+
version: 1, id: request.id, ok: true,
|
|
345
|
+
result: this.session.conversationPage({
|
|
346
|
+
after: request.after, limit: request.limit,
|
|
347
|
+
}),
|
|
348
|
+
});
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
case 'conversation_follow': {
|
|
352
|
+
this.requireConversation(request);
|
|
353
|
+
this.write(socket, {
|
|
354
|
+
version: 1, id: request.id, ok: true,
|
|
355
|
+
result: this.session.conversationPage({
|
|
356
|
+
after: request.after, limit: request.limit,
|
|
357
|
+
}),
|
|
358
|
+
});
|
|
359
|
+
// A live conversation view is an attached controller: permission
|
|
360
|
+
// requests must reach it instead of the unattended policy.
|
|
361
|
+
const controller = request.controller !== false;
|
|
362
|
+
if (controller)
|
|
363
|
+
this.session.setControllerAttached(true);
|
|
364
|
+
return { controller, stop: this.session.subscribeConversation(event => {
|
|
365
|
+
if (!socket.destroyed)
|
|
366
|
+
socket.write(JSON.stringify({ version: 1, conversationEvent: event }) + '\n');
|
|
367
|
+
}) };
|
|
368
|
+
}
|
|
369
|
+
case 'submit_prompt_v2': {
|
|
370
|
+
this.requireConversation(request);
|
|
371
|
+
if (!request.commandId?.trim() || !request.text?.trim() || !request.actor?.trim()
|
|
372
|
+
|| request.source !== 'owner_admin_console')
|
|
373
|
+
throw new SessionControlError('rejected', 'commandId, text, actor and authenticated source are required');
|
|
374
|
+
if (!this.session.submitPromptBrowser)
|
|
375
|
+
throw new SessionControlError('rejected', 'browser prompt admission is unavailable for this role');
|
|
376
|
+
try {
|
|
377
|
+
const receipt = await this.session.submitPromptBrowser({
|
|
378
|
+
commandId: request.commandId, text: request.text,
|
|
379
|
+
source: 'owner_admin_console', actorBrowserSession: request.actor,
|
|
380
|
+
});
|
|
381
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result: receipt });
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (error.name === 'IdempotencyConflictError')
|
|
385
|
+
throw new SessionControlError('rejected', 'idempotency_conflict: this command id was used with a different prompt body');
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
case 'interrupt_v2': {
|
|
391
|
+
this.requireConversation(request);
|
|
392
|
+
if (!request.commandId?.trim())
|
|
393
|
+
throw new SessionControlError('rejected', 'commandId is required');
|
|
394
|
+
const existing = this.interruptCommands.get(request.commandId);
|
|
395
|
+
if (existing) {
|
|
396
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result: existing });
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await this.session.interrupt('local-console');
|
|
400
|
+
const receipt = { accepted: true, commandId: request.commandId, at: new Date().toISOString() };
|
|
401
|
+
this.interruptCommands.set(request.commandId, receipt);
|
|
402
|
+
if (this.interruptCommands.size > 200) {
|
|
403
|
+
const oldest = this.interruptCommands.keys().next().value;
|
|
404
|
+
this.interruptCommands.delete(oldest);
|
|
405
|
+
}
|
|
406
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result: receipt });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
case 'respond_permission_v2': {
|
|
410
|
+
this.requireConversation(request);
|
|
411
|
+
if (!request.commandId?.trim() || !request.permissionId?.trim()
|
|
412
|
+
|| !request.optionId?.trim() || !request.sessionGeneration?.trim())
|
|
413
|
+
throw new SessionControlError('rejected', 'commandId, permissionId, optionId and sessionGeneration are required');
|
|
414
|
+
if (!this.session.respondPermissionV2)
|
|
415
|
+
throw new SessionControlError('rejected', 'generation-bound permission responses are unavailable for this role');
|
|
416
|
+
const result = this.session.respondPermissionV2(request.permissionId, request.optionId, request.sessionGeneration);
|
|
417
|
+
if (result === 'stale')
|
|
418
|
+
throw new SessionControlError('rejected', 'stale_state: permission is settled, expired, invalid, or belongs to another session generation');
|
|
419
|
+
this.write(socket, {
|
|
420
|
+
version: 1, id: request.id, ok: true,
|
|
421
|
+
result: { accepted: true, commandId: request.commandId },
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
317
425
|
}
|
|
318
426
|
}
|
|
319
427
|
catch (error) {
|
|
@@ -328,6 +436,13 @@ export class RoleControlServer {
|
|
|
328
436
|
});
|
|
329
437
|
}
|
|
330
438
|
}
|
|
439
|
+
/** Version and capability gate shared by every conversation v3 command. */
|
|
440
|
+
requireConversation(request) {
|
|
441
|
+
if (request.version !== 3)
|
|
442
|
+
throw new SessionControlError('rejected', 'protocol version 3 is required for conversation commands');
|
|
443
|
+
if (!this.session.conversationPage || !this.session.subscribeConversation)
|
|
444
|
+
throw new SessionControlError('rejected', 'this role has no conversation ledger');
|
|
445
|
+
}
|
|
331
446
|
write(socket, response) {
|
|
332
447
|
if (!socket.destroyed)
|
|
333
448
|
socket.write(JSON.stringify(response) + '\n');
|
|
@@ -376,7 +491,7 @@ export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
|
|
|
376
491
|
socket.end();
|
|
377
492
|
});
|
|
378
493
|
socket.once('connect', () => socket.write(JSON.stringify({
|
|
379
|
-
version: 2, id, token, ...request,
|
|
494
|
+
version: V3_COMMANDS.has(request.command) ? 3 : 2, id, token, ...request,
|
|
380
495
|
}) + '\n'));
|
|
381
496
|
});
|
|
382
497
|
}
|
|
@@ -401,7 +516,45 @@ export async function followControl(stateDir, onMessage) {
|
|
|
401
516
|
socket.once('connect', resolve);
|
|
402
517
|
socket.once('error', reject);
|
|
403
518
|
});
|
|
404
|
-
const send = (request) => socket.write(JSON.stringify({
|
|
519
|
+
const send = (request) => socket.write(JSON.stringify({
|
|
520
|
+
version: V3_COMMANDS.has(request.command) ? 3 : 1, id: randomUUID(), token, ...request,
|
|
521
|
+
}) + '\n');
|
|
405
522
|
send({ command: 'follow', since: 0 });
|
|
406
523
|
return { socket, send };
|
|
407
524
|
}
|
|
525
|
+
/**
|
|
526
|
+
* Open a live conversation follow on the role's private control socket. The
|
|
527
|
+
* first message is the initial page + snapshot; every subsequent message is
|
|
528
|
+
* `{ version, conversationEvent }`. Closing the socket detaches the
|
|
529
|
+
* controller-presence this connection contributed.
|
|
530
|
+
*/
|
|
531
|
+
export async function followConversation(stateDir, after, onMessage) {
|
|
532
|
+
const token = readFileSync(controlTokenPath(stateDir), 'utf8').trim();
|
|
533
|
+
const socket = createConnection(controlSocketPath(stateDir));
|
|
534
|
+
socket.setEncoding('utf8');
|
|
535
|
+
let buffer = '';
|
|
536
|
+
socket.on('data', chunk => {
|
|
537
|
+
buffer += chunk;
|
|
538
|
+
for (;;) {
|
|
539
|
+
const newline = buffer.indexOf('\n');
|
|
540
|
+
if (newline < 0)
|
|
541
|
+
break;
|
|
542
|
+
const line = buffer.slice(0, newline);
|
|
543
|
+
buffer = buffer.slice(newline + 1);
|
|
544
|
+
if (!line.trim())
|
|
545
|
+
continue;
|
|
546
|
+
try {
|
|
547
|
+
onMessage(JSON.parse(line));
|
|
548
|
+
}
|
|
549
|
+
catch { /* a torn frame ends this stream; the client resyncs over HTTP */ }
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
await new Promise((resolve, reject) => {
|
|
553
|
+
socket.once('connect', resolve);
|
|
554
|
+
socket.once('error', reject);
|
|
555
|
+
});
|
|
556
|
+
socket.write(JSON.stringify({
|
|
557
|
+
version: 3, id: randomUUID(), token, command: 'conversation_follow', after,
|
|
558
|
+
}) + '\n');
|
|
559
|
+
return { socket, close: () => socket.destroy() };
|
|
560
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { AdapterMeta, ConversationEventKind, ConversationPayload } from './conversation-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reduce one ACP v1 `session/update` into one conversation-domain event draft.
|
|
5
|
+
*
|
|
6
|
+
* This is the protocol seam: v1 create/update pairs (and a future v2 upsert
|
|
7
|
+
* stream) both land in the same domain shapes. The function is pure and total —
|
|
8
|
+
* it never throws, never passes raw wire objects through, caps every payload,
|
|
9
|
+
* and quarantines namespaced `_meta` so unknown extensions cannot leak into
|
|
10
|
+
* generic rendering paths.
|
|
11
|
+
*/
|
|
12
|
+
/** Cap for any single normalized text payload (spec §5.3). */
|
|
13
|
+
export declare const MAX_TEXT_BYTES: number;
|
|
14
|
+
/** Cap for one adapter `_meta` namespace value. */
|
|
15
|
+
export declare const MAX_META_BYTES: number;
|
|
16
|
+
/** Cap for serialized raw tool input/output retained as structured JSON. */
|
|
17
|
+
export declare const MAX_RAW_JSON_BYTES: number;
|
|
18
|
+
/** Cap for the sanitized preview of an unsupported update. */
|
|
19
|
+
export declare const MAX_UNSUPPORTED_PREVIEW_CHARS = 2048;
|
|
20
|
+
export interface NormalizeOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Replace every text payload with this placeholder, keeping byte count and
|
|
23
|
+
* digest. Used for scheduled-loop turns whose output must not be retained.
|
|
24
|
+
*/
|
|
25
|
+
redactText?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface NormalizedUpdate {
|
|
28
|
+
kind: ConversationEventKind;
|
|
29
|
+
payload: ConversationPayload;
|
|
30
|
+
messageId?: string;
|
|
31
|
+
toolCallId?: string;
|
|
32
|
+
adapterMeta?: AdapterMeta[];
|
|
33
|
+
}
|
|
34
|
+
export declare function normalizeSessionUpdate(update: acp.SessionUpdate, options?: NormalizeOptions): NormalizedUpdate;
|