@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
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync, writeSync, } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Durable, append-only, single-writer conversation ledger for one role
|
|
6
|
+
* (spec §5.3). The per-role runner is the only writer; readers page by seq.
|
|
7
|
+
*
|
|
8
|
+
* Unlike `SessionEvents` (a bounded diagnostic projection that may drop
|
|
9
|
+
* writes silently), this store is the transcript of record: an acknowledged
|
|
10
|
+
* browser prompt exists here before the browser hears "accepted", so
|
|
11
|
+
* `append` THROWS on failure. Agent-stream normalization uses `appendSafe`,
|
|
12
|
+
* which degrades visibly instead of killing role work already in progress.
|
|
13
|
+
*/
|
|
14
|
+
const MANIFEST = 'manifest.json';
|
|
15
|
+
const DEFAULT_SEGMENT_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
/** In-memory tail kept for fast paging and follow backfill. */
|
|
17
|
+
const TAIL_EVENTS = 1_000;
|
|
18
|
+
export class IdempotencyConflictError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super('idempotency key reused with a different body');
|
|
21
|
+
this.name = 'IdempotencyConflictError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ConversationEventStore {
|
|
25
|
+
dir;
|
|
26
|
+
nextSeq = 1;
|
|
27
|
+
segments = [];
|
|
28
|
+
tail = [];
|
|
29
|
+
listeners = new Set();
|
|
30
|
+
commands = new Map();
|
|
31
|
+
promptStates = new Map();
|
|
32
|
+
activeFd;
|
|
33
|
+
activeBytes = 0;
|
|
34
|
+
_degraded = false;
|
|
35
|
+
degradedReason;
|
|
36
|
+
segmentBytes;
|
|
37
|
+
roleId;
|
|
38
|
+
log;
|
|
39
|
+
constructor(dir, options) {
|
|
40
|
+
this.dir = dir;
|
|
41
|
+
this.roleId = options.roleId;
|
|
42
|
+
this.segmentBytes = options.segmentBytes ?? DEFAULT_SEGMENT_BYTES;
|
|
43
|
+
this.log = options.log ?? (() => { });
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
46
|
+
this.recover();
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.markDegraded(`store unavailable: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** First 24 hex chars of sha-256; the idempotency body-digest convention. */
|
|
53
|
+
static bodyDigest(body) {
|
|
54
|
+
return createHash('sha256').update(body).digest('hex').slice(0, 24);
|
|
55
|
+
}
|
|
56
|
+
get degraded() { return this._degraded; }
|
|
57
|
+
get degradedDetail() { return this.degradedReason; }
|
|
58
|
+
/**
|
|
59
|
+
* Durably append one event. Throws when the record cannot be persisted —
|
|
60
|
+
* the caller must fail its command rather than acknowledge a lost prompt.
|
|
61
|
+
*/
|
|
62
|
+
append(draft) {
|
|
63
|
+
const event = {
|
|
64
|
+
schemaVersion: 1,
|
|
65
|
+
roleId: this.roleId,
|
|
66
|
+
eventId: `e${this.nextSeq}`,
|
|
67
|
+
seq: this.nextSeq,
|
|
68
|
+
at: new Date().toISOString(),
|
|
69
|
+
...draft,
|
|
70
|
+
};
|
|
71
|
+
const line = JSON.stringify(event) + '\n';
|
|
72
|
+
const fd = this.segmentFd(Buffer.byteLength(line));
|
|
73
|
+
writeSync(fd, line);
|
|
74
|
+
fsyncSync(fd);
|
|
75
|
+
this.nextSeq++;
|
|
76
|
+
this.activeBytes += Buffer.byteLength(line);
|
|
77
|
+
this.tail.push(event);
|
|
78
|
+
if (this.tail.length > TAIL_EVENTS)
|
|
79
|
+
this.tail.shift();
|
|
80
|
+
this.trackPromptState(event);
|
|
81
|
+
for (const listener of this.listeners)
|
|
82
|
+
listener(event);
|
|
83
|
+
return event;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Append an agent-stream event; on failure record degradation and keep the
|
|
87
|
+
* role alive. Conversation durability may degrade, active work must not die.
|
|
88
|
+
*/
|
|
89
|
+
appendSafe(draft) {
|
|
90
|
+
try {
|
|
91
|
+
return this.append(draft);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.markDegraded(`event append failed: ${error.message}`);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
page(request = {}) {
|
|
99
|
+
const after = cursorSeq(request.after);
|
|
100
|
+
const limit = Math.min(Math.max(request.limit ?? 200, 1), 1_000);
|
|
101
|
+
const events = this.eventsAfter(after, limit + 1);
|
|
102
|
+
const hasMore = events.length > limit;
|
|
103
|
+
const pageEvents = hasMore ? events.slice(0, limit) : events;
|
|
104
|
+
const firstStored = this.firstStoredSeq();
|
|
105
|
+
return {
|
|
106
|
+
events: pageEvents,
|
|
107
|
+
...(firstStored !== undefined ? { firstAvailableCursor: String(firstStored) } : {}),
|
|
108
|
+
...(pageEvents.length ? { nextCursor: String(pageEvents.at(-1).seq) } : {}),
|
|
109
|
+
hasMore,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
subscribe(listener) {
|
|
113
|
+
this.listeners.add(listener);
|
|
114
|
+
return () => this.listeners.delete(listener);
|
|
115
|
+
}
|
|
116
|
+
/** Store the receipt a repeated command must get back. */
|
|
117
|
+
recordReceipt(commandId, receipt, bodyDigest) {
|
|
118
|
+
this.commands.set(commandId, { receipt, bodyDigest });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The receipt for a previously accepted command, or undefined for a new one.
|
|
122
|
+
* A reused ID with a different body digest is a conflict, never a replay.
|
|
123
|
+
*/
|
|
124
|
+
receiptFor(commandId, bodyDigest) {
|
|
125
|
+
const record = this.commands.get(commandId);
|
|
126
|
+
if (!record)
|
|
127
|
+
return undefined;
|
|
128
|
+
if (record.bodyDigest !== bodyDigest)
|
|
129
|
+
throw new IdempotencyConflictError();
|
|
130
|
+
return record.receipt;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Prompts with no terminal event, classified for restart recovery:
|
|
134
|
+
* `admitted` never started and is safe to restore into the FIFO;
|
|
135
|
+
* `started` may already have had side effects and must not be replayed.
|
|
136
|
+
*/
|
|
137
|
+
openPrompts() {
|
|
138
|
+
return [...this.promptStates.values()];
|
|
139
|
+
}
|
|
140
|
+
lastCursor() {
|
|
141
|
+
return this.nextSeq > 1 ? String(this.nextSeq - 1) : undefined;
|
|
142
|
+
}
|
|
143
|
+
close() {
|
|
144
|
+
if (this.activeFd !== undefined) {
|
|
145
|
+
try {
|
|
146
|
+
closeSync(this.activeFd);
|
|
147
|
+
}
|
|
148
|
+
catch { /* already closed */ }
|
|
149
|
+
this.activeFd = undefined;
|
|
150
|
+
}
|
|
151
|
+
this.listeners.clear();
|
|
152
|
+
}
|
|
153
|
+
// ── recovery ───────────────────────────────────────────────────────────────
|
|
154
|
+
recover() {
|
|
155
|
+
const manifest = this.readManifest();
|
|
156
|
+
this.segments = manifest?.segments.filter(name => existsSync(join(this.dir, name))) ?? this.discoverSegments();
|
|
157
|
+
if (!this.segments.length)
|
|
158
|
+
this.segments = this.discoverSegments();
|
|
159
|
+
let maxSeq = 0;
|
|
160
|
+
for (const segment of this.segments) {
|
|
161
|
+
for (const event of this.readSegment(segment)) {
|
|
162
|
+
maxSeq = Math.max(maxSeq, event.seq);
|
|
163
|
+
this.tail.push(event);
|
|
164
|
+
if (this.tail.length > TAIL_EVENTS)
|
|
165
|
+
this.tail.shift();
|
|
166
|
+
this.trackPromptState(event);
|
|
167
|
+
this.rebuildCommandIndex(event);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
this.nextSeq = maxSeq + 1;
|
|
171
|
+
if (this.segments.length) {
|
|
172
|
+
const active = join(this.dir, this.segments.at(-1));
|
|
173
|
+
this.activeBytes = existsSync(active) ? statSync(active).size : 0;
|
|
174
|
+
}
|
|
175
|
+
this.writeManifest();
|
|
176
|
+
}
|
|
177
|
+
readManifest() {
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(readFileSync(join(this.dir, MANIFEST), 'utf8'));
|
|
180
|
+
if (parsed.schemaVersion === 1 && Array.isArray(parsed.segments))
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
catch { /* recover from segments instead */ }
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
discoverSegments() {
|
|
187
|
+
try {
|
|
188
|
+
return readdirSync(this.dir)
|
|
189
|
+
.filter(name => /^events-\d{6}\.jsonl$/.test(name))
|
|
190
|
+
.sort();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
readSegment(name) {
|
|
197
|
+
const events = [];
|
|
198
|
+
let raw;
|
|
199
|
+
try {
|
|
200
|
+
raw = readFileSync(join(this.dir, name), 'utf8');
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
this.markDegraded(`segment ${name} unreadable: ${error.message}`);
|
|
204
|
+
return events;
|
|
205
|
+
}
|
|
206
|
+
for (const line of raw.split('\n')) {
|
|
207
|
+
if (!line.trim())
|
|
208
|
+
continue;
|
|
209
|
+
try {
|
|
210
|
+
const event = JSON.parse(line);
|
|
211
|
+
if (event.schemaVersion === 1 && typeof event.seq === 'number')
|
|
212
|
+
events.push(event);
|
|
213
|
+
else
|
|
214
|
+
this.markDegraded(`segment ${name} holds a record of an unknown schema`);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// A torn final line after a crash: everything before it stays readable.
|
|
218
|
+
this.markDegraded(`segment ${name} ends in a torn or corrupt line`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return events;
|
|
222
|
+
}
|
|
223
|
+
rebuildCommandIndex(event) {
|
|
224
|
+
if (event.kind !== 'prompt.admitted' || !event.commandId || !event.promptId)
|
|
225
|
+
return;
|
|
226
|
+
const payload = event.payload;
|
|
227
|
+
const bodyDigest = payload.text?.text !== undefined
|
|
228
|
+
? ConversationEventStore.bodyDigest(payload.text.text)
|
|
229
|
+
: payload.external?.digest ?? '';
|
|
230
|
+
this.commands.set(event.commandId, {
|
|
231
|
+
bodyDigest,
|
|
232
|
+
receipt: {
|
|
233
|
+
commandId: event.commandId,
|
|
234
|
+
promptId: event.promptId,
|
|
235
|
+
state: 'queued',
|
|
236
|
+
queuedBehind: payload.queuedBehind ?? 0,
|
|
237
|
+
acceptedAt: event.at,
|
|
238
|
+
eventCursor: String(event.seq),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
trackPromptState(event) {
|
|
243
|
+
if (!event.promptId)
|
|
244
|
+
return;
|
|
245
|
+
switch (event.kind) {
|
|
246
|
+
case 'prompt.admitted': {
|
|
247
|
+
const payload = event.payload;
|
|
248
|
+
this.promptStates.set(event.promptId, {
|
|
249
|
+
promptId: event.promptId,
|
|
250
|
+
state: 'admitted',
|
|
251
|
+
sessionGeneration: event.sessionGeneration,
|
|
252
|
+
...(payload.text?.text !== undefined ? { text: payload.text.text } : {}),
|
|
253
|
+
...(event.commandId ? { commandId: event.commandId } : {}),
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
case 'prompt.started': {
|
|
258
|
+
const open = this.promptStates.get(event.promptId);
|
|
259
|
+
if (open)
|
|
260
|
+
open.state = 'started';
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
case 'turn.completed':
|
|
264
|
+
this.promptStates.delete(event.promptId);
|
|
265
|
+
return;
|
|
266
|
+
default:
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// ── segment management ─────────────────────────────────────────────────────
|
|
270
|
+
segmentFd(incomingBytes) {
|
|
271
|
+
if (this.activeFd !== undefined
|
|
272
|
+
&& this.activeBytes + incomingBytes > this.segmentBytes) {
|
|
273
|
+
try {
|
|
274
|
+
closeSync(this.activeFd);
|
|
275
|
+
}
|
|
276
|
+
catch { /* rotating anyway */ }
|
|
277
|
+
this.activeFd = undefined;
|
|
278
|
+
}
|
|
279
|
+
if (this.activeFd === undefined) {
|
|
280
|
+
const needsNew = !this.segments.length
|
|
281
|
+
|| this.activeBytes + incomingBytes > this.segmentBytes;
|
|
282
|
+
if (needsNew) {
|
|
283
|
+
const name = `events-${String(this.segments.length + 1).padStart(6, '0')}.jsonl`;
|
|
284
|
+
this.segments.push(name);
|
|
285
|
+
this.activeBytes = 0;
|
|
286
|
+
this.writeManifest();
|
|
287
|
+
}
|
|
288
|
+
const path = join(this.dir, this.segments.at(-1));
|
|
289
|
+
this.activeFd = openSync(path, 'a', 0o600);
|
|
290
|
+
if (!this.activeBytes)
|
|
291
|
+
this.activeBytes = statSync(path).size;
|
|
292
|
+
}
|
|
293
|
+
return this.activeFd;
|
|
294
|
+
}
|
|
295
|
+
writeManifest() {
|
|
296
|
+
const manifest = {
|
|
297
|
+
schemaVersion: 1, nextSeq: this.nextSeq, segments: this.segments,
|
|
298
|
+
};
|
|
299
|
+
const tmp = join(this.dir, MANIFEST + '.tmp');
|
|
300
|
+
try {
|
|
301
|
+
writeFileSync(tmp, JSON.stringify(manifest) + '\n', { mode: 0o600 });
|
|
302
|
+
renameSync(tmp, join(this.dir, MANIFEST));
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
// The manifest is a recovery accelerator, not the source of truth;
|
|
306
|
+
// segments alone can always rebuild it.
|
|
307
|
+
this.log(`conversation manifest write failed: ${error.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// ── reads ──────────────────────────────────────────────────────────────────
|
|
311
|
+
firstStoredSeq() {
|
|
312
|
+
if (this.tail.length && this.tail[0].seq === 1)
|
|
313
|
+
return 1;
|
|
314
|
+
for (const segment of this.segments) {
|
|
315
|
+
const events = this.readSegment(segment);
|
|
316
|
+
if (events.length)
|
|
317
|
+
return events[0].seq;
|
|
318
|
+
}
|
|
319
|
+
return this.tail[0]?.seq;
|
|
320
|
+
}
|
|
321
|
+
eventsAfter(after, limit) {
|
|
322
|
+
// Serve from the in-memory tail whenever the range allows it.
|
|
323
|
+
if (this.tail.length && after >= this.tail[0].seq - 1)
|
|
324
|
+
return this.tail.filter(event => event.seq > after).slice(0, limit);
|
|
325
|
+
const events = [];
|
|
326
|
+
for (const segment of this.segments) {
|
|
327
|
+
for (const event of this.readSegment(segment)) {
|
|
328
|
+
if (event.seq <= after)
|
|
329
|
+
continue;
|
|
330
|
+
events.push(event);
|
|
331
|
+
if (events.length >= limit)
|
|
332
|
+
return events;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return events;
|
|
336
|
+
}
|
|
337
|
+
markDegraded(reason) {
|
|
338
|
+
if (!this._degraded)
|
|
339
|
+
this.log(`conversation store degraded: ${reason}`);
|
|
340
|
+
this._degraded = true;
|
|
341
|
+
this.degradedReason = reason;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function cursorSeq(cursor) {
|
|
345
|
+
const value = Number(cursor);
|
|
346
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
347
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Durable conversation domain schema (ACP web console, spec §5.2).
|
|
4
|
+
*
|
|
5
|
+
* `SessionEvent` in ./types.js remains the compact diagnostic projection; the
|
|
6
|
+
* types here describe the durable per-role conversation ledger. Nothing in this
|
|
7
|
+
* file touches the wire: ACP updates are reduced into these shapes by the
|
|
8
|
+
* normalizer, and the store (phase 1) assigns `seq`/`eventId`/timestamps.
|
|
9
|
+
*/
|
|
10
|
+
export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
|
|
11
|
+
/** A well-formed ACP update this version cannot represent. Bounded, never a crash. */
|
|
12
|
+
| 'unsupported';
|
|
13
|
+
/** Where a conversation record came from. Typed provenance, never prompt text. */
|
|
14
|
+
export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay';
|
|
15
|
+
export interface NormalizedText {
|
|
16
|
+
type: 'text';
|
|
17
|
+
text: string;
|
|
18
|
+
/** Byte length of the ORIGINAL text, before any cap or redaction. */
|
|
19
|
+
bytes: number;
|
|
20
|
+
truncated?: true;
|
|
21
|
+
/** Present when truncated or redacted: sha-256 (hex, first 24) of the original. */
|
|
22
|
+
digest?: string;
|
|
23
|
+
redacted?: true;
|
|
24
|
+
}
|
|
25
|
+
/** Media payloads are described, not carried; rendering is a later phase. */
|
|
26
|
+
export interface NormalizedMedia {
|
|
27
|
+
type: 'image' | 'audio';
|
|
28
|
+
mimeType: string;
|
|
29
|
+
bytes: number;
|
|
30
|
+
uri?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface NormalizedResourceLink {
|
|
33
|
+
type: 'resource_link';
|
|
34
|
+
uri: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
mimeType?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface NormalizedResource {
|
|
39
|
+
type: 'resource';
|
|
40
|
+
uri?: string;
|
|
41
|
+
mimeType?: string;
|
|
42
|
+
bytes: number;
|
|
43
|
+
}
|
|
44
|
+
export type NormalizedContentBlock = NormalizedText | NormalizedMedia | NormalizedResourceLink | NormalizedResource;
|
|
45
|
+
/** A capped text fragment inside a larger payload (diff sides, previews). */
|
|
46
|
+
export interface CappedText {
|
|
47
|
+
text: string;
|
|
48
|
+
bytes: number;
|
|
49
|
+
truncated?: true;
|
|
50
|
+
digest?: string;
|
|
51
|
+
}
|
|
52
|
+
export type NormalizedToolContent = {
|
|
53
|
+
type: 'content';
|
|
54
|
+
content: NormalizedContentBlock;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'diff';
|
|
57
|
+
path: string;
|
|
58
|
+
newText: CappedText;
|
|
59
|
+
oldText?: CappedText;
|
|
60
|
+
}
|
|
61
|
+
/** A tool-owned display terminal reference — never a PTY attachment. */
|
|
62
|
+
| {
|
|
63
|
+
type: 'terminal';
|
|
64
|
+
terminalId: string;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Adapter-specific `_meta`, quarantined by namespace. The UI may ignore any
|
|
68
|
+
* entry; nothing outside adapter-specific renderers may interpret `value`.
|
|
69
|
+
*/
|
|
70
|
+
export interface AdapterMeta {
|
|
71
|
+
namespace: string;
|
|
72
|
+
value?: unknown;
|
|
73
|
+
/** Set when the value exceeded the metadata cap and was dropped. */
|
|
74
|
+
truncated?: true;
|
|
75
|
+
bytes?: number;
|
|
76
|
+
}
|
|
77
|
+
export interface MessageChunkPayload {
|
|
78
|
+
role: 'user' | 'assistant';
|
|
79
|
+
content: NormalizedContentBlock;
|
|
80
|
+
}
|
|
81
|
+
export interface ThoughtChunkPayload {
|
|
82
|
+
content: NormalizedContentBlock;
|
|
83
|
+
}
|
|
84
|
+
export interface PlanEntryPayload {
|
|
85
|
+
content: CappedText;
|
|
86
|
+
priority: 'high' | 'medium' | 'low';
|
|
87
|
+
status: 'pending' | 'in_progress' | 'completed';
|
|
88
|
+
}
|
|
89
|
+
export interface PlanReplacePayload {
|
|
90
|
+
/** Absent for the standard whole-session plan; set for ID'd (unstable) plans. */
|
|
91
|
+
planId?: string;
|
|
92
|
+
entries?: PlanEntryPayload[];
|
|
93
|
+
/** Unstable plan representations we can reference but not structure. */
|
|
94
|
+
file?: {
|
|
95
|
+
uri: string;
|
|
96
|
+
};
|
|
97
|
+
markdown?: CappedText;
|
|
98
|
+
removed?: true;
|
|
99
|
+
}
|
|
100
|
+
export interface ToolUpsertPayload {
|
|
101
|
+
toolCallId: string;
|
|
102
|
+
/** True for `tool_call` (full snapshot), false for `tool_call_update` (patch). */
|
|
103
|
+
snapshot: boolean;
|
|
104
|
+
title?: string;
|
|
105
|
+
kind?: string;
|
|
106
|
+
status?: string;
|
|
107
|
+
content?: NormalizedToolContent[];
|
|
108
|
+
locations?: Array<{
|
|
109
|
+
path: string;
|
|
110
|
+
line?: number;
|
|
111
|
+
}>;
|
|
112
|
+
rawInput?: BoundedJson;
|
|
113
|
+
rawOutput?: BoundedJson;
|
|
114
|
+
}
|
|
115
|
+
export interface UsageUpdatedPayload {
|
|
116
|
+
used: number;
|
|
117
|
+
size: number;
|
|
118
|
+
/** Agent-reported; absence is normal and cost semantics differ per adapter. */
|
|
119
|
+
cost?: {
|
|
120
|
+
amount: number;
|
|
121
|
+
currency: string;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export interface SessionStatePayload {
|
|
125
|
+
currentModeId?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface SessionInfoPayload {
|
|
128
|
+
title?: string | null;
|
|
129
|
+
updatedAt?: string | null;
|
|
130
|
+
}
|
|
131
|
+
export interface CapabilitiesUpdatedPayload {
|
|
132
|
+
commands?: Array<{
|
|
133
|
+
name: string;
|
|
134
|
+
description: CappedText;
|
|
135
|
+
inputHint?: string;
|
|
136
|
+
}>;
|
|
137
|
+
configOptions?: BoundedJson;
|
|
138
|
+
}
|
|
139
|
+
export interface UnsupportedPayload {
|
|
140
|
+
/** The wire discriminant (or 'unknown' when even that was absent). */
|
|
141
|
+
sessionUpdate: string;
|
|
142
|
+
bytes: number;
|
|
143
|
+
/** Sanitized JSON preview, capped; enough to diagnose, never to exhaust. */
|
|
144
|
+
preview?: string;
|
|
145
|
+
}
|
|
146
|
+
export interface PromptAdmittedPayload {
|
|
147
|
+
text?: NormalizedText;
|
|
148
|
+
/** Human-only display body; never used to recover or dispatch an external prompt. */
|
|
149
|
+
displayText?: NormalizedText;
|
|
150
|
+
/** External E2E bodies stay out of fleet state: digest/size only. */
|
|
151
|
+
external?: {
|
|
152
|
+
digest: string;
|
|
153
|
+
bytes: number;
|
|
154
|
+
};
|
|
155
|
+
queuedBehind: number;
|
|
156
|
+
}
|
|
157
|
+
export interface PromptStartedPayload {
|
|
158
|
+
queuedBehind?: number;
|
|
159
|
+
}
|
|
160
|
+
export interface PromptInterruptRequestedPayload {
|
|
161
|
+
commandId?: string;
|
|
162
|
+
cancellationSource?: TurnCancellationSource;
|
|
163
|
+
}
|
|
164
|
+
export interface PermissionRequestedPayload {
|
|
165
|
+
toolCallId?: string;
|
|
166
|
+
title?: string;
|
|
167
|
+
options: Array<{
|
|
168
|
+
optionId: string;
|
|
169
|
+
name: string;
|
|
170
|
+
kind: string;
|
|
171
|
+
}>;
|
|
172
|
+
expiresAt?: string;
|
|
173
|
+
}
|
|
174
|
+
export interface PermissionResolvedPayload {
|
|
175
|
+
decision: 'allowed' | 'denied' | 'cancelled' | 'expired';
|
|
176
|
+
decisionSource: 'automatic' | 'manual';
|
|
177
|
+
optionId?: string;
|
|
178
|
+
policy?: string;
|
|
179
|
+
reason?: string;
|
|
180
|
+
}
|
|
181
|
+
export interface TurnStatePayload {
|
|
182
|
+
state: 'queued' | 'running' | 'awaiting_permission' | 'interrupt_requested';
|
|
183
|
+
}
|
|
184
|
+
export interface TurnCompletedPayload {
|
|
185
|
+
outcome: TurnOutcome | 'unknown_after_restart';
|
|
186
|
+
stopReason?: string;
|
|
187
|
+
cancellationSource?: TurnCancellationSource;
|
|
188
|
+
}
|
|
189
|
+
export interface SessionLifecyclePayload {
|
|
190
|
+
status: 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed' | 'offline';
|
|
191
|
+
detail?: string;
|
|
192
|
+
}
|
|
193
|
+
export interface ErrorPayload {
|
|
194
|
+
message: string;
|
|
195
|
+
}
|
|
196
|
+
/** Structured-but-untrusted JSON, serialized and capped rather than trusted. */
|
|
197
|
+
export interface BoundedJson {
|
|
198
|
+
json?: unknown;
|
|
199
|
+
bytes: number;
|
|
200
|
+
truncated?: true;
|
|
201
|
+
digest?: string;
|
|
202
|
+
redacted?: true;
|
|
203
|
+
}
|
|
204
|
+
export type ConversationPayload = MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | SessionLifecyclePayload | ErrorPayload;
|
|
205
|
+
export interface ConversationEventV1 {
|
|
206
|
+
schemaVersion: 1;
|
|
207
|
+
roleId: string;
|
|
208
|
+
/** Opaque, durable, unique. Browser dedupe key across replay/reconnect. */
|
|
209
|
+
eventId: string;
|
|
210
|
+
/** Durable monotonic sequence for this role's store. */
|
|
211
|
+
seq: number;
|
|
212
|
+
at: string;
|
|
213
|
+
/** Changes on every runner restart; pending IDs from prior generations are stale. */
|
|
214
|
+
sessionGeneration: string;
|
|
215
|
+
acpSessionId?: string;
|
|
216
|
+
kind: ConversationEventKind;
|
|
217
|
+
/** Fleet admission ID. */
|
|
218
|
+
promptId?: string;
|
|
219
|
+
/** Fleet turn ID; v1 turns reuse the prompt ID. */
|
|
220
|
+
turnId?: string;
|
|
221
|
+
/** Agent-owned when supplied; optional in ACP v1. */
|
|
222
|
+
messageId?: string;
|
|
223
|
+
toolCallId?: string;
|
|
224
|
+
permissionId?: string;
|
|
225
|
+
/**
|
|
226
|
+
* The idempotency key of the command that produced this record. Persisted so
|
|
227
|
+
* the command-id index can be rebuilt from segments alone on recovery.
|
|
228
|
+
*/
|
|
229
|
+
commandId?: string;
|
|
230
|
+
source?: ConversationSource;
|
|
231
|
+
actor?: {
|
|
232
|
+
browserSession?: string;
|
|
233
|
+
externalSenderDigest?: string;
|
|
234
|
+
};
|
|
235
|
+
payload: ConversationPayload;
|
|
236
|
+
adapterMeta?: AdapterMeta[];
|
|
237
|
+
}
|
|
238
|
+
export interface SubmitPromptCommand {
|
|
239
|
+
/** Idempotency-Key / clientRequestId. Reuse with a different body is a conflict. */
|
|
240
|
+
commandId: string;
|
|
241
|
+
text: string;
|
|
242
|
+
source: 'owner_admin_console';
|
|
243
|
+
actorBrowserSession: string;
|
|
244
|
+
}
|
|
245
|
+
export interface PromptReceipt {
|
|
246
|
+
commandId: string;
|
|
247
|
+
promptId: string;
|
|
248
|
+
state: 'queued' | 'starting';
|
|
249
|
+
queuedBehind: number;
|
|
250
|
+
acceptedAt: string;
|
|
251
|
+
/** Cursor of the durable `prompt.admitted` event. */
|
|
252
|
+
eventCursor: string;
|
|
253
|
+
}
|
|
254
|
+
export interface PermissionDecisionCommand {
|
|
255
|
+
commandId: string;
|
|
256
|
+
permissionId: string;
|
|
257
|
+
sessionGeneration: string;
|
|
258
|
+
optionId: string;
|
|
259
|
+
}
|
|
260
|
+
export interface ConversationSnapshot {
|
|
261
|
+
sessionGeneration: string;
|
|
262
|
+
readiness: 'starting' | 'idle' | 'running' | 'awaiting_permission' | 'failed' | 'offline';
|
|
263
|
+
queueDepth: number;
|
|
264
|
+
pendingPermissionIds: string[];
|
|
265
|
+
historyDegraded?: boolean;
|
|
266
|
+
}
|
|
267
|
+
export interface ConversationPage {
|
|
268
|
+
events: ConversationEventV1[];
|
|
269
|
+
firstAvailableCursor?: string;
|
|
270
|
+
nextCursor?: string;
|
|
271
|
+
hasMore: boolean;
|
|
272
|
+
snapshot: ConversationSnapshot;
|
|
273
|
+
}
|
|
274
|
+
export type { PromptOrigin };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|