killeros 1.5.2 → 1.5.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/CHANGELOG.md +25 -0
- package/Killeros.ts +30 -4
- package/README.md +38 -14
- package/killeros/commands.ts +242 -2
- package/killeros/context-compaction.ts +566 -0
- package/killeros/goals.ts +25 -2
- package/killeros/runtime.ts +25 -0
- package/killeros/subagent-lifecycle.ts +237 -8
- package/killeros/subagent-persistence.ts +572 -0
- package/killeros/subagent-process.ts +583 -565
- package/killeros/subagent-ui.ts +17 -1
- package/killeros/subagents.ts +2579 -1581
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
declare const threadIdBrand: unique symbol;
|
|
4
4
|
|
|
5
5
|
export type SubagentThreadId = string & { readonly [threadIdBrand]: "SubagentThreadId" };
|
|
6
|
-
export type SubagentThreadState = "queued" | "active" | "done" | "failed" | "stopped" | "closed";
|
|
7
|
-
export type SubagentTerminalState = Extract<SubagentThreadState, "done" | "failed" | "stopped">;
|
|
6
|
+
export type SubagentThreadState = "queued" | "active" | "done" | "failed" | "stopped" | "orphaned" | "closed";
|
|
7
|
+
export type SubagentTerminalState = Extract<SubagentThreadState, "done" | "failed" | "stopped" | "orphaned">;
|
|
8
8
|
export type SubagentFilesystemAccess = "none" | "read" | "write";
|
|
9
9
|
export type SubagentNetworkAccess = "none" | "read" | "full";
|
|
10
10
|
export type SubagentProcessAccess = "none" | "limited" | "full";
|
|
@@ -59,13 +59,20 @@ export interface SubagentThreadTimestamps {
|
|
|
59
59
|
closedAt?: number;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface SubagentThreadSession {
|
|
63
|
+
id: string;
|
|
64
|
+
directory: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
export interface SubagentThreadSpec {
|
|
63
68
|
parentId?: SubagentThreadId;
|
|
69
|
+
displayName: string;
|
|
64
70
|
role: string;
|
|
65
71
|
prompt: string;
|
|
66
72
|
model: string;
|
|
67
73
|
tools: readonly string[];
|
|
68
74
|
capabilityBoundary: SubagentCapabilityBoundary;
|
|
75
|
+
session: SubagentThreadSession;
|
|
69
76
|
handoff?: SubagentHandoff;
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -90,6 +97,7 @@ export interface SubagentStop extends SubagentThreadPatch {
|
|
|
90
97
|
|
|
91
98
|
export interface SubagentThread extends SubagentThreadSpec {
|
|
92
99
|
id: SubagentThreadId;
|
|
100
|
+
attempt: number;
|
|
93
101
|
state: SubagentThreadState;
|
|
94
102
|
usage: SubagentUsage;
|
|
95
103
|
trace: SubagentTraceEvent[];
|
|
@@ -113,6 +121,7 @@ export type SubagentThreadChangeType =
|
|
|
113
121
|
| "fail"
|
|
114
122
|
| "stop"
|
|
115
123
|
| "interrupt"
|
|
124
|
+
| "resume"
|
|
116
125
|
| "close";
|
|
117
126
|
|
|
118
127
|
export interface SubagentThreadChange {
|
|
@@ -129,10 +138,21 @@ export interface SubagentThreadRegistryOptions {
|
|
|
129
138
|
|
|
130
139
|
export type SubagentThreadListener = (change: SubagentThreadChange) => void;
|
|
131
140
|
|
|
141
|
+
export interface SubagentWaitResult {
|
|
142
|
+
threadIds: readonly SubagentThreadId[];
|
|
143
|
+
completedThreadIds: readonly SubagentThreadId[];
|
|
144
|
+
pendingThreadIds: readonly SubagentThreadId[];
|
|
145
|
+
timedOut: boolean;
|
|
146
|
+
waitedMs: number;
|
|
147
|
+
threads: readonly SubagentThread[];
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
const DEFAULT_MAX_STEERING_MESSAGES = 20;
|
|
133
151
|
const DEFAULT_MAX_STEERING_MESSAGE_LENGTH = 4_000;
|
|
134
152
|
const UPDATABLE_STATES = new Set<SubagentThreadState>(["queued", "active"]);
|
|
135
|
-
const TERMINAL_STATES = new Set<SubagentThreadState>(["done", "failed", "stopped"]);
|
|
153
|
+
const TERMINAL_STATES = new Set<SubagentThreadState>(["done", "failed", "stopped", "orphaned"]);
|
|
154
|
+
const DISPLAY_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._ -]{0,47}$/u;
|
|
155
|
+
const MAX_WAIT_TIMEOUT_MS = 2_147_483_647;
|
|
136
156
|
const USAGE_FIELDS = [
|
|
137
157
|
"inputTokens",
|
|
138
158
|
"outputTokens",
|
|
@@ -168,6 +188,10 @@ function copyBoundary(boundary: SubagentCapabilityBoundary): SubagentCapabilityB
|
|
|
168
188
|
return { ...boundary };
|
|
169
189
|
}
|
|
170
190
|
|
|
191
|
+
function copySession(session: SubagentThreadSession): SubagentThreadSession {
|
|
192
|
+
return { id: session.id, directory: session.directory };
|
|
193
|
+
}
|
|
194
|
+
|
|
171
195
|
function copyTraceEvent(event: SubagentTraceEvent): SubagentTraceEvent {
|
|
172
196
|
return {
|
|
173
197
|
at: event.at,
|
|
@@ -181,12 +205,15 @@ function snapshot(thread: SubagentThread): SubagentThread {
|
|
|
181
205
|
return {
|
|
182
206
|
id: thread.id,
|
|
183
207
|
parentId: thread.parentId,
|
|
208
|
+
displayName: thread.displayName,
|
|
184
209
|
role: thread.role,
|
|
185
210
|
prompt: thread.prompt,
|
|
186
211
|
model: thread.model,
|
|
187
212
|
tools: [...thread.tools],
|
|
188
213
|
capabilityBoundary: copyBoundary(thread.capabilityBoundary),
|
|
214
|
+
session: copySession(thread.session),
|
|
189
215
|
handoff: copyHandoff(thread.handoff),
|
|
216
|
+
attempt: thread.attempt,
|
|
190
217
|
state: thread.state,
|
|
191
218
|
usage: { ...thread.usage },
|
|
192
219
|
trace: thread.trace.map(copyTraceEvent),
|
|
@@ -201,7 +228,7 @@ function snapshot(thread: SubagentThread): SubagentThread {
|
|
|
201
228
|
}
|
|
202
229
|
|
|
203
230
|
function requireText(value: string, name: string): void {
|
|
204
|
-
if (!value.trim()) throw new Error(`${name} must be non-empty`);
|
|
231
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`${name} must be non-empty`);
|
|
205
232
|
}
|
|
206
233
|
|
|
207
234
|
function requirePositiveInteger(value: number, name: string): void {
|
|
@@ -218,12 +245,27 @@ function validateUsage(patch: Partial<SubagentUsage>): void {
|
|
|
218
245
|
}
|
|
219
246
|
|
|
220
247
|
function validateHandoff(handoff: SubagentHandoff): void {
|
|
248
|
+
if (!handoff || typeof handoff !== "object") throw new Error("handoff must be an object");
|
|
221
249
|
requireText(handoff.summary, "handoff.summary");
|
|
222
250
|
if (handoff.nextAction !== undefined) requireText(handoff.nextAction, "handoff.nextAction");
|
|
251
|
+
if (handoff.artifacts !== undefined && !Array.isArray(handoff.artifacts)) throw new Error("handoff.artifacts must be an array");
|
|
223
252
|
for (const artifact of handoff.artifacts ?? []) requireText(artifact, "handoff artifact");
|
|
224
253
|
}
|
|
225
254
|
|
|
255
|
+
function validateDisplayName(displayName: string): void {
|
|
256
|
+
if (typeof displayName !== "string" || !DISPLAY_NAME_PATTERN.test(displayName)) {
|
|
257
|
+
throw new Error("display name must match ^[A-Za-z0-9][A-Za-z0-9._ -]{0,47}$");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function validateSession(session: SubagentThreadSession): void {
|
|
262
|
+
if (!session || typeof session !== "object") throw new Error("session must be an object");
|
|
263
|
+
requireText(session.id, "session.id");
|
|
264
|
+
requireText(session.directory, "session.directory");
|
|
265
|
+
}
|
|
266
|
+
|
|
226
267
|
function validateBoundary(boundary: SubagentCapabilityBoundary): void {
|
|
268
|
+
if (!boundary || typeof boundary !== "object") throw new Error("capabilityBoundary must be an object");
|
|
227
269
|
if (!(["none", "read", "write"] as string[]).includes(boundary.filesystem)) {
|
|
228
270
|
throw new Error("capabilityBoundary.filesystem must be none, read, or write");
|
|
229
271
|
}
|
|
@@ -236,10 +278,56 @@ function validateBoundary(boundary: SubagentCapabilityBoundary): void {
|
|
|
236
278
|
if (typeof boundary.childThreads !== "boolean") throw new Error("capabilityBoundary.childThreads must be a boolean");
|
|
237
279
|
}
|
|
238
280
|
|
|
281
|
+
function validateTraceDetails(details: Readonly<Record<string, string | number | boolean | null>> | undefined): void {
|
|
282
|
+
if (details === undefined) return;
|
|
283
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) throw new Error("trace.details must be an object");
|
|
284
|
+
for (const value of Object.values(details)) {
|
|
285
|
+
if (value !== null && !["string", "number", "boolean"].includes(typeof value)) {
|
|
286
|
+
throw new Error("trace.details values must be strings, numbers, booleans, or null");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function validateTraceEvent(event: SubagentTraceEvent, index: number): void {
|
|
292
|
+
if (!event || typeof event !== "object") throw new Error(`trace[${index}] must be an object`);
|
|
293
|
+
if (!Number.isFinite(event.at)) throw new Error(`trace[${index}].at must be a finite number`);
|
|
294
|
+
requireText(event.kind, `trace[${index}].kind`);
|
|
295
|
+
if (event.message !== undefined) requireText(event.message, `trace[${index}].message`);
|
|
296
|
+
validateTraceDetails(event.details);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateSteeringMessage(message: SubagentSteeringMessage, index: number): void {
|
|
300
|
+
if (!message || typeof message !== "object") throw new Error(`steering[${index}] must be an object`);
|
|
301
|
+
requirePositiveInteger(message.id, `steering[${index}].id`);
|
|
302
|
+
if (!Number.isFinite(message.at)) throw new Error(`steering[${index}].at must be a finite number`);
|
|
303
|
+
requireText(message.message, `steering[${index}].message`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function validateCompleteUsage(usage: SubagentUsage): void {
|
|
307
|
+
if (!usage || typeof usage !== "object") throw new Error("usage must be an object");
|
|
308
|
+
for (const field of USAGE_FIELDS) {
|
|
309
|
+
if (!(field in usage)) throw new Error(`usage.${field} is required`);
|
|
310
|
+
}
|
|
311
|
+
validateUsage(usage);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function validateTimestamps(timestamps: SubagentThreadTimestamps): void {
|
|
315
|
+
if (!timestamps || typeof timestamps !== "object") throw new Error("timestamps must be an object");
|
|
316
|
+
for (const field of ["createdAt", "updatedAt", "startedAt", "endedAt", "closedAt"] as const) {
|
|
317
|
+
const value = timestamps[field];
|
|
318
|
+
if (value !== undefined && !Number.isFinite(value)) throw new Error(`timestamps.${field} must be a finite number`);
|
|
319
|
+
}
|
|
320
|
+
if (timestamps.createdAt === undefined) throw new Error("timestamps.createdAt is required");
|
|
321
|
+
if (timestamps.updatedAt === undefined) throw new Error("timestamps.updatedAt is required");
|
|
322
|
+
}
|
|
323
|
+
|
|
239
324
|
function isTerminal(state: SubagentThreadState): state is SubagentTerminalState {
|
|
240
325
|
return TERMINAL_STATES.has(state);
|
|
241
326
|
}
|
|
242
327
|
|
|
328
|
+
// Module-scoped so fresh registries keep assigning fresh ids across session replacements.
|
|
329
|
+
let nextId = 0;
|
|
330
|
+
|
|
243
331
|
/**
|
|
244
332
|
* Owns child-thread state only. Callers execute, cancel, and transport work.
|
|
245
333
|
* Each read returns a copy, so callers cannot mutate registry state.
|
|
@@ -251,7 +339,6 @@ export class SubagentThreadRegistry {
|
|
|
251
339
|
private readonly createId: () => string;
|
|
252
340
|
private readonly maxSteeringMessages: number;
|
|
253
341
|
private readonly maxSteeringMessageLength: number;
|
|
254
|
-
private nextId = 0;
|
|
255
342
|
private nextSteeringId = 0;
|
|
256
343
|
private disposed = false;
|
|
257
344
|
|
|
@@ -261,7 +348,7 @@ export class SubagentThreadRegistry {
|
|
|
261
348
|
this.maxSteeringMessageLength = options.maxSteeringMessageLength ?? DEFAULT_MAX_STEERING_MESSAGE_LENGTH;
|
|
262
349
|
requirePositiveInteger(this.maxSteeringMessages, "maxSteeringMessages");
|
|
263
350
|
requirePositiveInteger(this.maxSteeringMessageLength, "maxSteeringMessageLength");
|
|
264
|
-
this.createId = options.createId ?? (() => `subagent-${++
|
|
351
|
+
this.createId = options.createId ?? (() => `subagent-${++nextId}`);
|
|
265
352
|
}
|
|
266
353
|
|
|
267
354
|
get isDisposed(): boolean {
|
|
@@ -271,6 +358,7 @@ export class SubagentThreadRegistry {
|
|
|
271
358
|
spawn(spec: SubagentThreadSpec): SubagentThread {
|
|
272
359
|
this.assertOpen();
|
|
273
360
|
this.validateSpec(spec);
|
|
361
|
+
this.assertUniqueDisplayName(spec.displayName, spec.parentId);
|
|
274
362
|
const rawId = this.createId();
|
|
275
363
|
requireText(rawId, "thread id");
|
|
276
364
|
const id = rawId as SubagentThreadId;
|
|
@@ -280,12 +368,15 @@ export class SubagentThreadRegistry {
|
|
|
280
368
|
const thread: SubagentThread = {
|
|
281
369
|
id,
|
|
282
370
|
parentId: spec.parentId,
|
|
371
|
+
displayName: spec.displayName,
|
|
283
372
|
role: spec.role,
|
|
284
373
|
prompt: spec.prompt,
|
|
285
374
|
model: spec.model,
|
|
286
375
|
tools: [...spec.tools],
|
|
287
376
|
capabilityBoundary: copyBoundary(spec.capabilityBoundary),
|
|
377
|
+
session: copySession(spec.session),
|
|
288
378
|
handoff: copyHandoff(spec.handoff),
|
|
379
|
+
attempt: 1,
|
|
289
380
|
state: "queued",
|
|
290
381
|
usage: emptyUsage(),
|
|
291
382
|
trace: [],
|
|
@@ -337,13 +428,16 @@ export class SubagentThreadRegistry {
|
|
|
337
428
|
throw new Error(`steering message exceeds ${this.maxSteeringMessageLength} characters`);
|
|
338
429
|
}
|
|
339
430
|
thread.steering.push({ id: ++this.nextSteeringId, at: this.now(), message });
|
|
340
|
-
if (thread.steering.length > this.maxSteeringMessages) thread.steering.splice(
|
|
431
|
+
if (thread.steering.length > this.maxSteeringMessages) thread.steering.splice(this.maxSteeringMessages);
|
|
341
432
|
this.changed(thread, "steer");
|
|
342
433
|
return snapshot(thread);
|
|
343
434
|
}
|
|
344
435
|
|
|
345
436
|
complete(id: SubagentThreadId, completion: SubagentCompletion = {}): SubagentThread {
|
|
346
437
|
const thread = this.requireState(id, ["active"]);
|
|
438
|
+
const result = completion.result !== undefined ? completion.result : thread.result;
|
|
439
|
+
if (result === undefined || result === null) throw new Error(`Cannot complete thread ${id} without a usable result`);
|
|
440
|
+
requireText(result, "result");
|
|
347
441
|
this.applyPatch(thread, completion);
|
|
348
442
|
thread.state = "done";
|
|
349
443
|
thread.timestamps.endedAt = this.now();
|
|
@@ -397,11 +491,107 @@ export class SubagentThreadRegistry {
|
|
|
397
491
|
return thread ? snapshot(thread) : undefined;
|
|
398
492
|
}
|
|
399
493
|
|
|
494
|
+
resolve(reference: string, parentId?: SubagentThreadId): SubagentThread | undefined {
|
|
495
|
+
if (typeof reference !== "string") return undefined;
|
|
496
|
+
const exact = this.threads.get(reference as SubagentThreadId);
|
|
497
|
+
if (exact) return snapshot(exact);
|
|
498
|
+
const name = reference.toLocaleLowerCase();
|
|
499
|
+
const match = [...this.threads.values()].find((thread) =>
|
|
500
|
+
thread.parentId === parentId && thread.displayName.toLocaleLowerCase() === name,
|
|
501
|
+
);
|
|
502
|
+
return match ? snapshot(match) : undefined;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
hydrate(thread: SubagentThread): SubagentThread {
|
|
506
|
+
this.assertOpen();
|
|
507
|
+
this.validateThreadSnapshot(thread);
|
|
508
|
+
if (this.threads.has(thread.id)) throw new Error(`Duplicate thread id ${thread.id}`);
|
|
509
|
+
this.assertUniqueDisplayName(thread.displayName, thread.parentId);
|
|
510
|
+
|
|
511
|
+
const hydrated = snapshot(thread);
|
|
512
|
+
if (hydrated.state === "queued" || hydrated.state === "active") {
|
|
513
|
+
hydrated.state = "orphaned";
|
|
514
|
+
hydrated.stopReason = "parent_restarted";
|
|
515
|
+
}
|
|
516
|
+
this.threads.set(hydrated.id, hydrated);
|
|
517
|
+
for (const message of hydrated.steering) this.nextSteeringId = Math.max(this.nextSteeringId, message.id);
|
|
518
|
+
return snapshot(hydrated);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
resume(id: SubagentThreadId, prompt?: string): SubagentThread {
|
|
522
|
+
const thread = this.requireState(id, ["done", "failed", "stopped", "orphaned"]);
|
|
523
|
+
if (prompt !== undefined) {
|
|
524
|
+
requireText(prompt, "prompt");
|
|
525
|
+
thread.prompt = prompt;
|
|
526
|
+
}
|
|
527
|
+
thread.attempt += 1;
|
|
528
|
+
thread.result = undefined;
|
|
529
|
+
thread.failure = undefined;
|
|
530
|
+
thread.stopReason = undefined;
|
|
531
|
+
delete thread.timestamps.startedAt;
|
|
532
|
+
delete thread.timestamps.endedAt;
|
|
533
|
+
thread.state = "queued";
|
|
534
|
+
this.changed(thread, "resume");
|
|
535
|
+
return snapshot(thread);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
waitForTerminal(ids: readonly SubagentThreadId[], timeoutMs: number): Promise<SubagentWaitResult> {
|
|
539
|
+
const threadIds = [...ids];
|
|
540
|
+
if (threadIds.length === 0) {
|
|
541
|
+
return Promise.resolve({
|
|
542
|
+
threadIds,
|
|
543
|
+
completedThreadIds: [],
|
|
544
|
+
pendingThreadIds: [],
|
|
545
|
+
timedOut: false,
|
|
546
|
+
waitedMs: 0,
|
|
547
|
+
threads: [],
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > MAX_WAIT_TIMEOUT_MS) {
|
|
551
|
+
throw new Error(`timeoutMs must be an integer from 0 to ${MAX_WAIT_TIMEOUT_MS}`);
|
|
552
|
+
}
|
|
553
|
+
for (const id of threadIds) this.requireThread(id);
|
|
554
|
+
|
|
555
|
+
const startedAt = Date.now();
|
|
556
|
+
const currentThreads = (): SubagentThread[] => threadIds.map((id) => snapshot(this.threads.get(id)!));
|
|
557
|
+
const makeResult = (timedOut: boolean): SubagentWaitResult => {
|
|
558
|
+
const threads = currentThreads();
|
|
559
|
+
const completedThreadIds = threads.filter((thread) => isTerminal(thread.state)).map((thread) => thread.id);
|
|
560
|
+
return {
|
|
561
|
+
threadIds,
|
|
562
|
+
completedThreadIds,
|
|
563
|
+
pendingThreadIds: threads.filter((thread) => !isTerminal(thread.state)).map((thread) => thread.id),
|
|
564
|
+
timedOut,
|
|
565
|
+
waitedMs: Math.max(0, Date.now() - startedAt),
|
|
566
|
+
threads,
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
const allTerminal = (): boolean => threadIds.every((id) => isTerminal(this.threads.get(id)!.state));
|
|
570
|
+
if (allTerminal()) return Promise.resolve(makeResult(false));
|
|
571
|
+
|
|
572
|
+
return new Promise<SubagentWaitResult>((resolve) => {
|
|
573
|
+
let settled = false;
|
|
574
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
575
|
+
let unsubscribe = (): void => {};
|
|
576
|
+
const finish = (timedOut: boolean): void => {
|
|
577
|
+
if (settled) return;
|
|
578
|
+
settled = true;
|
|
579
|
+
unsubscribe();
|
|
580
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
581
|
+
resolve(makeResult(timedOut));
|
|
582
|
+
};
|
|
583
|
+
unsubscribe = this.subscribe((change) => {
|
|
584
|
+
if (threadIds.includes(change.thread.id) && allTerminal()) finish(false);
|
|
585
|
+
});
|
|
586
|
+
timer = setTimeout(() => finish(true), timeoutMs);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
400
590
|
listActive(): SubagentThread[] {
|
|
401
591
|
return this.list((thread) => thread.state === "active");
|
|
402
592
|
}
|
|
403
593
|
|
|
404
|
-
/** Returns all terminal records: done, failed, and
|
|
594
|
+
/** Returns all terminal records: done, failed, stopped, and orphaned. */
|
|
405
595
|
listDone(): SubagentThread[] {
|
|
406
596
|
return this.list((thread) => isTerminal(thread.state));
|
|
407
597
|
}
|
|
@@ -468,10 +658,15 @@ export class SubagentThreadRegistry {
|
|
|
468
658
|
}
|
|
469
659
|
|
|
470
660
|
private validateSpec(spec: SubagentThreadSpec): void {
|
|
661
|
+
if (!spec || typeof spec !== "object") throw new Error("thread spec must be an object");
|
|
662
|
+
if (spec.parentId !== undefined) requireText(spec.parentId, "parent id");
|
|
663
|
+
validateDisplayName(spec.displayName);
|
|
471
664
|
requireText(spec.role, "role");
|
|
472
665
|
requireText(spec.prompt, "prompt");
|
|
473
666
|
requireText(spec.model, "model");
|
|
474
667
|
validateBoundary(spec.capabilityBoundary);
|
|
668
|
+
validateSession(spec.session);
|
|
669
|
+
if (!Array.isArray(spec.tools)) throw new Error("tools must be an array");
|
|
475
670
|
const tools = new Set<string>();
|
|
476
671
|
for (const tool of spec.tools) {
|
|
477
672
|
requireText(tool, "tool");
|
|
@@ -481,6 +676,31 @@ export class SubagentThreadRegistry {
|
|
|
481
676
|
if (spec.handoff) validateHandoff(spec.handoff);
|
|
482
677
|
}
|
|
483
678
|
|
|
679
|
+
private validateThreadSnapshot(thread: SubagentThread): void {
|
|
680
|
+
if (!thread || typeof thread !== "object") throw new Error("thread snapshot must be an object");
|
|
681
|
+
requireText(thread.id, "thread id");
|
|
682
|
+
this.validateSpec(thread);
|
|
683
|
+
if (!(["queued", "active", "done", "failed", "stopped", "orphaned", "closed"] as string[]).includes(thread.state)) {
|
|
684
|
+
throw new Error(`Unknown thread state ${thread.state}`);
|
|
685
|
+
}
|
|
686
|
+
requirePositiveInteger(thread.attempt, "attempt");
|
|
687
|
+
validateCompleteUsage(thread.usage);
|
|
688
|
+
if (!Array.isArray(thread.trace)) throw new Error("trace must be an array");
|
|
689
|
+
thread.trace.forEach(validateTraceEvent);
|
|
690
|
+
if (!Array.isArray(thread.steering)) throw new Error("steering must be an array");
|
|
691
|
+
thread.steering.forEach(validateSteeringMessage);
|
|
692
|
+
if (thread.result !== undefined) requireText(thread.result, "result");
|
|
693
|
+
if (thread.failure !== undefined) {
|
|
694
|
+
if (!thread.failure || typeof thread.failure !== "object") throw new Error("failure must be an object");
|
|
695
|
+
requireText(thread.failure.message, "failure.message");
|
|
696
|
+
if (thread.failure.code !== undefined) requireText(thread.failure.code, "failure.code");
|
|
697
|
+
}
|
|
698
|
+
if (thread.stopReason !== undefined) requireText(thread.stopReason, "stopReason");
|
|
699
|
+
if (typeof thread.evicted !== "boolean") throw new Error("evicted must be a boolean");
|
|
700
|
+
validateTimestamps(thread.timestamps);
|
|
701
|
+
requirePositiveInteger(thread.version, "version");
|
|
702
|
+
}
|
|
703
|
+
|
|
484
704
|
private applyPatch(thread: SubagentThread, patch: SubagentThreadPatch): void {
|
|
485
705
|
if (patch.usage) {
|
|
486
706
|
validateUsage(patch.usage);
|
|
@@ -500,6 +720,15 @@ export class SubagentThreadRegistry {
|
|
|
500
720
|
return [...this.threads.values()].filter(matches).map(snapshot);
|
|
501
721
|
}
|
|
502
722
|
|
|
723
|
+
private assertUniqueDisplayName(displayName: string, parentId?: SubagentThreadId): void {
|
|
724
|
+
const name = displayName.toLocaleLowerCase();
|
|
725
|
+
if ([...this.threads.values()].some((thread) =>
|
|
726
|
+
thread.parentId === parentId && thread.displayName.toLocaleLowerCase() === name,
|
|
727
|
+
)) {
|
|
728
|
+
throw new Error(`display name ${displayName} already exists for this parent`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
503
732
|
private requireThread(id: SubagentThreadId): SubagentThread {
|
|
504
733
|
this.assertOpen();
|
|
505
734
|
const thread = this.threads.get(id);
|