killeros 1.5.4 → 1.5.6
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/README.md +5 -5
- package/killeros/context-compaction.ts +64 -16
- package/killeros/runtime.ts +1 -1
- package/killeros/subagents.ts +1422 -1291
- package/package.json +5 -1
package/killeros/subagents.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,7 +19,7 @@ import { SubagentThreadRegistry, type SubagentThread, type SubagentThreadId, typ
|
|
|
19
19
|
import { MAX_NODE_TIMER_MS, runSubagentProcess, type SubagentProcessHandle, type SubagentProcessResult } from "./subagent-process.ts";
|
|
20
20
|
import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadBoardRecord } from "./subagent-ui.ts";
|
|
21
21
|
|
|
22
|
-
export const SUBAGENT_LIMITS = {
|
|
22
|
+
export const SUBAGENT_LIMITS = {
|
|
23
23
|
maxTasks: 10,
|
|
24
24
|
maxReadConcurrency: 4,
|
|
25
25
|
toolOutputBytes: 50 * 1024,
|
|
@@ -29,11 +29,11 @@ export const SUBAGENT_LIMITS = {
|
|
|
29
29
|
threadRetentionRecords: 64,
|
|
30
30
|
threadRetentionBytes: 128 * 1024 * 1024,
|
|
31
31
|
roleFileBytes: 64 * 1024,
|
|
32
|
-
taskCharacters: 20_000,
|
|
33
|
-
killGraceMs: 5_000,
|
|
34
|
-
defaultWallTimeMs: 1_800_000,
|
|
35
|
-
processExitWaitMs: 10_000,
|
|
36
|
-
} as const;
|
|
32
|
+
taskCharacters: 20_000,
|
|
33
|
+
killGraceMs: 5_000,
|
|
34
|
+
defaultWallTimeMs: 1_800_000,
|
|
35
|
+
processExitWaitMs: 10_000,
|
|
36
|
+
} as const;
|
|
37
37
|
|
|
38
38
|
const WEB_TOOLS = new Set(["web_search", "source_check", "fetch_content", "get_search_content"]);
|
|
39
39
|
const READ_TOOLS = new Set(["read", "grep", "find", "ls", ...WEB_TOOLS]);
|
|
@@ -46,9 +46,9 @@ const ROLE_FIELDS = new Set(["name", "description", "access", "tools", "model",
|
|
|
46
46
|
|
|
47
47
|
type ThinkingLevel = ModelThinkingLevel;
|
|
48
48
|
export type AgentAccess = "read" | "write";
|
|
49
|
-
export type AgentSource = "bundled" | "personal" | "project";
|
|
49
|
+
export type AgentSource = "bundled" | "personal" | "project" | "inline";
|
|
50
50
|
export type AgentScope = "user" | "project" | "both";
|
|
51
|
-
export type SubagentStatus = "queued" | "running" | "complete" | "failed" | "cancelled" | "limited" | "orphaned";
|
|
51
|
+
export type SubagentStatus = "queued" | "running" | "complete" | "failed" | "cancelled" | "limited" | "orphaned";
|
|
52
52
|
|
|
53
53
|
export interface AgentRole {
|
|
54
54
|
name: string;
|
|
@@ -63,6 +63,15 @@ export interface AgentRole {
|
|
|
63
63
|
filePath: string;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
export interface InlineAgentRole {
|
|
67
|
+
name: string;
|
|
68
|
+
description: string;
|
|
69
|
+
access: AgentAccess;
|
|
70
|
+
tools: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type AgentSpec = string | InlineAgentRole;
|
|
74
|
+
|
|
66
75
|
export interface AgentDiscoveryResult {
|
|
67
76
|
agents: AgentRole[];
|
|
68
77
|
projectAgentsDir: string | null;
|
|
@@ -84,10 +93,10 @@ export interface SubagentUsage {
|
|
|
84
93
|
turns: number;
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
export interface SubagentTaskResult {
|
|
88
|
-
id: string;
|
|
89
|
-
name?: string;
|
|
90
|
-
attempt: number;
|
|
96
|
+
export interface SubagentTaskResult {
|
|
97
|
+
id: string;
|
|
98
|
+
name?: string;
|
|
99
|
+
attempt: number;
|
|
91
100
|
agent: string;
|
|
92
101
|
agentSource: AgentSource | "unknown";
|
|
93
102
|
sourcePath?: string;
|
|
@@ -109,14 +118,14 @@ export interface SubagentTaskResult {
|
|
|
109
118
|
toolCallCount: number;
|
|
110
119
|
usage: SubagentUsage;
|
|
111
120
|
durationMs: number;
|
|
112
|
-
exitCode: number | null;
|
|
113
|
-
exitConfirmed: boolean;
|
|
114
|
-
terminationReason?: string;
|
|
121
|
+
exitCode: number | null;
|
|
122
|
+
exitConfirmed: boolean;
|
|
123
|
+
terminationReason?: string;
|
|
115
124
|
errorMessage?: string;
|
|
116
125
|
step?: number;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
export interface SubagentDetails {
|
|
128
|
+
export interface SubagentDetails {
|
|
120
129
|
mode: "single" | "parallel" | "chain";
|
|
121
130
|
agentScope: AgentScope;
|
|
122
131
|
projectAgentsDir: string | null;
|
|
@@ -127,38 +136,38 @@ export interface SubagentDetails {
|
|
|
127
136
|
threads?: SubagentThread[];
|
|
128
137
|
activeThreads?: SubagentThread[];
|
|
129
138
|
doneThreads?: SubagentThread[];
|
|
130
|
-
selectedThreadId?: string;
|
|
131
|
-
wait?: SubagentWaitSummary;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export interface SubagentWaitSummary {
|
|
135
|
-
targetThreadIds: string[];
|
|
136
|
-
completedThreadIds: string[];
|
|
137
|
-
pendingThreadIds: string[];
|
|
138
|
-
timedOut: boolean;
|
|
139
|
-
waitedMs: number;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface SubagentControlRequest {
|
|
143
|
-
action: "list" | "inspect" | "wait" | "steer" | "interrupt" | "collect" | "resume" | "close";
|
|
144
|
-
threadId?: string;
|
|
145
|
-
all?: true;
|
|
146
|
-
message?: string;
|
|
147
|
-
task?: string;
|
|
148
|
-
timeoutMs?: number;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface SubagentControlResult {
|
|
152
|
-
text: string;
|
|
153
|
-
details: SubagentDetails;
|
|
154
|
-
usage: SubagentUsage;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export interface SubagentControlApi {
|
|
158
|
-
execute(request: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult>;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export const SUBAGENT_PERSISTENCE_TYPE = "killeros-subagent-v1";
|
|
139
|
+
selectedThreadId?: string;
|
|
140
|
+
wait?: SubagentWaitSummary;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SubagentWaitSummary {
|
|
144
|
+
targetThreadIds: string[];
|
|
145
|
+
completedThreadIds: string[];
|
|
146
|
+
pendingThreadIds: string[];
|
|
147
|
+
timedOut: boolean;
|
|
148
|
+
waitedMs: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface SubagentControlRequest {
|
|
152
|
+
action: "list" | "inspect" | "wait" | "steer" | "interrupt" | "collect" | "resume" | "close";
|
|
153
|
+
threadId?: string;
|
|
154
|
+
all?: true;
|
|
155
|
+
message?: string;
|
|
156
|
+
task?: string;
|
|
157
|
+
timeoutMs?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface SubagentControlResult {
|
|
161
|
+
text: string;
|
|
162
|
+
details: SubagentDetails;
|
|
163
|
+
usage: SubagentUsage;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface SubagentControlApi {
|
|
167
|
+
execute(request: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export const SUBAGENT_PERSISTENCE_TYPE = "killeros-subagent-v1";
|
|
162
171
|
|
|
163
172
|
interface ModelContext {
|
|
164
173
|
model?: Model<any>;
|
|
@@ -183,7 +192,7 @@ interface SpawnedProcess {
|
|
|
183
192
|
once(event: "close", listener: (code: number | null) => void): this;
|
|
184
193
|
}
|
|
185
194
|
|
|
186
|
-
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
195
|
+
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
187
196
|
wallTimeMs?: number;
|
|
188
197
|
jsonlLineBytes?: number;
|
|
189
198
|
traceBytes?: number;
|
|
@@ -193,7 +202,7 @@ type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
|
193
202
|
quotaUsd?: number;
|
|
194
203
|
};
|
|
195
204
|
|
|
196
|
-
export interface SubagentRuntimeOptions {
|
|
205
|
+
export interface SubagentRuntimeOptions {
|
|
197
206
|
bundledAgentsDir?: string;
|
|
198
207
|
userAgentsDir?: string;
|
|
199
208
|
webExtension?: string;
|
|
@@ -520,11 +529,11 @@ function truncateUtf8(text: string, maxBytes: number): { text: string; omittedBy
|
|
|
520
529
|
return { text: truncated, omittedBytes: bytes.length - Buffer.byteLength(truncated, "utf8") };
|
|
521
530
|
}
|
|
522
531
|
|
|
523
|
-
function makeQueuedResult(id: string, agent: string, task: string, step?: number, name?: string, attempt = 1): SubagentTaskResult {
|
|
524
|
-
return {
|
|
525
|
-
id,
|
|
526
|
-
name,
|
|
527
|
-
attempt,
|
|
532
|
+
function makeQueuedResult(id: string, agent: string, task: string, step?: number, name?: string, attempt = 1): SubagentTaskResult {
|
|
533
|
+
return {
|
|
534
|
+
id,
|
|
535
|
+
name,
|
|
536
|
+
attempt,
|
|
528
537
|
agent,
|
|
529
538
|
agentSource: "unknown",
|
|
530
539
|
task,
|
|
@@ -541,12 +550,12 @@ function makeQueuedResult(id: string, agent: string, task: string, step?: number
|
|
|
541
550
|
outputTruncatedBytes: 0,
|
|
542
551
|
toolCallCount: 0,
|
|
543
552
|
usage: emptyUsage(),
|
|
544
|
-
durationMs: 0,
|
|
545
|
-
exitCode: null,
|
|
546
|
-
exitConfirmed: false,
|
|
547
|
-
step,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
553
|
+
durationMs: 0,
|
|
554
|
+
exitCode: null,
|
|
555
|
+
exitConfirmed: false,
|
|
556
|
+
step,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
550
559
|
|
|
551
560
|
function cloneResult(result: SubagentTaskResult): SubagentTaskResult {
|
|
552
561
|
return {
|
|
@@ -606,13 +615,13 @@ async function writeRolePrompt(agent: AgentRole): Promise<{ directory: string; f
|
|
|
606
615
|
return { directory, filePath };
|
|
607
616
|
}
|
|
608
617
|
|
|
609
|
-
interface RunTaskOptions {
|
|
618
|
+
interface RunTaskOptions {
|
|
610
619
|
cwd: string;
|
|
611
620
|
agent: AgentRole;
|
|
612
621
|
task: string;
|
|
613
|
-
id: string;
|
|
614
|
-
displayName: string;
|
|
615
|
-
attempt: number;
|
|
622
|
+
id: string;
|
|
623
|
+
displayName: string;
|
|
624
|
+
attempt: number;
|
|
616
625
|
step?: number;
|
|
617
626
|
model: ResolvedModel;
|
|
618
627
|
signal?: AbortSignal;
|
|
@@ -620,8 +629,8 @@ interface RunTaskOptions {
|
|
|
620
629
|
webExtension?: string;
|
|
621
630
|
projectTrusted: boolean;
|
|
622
631
|
limits: SubagentLimits;
|
|
623
|
-
sessionDirectory: string;
|
|
624
|
-
sessionId: string;
|
|
632
|
+
sessionDirectory: string;
|
|
633
|
+
sessionId: string;
|
|
625
634
|
timeoutMs?: number;
|
|
626
635
|
onChange: (result: SubagentTaskResult) => void;
|
|
627
636
|
onHandle?: (handle: SubagentProcessHandle) => void;
|
|
@@ -647,16 +656,16 @@ function applyProcessResult(
|
|
|
647
656
|
target.usage = { ...source.usage, cost: { ...source.usage.cost } };
|
|
648
657
|
target.model = source.model ?? target.model;
|
|
649
658
|
target.terminationReason = source.terminationReason;
|
|
650
|
-
target.errorMessage = source.errorMessage;
|
|
651
|
-
target.exitCode = source.exitCode;
|
|
652
|
-
target.exitConfirmed = source.exitConfirmed;
|
|
653
|
-
target.durationMs = source.durationMs || Date.now() - startedAt;
|
|
659
|
+
target.errorMessage = source.errorMessage;
|
|
660
|
+
target.exitCode = source.exitCode;
|
|
661
|
+
target.exitConfirmed = source.exitConfirmed;
|
|
662
|
+
target.durationMs = source.durationMs || Date.now() - startedAt;
|
|
654
663
|
onChange(target);
|
|
655
664
|
}
|
|
656
665
|
|
|
657
|
-
async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
658
|
-
const { agent, limits } = options;
|
|
659
|
-
const result = makeQueuedResult(options.id, agent.name, options.task, options.step, options.displayName, options.attempt);
|
|
666
|
+
async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
667
|
+
const { agent, limits } = options;
|
|
668
|
+
const result = makeQueuedResult(options.id, agent.name, options.task, options.step, options.displayName, options.attempt);
|
|
660
669
|
result.agentSource = agent.source;
|
|
661
670
|
result.sourcePath = agent.filePath;
|
|
662
671
|
result.access = agent.access;
|
|
@@ -696,10 +705,10 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
696
705
|
const args = [
|
|
697
706
|
"--mode", "json",
|
|
698
707
|
"-p",
|
|
699
|
-
"--session-dir", options.sessionDirectory,
|
|
700
|
-
"--session-id", options.sessionId,
|
|
701
|
-
"--name", options.displayName,
|
|
702
|
-
"--no-extensions",
|
|
708
|
+
"--session-dir", options.sessionDirectory,
|
|
709
|
+
"--session-id", options.sessionId,
|
|
710
|
+
"--name", options.displayName,
|
|
711
|
+
"--no-extensions",
|
|
703
712
|
"--extension", options.webExtension ?? SUBAGENT_WEB_EXTENSION,
|
|
704
713
|
"--no-prompt-templates",
|
|
705
714
|
options.projectTrusted ? "--approve" : "--no-approve",
|
|
@@ -721,9 +730,9 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
721
730
|
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes }),
|
|
722
731
|
...(limits.taskOutputBytes === undefined ? {} : { outputBytes: limits.taskOutputBytes }),
|
|
723
732
|
...(limits.quotaTokens === undefined ? {} : { quotaTokens: limits.quotaTokens }),
|
|
724
|
-
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd }),
|
|
725
|
-
killGraceMs: limits.killGraceMs,
|
|
726
|
-
processExitWaitMs: limits.processExitWaitMs,
|
|
733
|
+
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd }),
|
|
734
|
+
killGraceMs: limits.killGraceMs,
|
|
735
|
+
processExitWaitMs: limits.processExitWaitMs,
|
|
727
736
|
},
|
|
728
737
|
retention: {
|
|
729
738
|
traceBytes: limits.traceRetentionBytes,
|
|
@@ -767,7 +776,7 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
767
776
|
await Promise.all(workers);
|
|
768
777
|
}
|
|
769
778
|
|
|
770
|
-
function waitForConfirmedProcessExit(handle: SubagentProcessHandle, timeoutMs = 1_000): Promise<boolean> {
|
|
779
|
+
function waitForConfirmedProcessExit(handle: SubagentProcessHandle, timeoutMs = 1_000): Promise<boolean> {
|
|
771
780
|
if (handle.hasExited) return Promise.resolve(true);
|
|
772
781
|
return new Promise((resolve) => {
|
|
773
782
|
let settled = false;
|
|
@@ -783,17 +792,17 @@ function waitForConfirmedProcessExit(handle: SubagentProcessHandle, timeoutMs =
|
|
|
783
792
|
});
|
|
784
793
|
}
|
|
785
794
|
|
|
786
|
-
const SUBAGENT_ACTION = {
|
|
787
|
-
spawn: "spawn",
|
|
788
|
-
list: "list",
|
|
789
|
-
inspect: "inspect",
|
|
790
|
-
steer: "steer",
|
|
791
|
-
interrupt: "interrupt",
|
|
792
|
-
collect: "collect",
|
|
793
|
-
wait: "wait",
|
|
794
|
-
resume: "resume",
|
|
795
|
-
close: "close",
|
|
796
|
-
} as const;
|
|
795
|
+
const SUBAGENT_ACTION = {
|
|
796
|
+
spawn: "spawn",
|
|
797
|
+
list: "list",
|
|
798
|
+
inspect: "inspect",
|
|
799
|
+
steer: "steer",
|
|
800
|
+
interrupt: "interrupt",
|
|
801
|
+
collect: "collect",
|
|
802
|
+
wait: "wait",
|
|
803
|
+
resume: "resume",
|
|
804
|
+
close: "close",
|
|
805
|
+
} as const;
|
|
797
806
|
type SubagentAction = typeof SUBAGENT_ACTION[keyof typeof SUBAGENT_ACTION];
|
|
798
807
|
|
|
799
808
|
type SpawnOptions = {
|
|
@@ -802,12 +811,12 @@ type SpawnOptions = {
|
|
|
802
811
|
agentScope?: AgentScope;
|
|
803
812
|
};
|
|
804
813
|
|
|
805
|
-
type SpawnSingleRequest = SpawnOptions & {
|
|
806
|
-
action?: "spawn";
|
|
807
|
-
agent:
|
|
808
|
-
task: string;
|
|
809
|
-
name?: string;
|
|
810
|
-
};
|
|
814
|
+
type SpawnSingleRequest = SpawnOptions & {
|
|
815
|
+
action?: "spawn";
|
|
816
|
+
agent: AgentSpec;
|
|
817
|
+
task: string;
|
|
818
|
+
name?: string;
|
|
819
|
+
};
|
|
811
820
|
|
|
812
821
|
type SpawnParallelRequest = SpawnOptions & {
|
|
813
822
|
action?: "spawn";
|
|
@@ -828,37 +837,37 @@ export type NormalizedSubagentRequest =
|
|
|
828
837
|
| { kind: "inspect"; input: { action: "inspect"; threadId: string } }
|
|
829
838
|
| { kind: "steer"; input: { action: "steer"; threadId: string; message: string } }
|
|
830
839
|
| { kind: "interrupt-one"; input: { action: "interrupt"; threadId: string } }
|
|
831
|
-
| { kind: "interrupt-all"; input: { action: "interrupt"; all: true } }
|
|
832
|
-
| { kind: "collect"; input: { action: "collect"; threadId: string } }
|
|
833
|
-
| { kind: "wait"; input: { action: "wait"; threadId?: string; all?: true; timeoutMs: number } }
|
|
834
|
-
| { kind: "resume"; input: { action: "resume"; threadId: string; task?: string } }
|
|
835
|
-
| { kind: "close"; input: { action: "close"; threadId: string } };
|
|
840
|
+
| { kind: "interrupt-all"; input: { action: "interrupt"; all: true } }
|
|
841
|
+
| { kind: "collect"; input: { action: "collect"; threadId: string } }
|
|
842
|
+
| { kind: "wait"; input: { action: "wait"; threadId?: string; all?: true; timeoutMs: number } }
|
|
843
|
+
| { kind: "resume"; input: { action: "resume"; threadId: string; task?: string } }
|
|
844
|
+
| { kind: "close"; input: { action: "close"; threadId: string } };
|
|
836
845
|
|
|
837
846
|
type SubagentRequestParse =
|
|
838
847
|
| { ok: true; request: NormalizedSubagentRequest }
|
|
839
848
|
| { ok: false; message: string };
|
|
840
849
|
|
|
841
|
-
const SUBAGENT_ACTIONS = Object.values(SUBAGENT_ACTION);
|
|
842
|
-
const SPAWN_OPTION_FIELDS = ["model", "thinking", "agentScope"] as const;
|
|
843
|
-
const THREAD_NAME_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._ -]{0,47}$";
|
|
844
|
-
const THREAD_NAME_RE = new RegExp(THREAD_NAME_PATTERN, "u");
|
|
845
|
-
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
846
|
-
const MAX_WAIT_TIMEOUT_MS = 3_600_000;
|
|
847
|
-
|
|
848
|
-
export function validateThreadName(name: string): void {
|
|
849
|
-
if (!name.trim()) throw new Error("Invalid subagent request: name must be a non-empty string.");
|
|
850
|
-
if ([...name].length > 48 || !THREAD_NAME_RE.test(name)) {
|
|
851
|
-
throw new Error(`Invalid subagent request: name must match ${THREAD_NAME_PATTERN}.`);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function optionalThreadName(record: Record<string, unknown>): string | undefined {
|
|
856
|
-
if (!Object.hasOwn(record, "name")) return undefined;
|
|
857
|
-
const value = record.name;
|
|
858
|
-
if (typeof value !== "string") throw new Error("Invalid subagent request: name must be a non-empty string.");
|
|
859
|
-
validateThreadName(value);
|
|
860
|
-
return value;
|
|
861
|
-
}
|
|
850
|
+
const SUBAGENT_ACTIONS = Object.values(SUBAGENT_ACTION);
|
|
851
|
+
const SPAWN_OPTION_FIELDS = ["model", "thinking", "agentScope"] as const;
|
|
852
|
+
const THREAD_NAME_PATTERN = "^[A-Za-z0-9][A-Za-z0-9._ -]{0,47}$";
|
|
853
|
+
const THREAD_NAME_RE = new RegExp(THREAD_NAME_PATTERN, "u");
|
|
854
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
855
|
+
const MAX_WAIT_TIMEOUT_MS = 3_600_000;
|
|
856
|
+
|
|
857
|
+
export function validateThreadName(name: string): void {
|
|
858
|
+
if (!name.trim()) throw new Error("Invalid subagent request: name must be a non-empty string.");
|
|
859
|
+
if ([...name].length > 48 || !THREAD_NAME_RE.test(name)) {
|
|
860
|
+
throw new Error(`Invalid subagent request: name must match ${THREAD_NAME_PATTERN}.`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function optionalThreadName(record: Record<string, unknown>): string | undefined {
|
|
865
|
+
if (!Object.hasOwn(record, "name")) return undefined;
|
|
866
|
+
const value = record.name;
|
|
867
|
+
if (typeof value !== "string") throw new Error("Invalid subagent request: name must be a non-empty string.");
|
|
868
|
+
validateThreadName(value);
|
|
869
|
+
return value;
|
|
870
|
+
}
|
|
862
871
|
|
|
863
872
|
function requireRecord(value: unknown, name: string): Record<string, unknown> {
|
|
864
873
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -891,21 +900,73 @@ function requireOnlyFields(record: Record<string, unknown>, allowed: readonly st
|
|
|
891
900
|
if (invalid) throw new Error(`Invalid subagent request: field ${JSON.stringify(invalid)} is not valid with action ${JSON.stringify(action)}.`);
|
|
892
901
|
}
|
|
893
902
|
|
|
903
|
+
function parseAgentSpec(value: unknown): AgentSpec {
|
|
904
|
+
if (typeof value === "string") {
|
|
905
|
+
if (!value.length) throw new Error("Invalid subagent request: agent must be a non-empty string or inline role.");
|
|
906
|
+
if ([...value].length > 64) throw new Error("Invalid subagent request: agent must be no longer than 64 characters.");
|
|
907
|
+
return value;
|
|
908
|
+
}
|
|
909
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
910
|
+
throw new Error("Invalid subagent request: agent must be a non-empty string or inline role.");
|
|
911
|
+
}
|
|
912
|
+
const role = requireRecord(value, "inline role");
|
|
913
|
+
for (const field of Object.keys(role)) {
|
|
914
|
+
if (!["name", "description", "access", "tools"].includes(field)) {
|
|
915
|
+
throw new AgentConfigurationError("inline role", field, "unknown role field");
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const name = requiredString(role, "inline role", "name");
|
|
919
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u.test(name)) {
|
|
920
|
+
throw new AgentConfigurationError("inline role", "name", "must use 1-64 letters, numbers, dots, underscores, or hyphens");
|
|
921
|
+
}
|
|
922
|
+
const description = requiredString(role, "inline role", "description");
|
|
923
|
+
if (description.length > 500) throw new AgentConfigurationError("inline role", "description", "must not exceed 500 characters");
|
|
924
|
+
const access = requiredString(role, "inline role", "access");
|
|
925
|
+
if (access !== "read" && access !== "write") {
|
|
926
|
+
throw new AgentConfigurationError("inline role", "access", 'must be "read" or "write"');
|
|
927
|
+
}
|
|
928
|
+
if (!Array.isArray(role.tools) || role.tools.length === 0) {
|
|
929
|
+
throw new AgentConfigurationError("inline role", "tools", "must contain at least one tool");
|
|
930
|
+
}
|
|
931
|
+
const tools = [...new Set(role.tools.map((tool) => {
|
|
932
|
+
if (typeof tool !== "string" || !tool.trim()) {
|
|
933
|
+
throw new AgentConfigurationError("inline role", "tools", "must contain non-empty tool names");
|
|
934
|
+
}
|
|
935
|
+
return tool.trim();
|
|
936
|
+
}))];
|
|
937
|
+
for (const tool of tools) {
|
|
938
|
+
if (!KNOWN_TOOLS.has(tool)) throw new AgentConfigurationError("inline role", "tools", `unknown child tool ${JSON.stringify(tool)}`);
|
|
939
|
+
if (access === "read" && WRITE_TOOLS.has(tool)) {
|
|
940
|
+
throw new AgentConfigurationError("inline role", "tools", `read-only roles cannot use ${tool}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return { name, description, access, tools };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function inlineAgentRole(role: InlineAgentRole): AgentRole {
|
|
947
|
+
return {
|
|
948
|
+
...role,
|
|
949
|
+
prompt: role.description,
|
|
950
|
+
source: "inline",
|
|
951
|
+
filePath: `inline:${role.name}`,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
894
955
|
function parseTaskInputs(value: unknown, field: "tasks" | "chain", limits: Pick<SubagentLimits, "maxTasks" | "taskCharacters">): TaskInput[] {
|
|
895
956
|
if (!Array.isArray(value) || value.length === 0) {
|
|
896
957
|
throw new Error(`Invalid subagent request: ${field} must be a non-empty array.`);
|
|
897
958
|
}
|
|
898
959
|
if (value.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
899
|
-
return value.map((entry, index) => {
|
|
900
|
-
const task = requireRecord(entry, `${field}[${index}]`);
|
|
901
|
-
requireOnlyFields(task, ["agent", "task", "name"], `spawn ${field}`);
|
|
902
|
-
const name = optionalThreadName(task);
|
|
903
|
-
return {
|
|
904
|
-
agent:
|
|
905
|
-
task: requireTextField(task, "task", limits.taskCharacters),
|
|
906
|
-
...(name === undefined ? {} : { name }),
|
|
907
|
-
};
|
|
908
|
-
});
|
|
960
|
+
return value.map((entry, index) => {
|
|
961
|
+
const task = requireRecord(entry, `${field}[${index}]`);
|
|
962
|
+
requireOnlyFields(task, ["agent", "task", "name"], `spawn ${field}`);
|
|
963
|
+
const name = optionalThreadName(task);
|
|
964
|
+
return {
|
|
965
|
+
agent: parseAgentSpec(task.agent),
|
|
966
|
+
task: requireTextField(task, "task", limits.taskCharacters),
|
|
967
|
+
...(name === undefined ? {} : { name }),
|
|
968
|
+
};
|
|
969
|
+
});
|
|
909
970
|
}
|
|
910
971
|
|
|
911
972
|
function parseSpawnOptions(record: Record<string, unknown>): SpawnOptions {
|
|
@@ -929,12 +990,12 @@ export function normalizeSubagentRequest(
|
|
|
929
990
|
const record = requireRecord(value, "subagent request");
|
|
930
991
|
const action = record.action === undefined ? SUBAGENT_ACTION.spawn : requireAction(record.action);
|
|
931
992
|
|
|
932
|
-
if (action !== "steer" && Object.hasOwn(record, "message")) {
|
|
993
|
+
if (action !== "steer" && action !== "spawn" && Object.hasOwn(record, "message")) {
|
|
933
994
|
throw new Error('Invalid subagent request: message is only valid with action "steer". Use {"action":"steer","threadId":"...","message":"..."}.');
|
|
934
995
|
}
|
|
935
996
|
|
|
936
997
|
if (action === "spawn") {
|
|
937
|
-
const hasSingle = Object.hasOwn(record, "agent") || Object.hasOwn(record, "task");
|
|
998
|
+
const hasSingle = Object.hasOwn(record, "agent") || Object.hasOwn(record, "task") || Object.hasOwn(record, "message");
|
|
938
999
|
const hasParallel = Object.hasOwn(record, "tasks");
|
|
939
1000
|
const hasChain = Object.hasOwn(record, "chain");
|
|
940
1001
|
if (Number(hasSingle) + Number(hasParallel) + Number(hasChain) !== 1) {
|
|
@@ -945,18 +1006,21 @@ export function normalizeSubagentRequest(
|
|
|
945
1006
|
}
|
|
946
1007
|
const actionField = Object.hasOwn(record, "action") ? { action: "spawn" as const } : {};
|
|
947
1008
|
const options = parseSpawnOptions(record);
|
|
948
|
-
if (hasSingle) {
|
|
949
|
-
requireOnlyFields(record, ["action", "agent", "task", "name", ...SPAWN_OPTION_FIELDS], "spawn single");
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
...
|
|
958
|
-
|
|
959
|
-
|
|
1009
|
+
if (hasSingle) {
|
|
1010
|
+
requireOnlyFields(record, ["action", "agent", "task", "message", "name", ...SPAWN_OPTION_FIELDS], "spawn single");
|
|
1011
|
+
if (Object.hasOwn(record, "task") && Object.hasOwn(record, "message")) {
|
|
1012
|
+
throw new Error("Invalid subagent request: spawn single cannot combine task with its message alias.");
|
|
1013
|
+
}
|
|
1014
|
+
const name = optionalThreadName(record);
|
|
1015
|
+
return {
|
|
1016
|
+
kind: "spawn-single",
|
|
1017
|
+
input: {
|
|
1018
|
+
...actionField,
|
|
1019
|
+
agent: parseAgentSpec(record.agent),
|
|
1020
|
+
task: requireTextField(record, Object.hasOwn(record, "message") ? "message" : "task", limits.taskCharacters),
|
|
1021
|
+
...(name === undefined ? {} : { name }),
|
|
1022
|
+
...options,
|
|
1023
|
+
},
|
|
960
1024
|
};
|
|
961
1025
|
}
|
|
962
1026
|
if (hasParallel) {
|
|
@@ -994,7 +1058,7 @@ export function normalizeSubagentRequest(
|
|
|
994
1058
|
const input = { action, threadId: requireTextField(record, "threadId", 128) };
|
|
995
1059
|
return { kind: action, input } as NormalizedSubagentRequest;
|
|
996
1060
|
}
|
|
997
|
-
if (action === "steer") {
|
|
1061
|
+
if (action === "steer") {
|
|
998
1062
|
requireOnlyFields(record, ["action", "threadId", "message"], action);
|
|
999
1063
|
return {
|
|
1000
1064
|
kind: "steer",
|
|
@@ -1004,43 +1068,43 @@ export function normalizeSubagentRequest(
|
|
|
1004
1068
|
message: requireTextField(record, "message", 4_000),
|
|
1005
1069
|
},
|
|
1006
1070
|
};
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (action === "wait") {
|
|
1010
|
-
requireOnlyFields(record, ["action", "threadId", "all", "timeoutMs"], action);
|
|
1011
|
-
const hasThreadId = Object.hasOwn(record, "threadId");
|
|
1012
|
-
const hasAll = Object.hasOwn(record, "all");
|
|
1013
|
-
if (hasThreadId && hasAll || hasAll && record.all !== true) {
|
|
1014
|
-
throw new Error('Invalid subagent request: action "wait" cannot combine threadId with all: true.');
|
|
1015
|
-
}
|
|
1016
|
-
const timeoutValue = record.timeoutMs === undefined ? DEFAULT_WAIT_TIMEOUT_MS : record.timeoutMs;
|
|
1017
|
-
if (typeof timeoutValue !== "number" || !Number.isSafeInteger(timeoutValue) || timeoutValue <= 0 || timeoutValue > MAX_WAIT_TIMEOUT_MS) {
|
|
1018
|
-
throw new Error(`Invalid subagent request: timeoutMs must be a positive integer no greater than ${MAX_WAIT_TIMEOUT_MS}.`);
|
|
1019
|
-
}
|
|
1020
|
-
return {
|
|
1021
|
-
kind: "wait",
|
|
1022
|
-
input: {
|
|
1023
|
-
action,
|
|
1024
|
-
...(hasThreadId ? { threadId: requireTextField(record, "threadId", 128) } : {}),
|
|
1025
|
-
...(hasAll || !hasThreadId ? { all: true as const } : {}),
|
|
1026
|
-
timeoutMs: timeoutValue,
|
|
1027
|
-
},
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (action === "resume") {
|
|
1032
|
-
requireOnlyFields(record, ["action", "threadId", "task"], action);
|
|
1033
|
-
return {
|
|
1034
|
-
kind: "resume",
|
|
1035
|
-
input: {
|
|
1036
|
-
action,
|
|
1037
|
-
threadId: requireTextField(record, "threadId", 128),
|
|
1038
|
-
...(Object.hasOwn(record, "task") ? { task: requireTextField(record, "task", limits.taskCharacters) } : {}),
|
|
1039
|
-
},
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
requireOnlyFields(record, ["action", "threadId", "all"], action);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (action === "wait") {
|
|
1074
|
+
requireOnlyFields(record, ["action", "threadId", "all", "timeoutMs"], action);
|
|
1075
|
+
const hasThreadId = Object.hasOwn(record, "threadId");
|
|
1076
|
+
const hasAll = Object.hasOwn(record, "all");
|
|
1077
|
+
if (hasThreadId && hasAll || hasAll && record.all !== true) {
|
|
1078
|
+
throw new Error('Invalid subagent request: action "wait" cannot combine threadId with all: true.');
|
|
1079
|
+
}
|
|
1080
|
+
const timeoutValue = record.timeoutMs === undefined ? DEFAULT_WAIT_TIMEOUT_MS : record.timeoutMs;
|
|
1081
|
+
if (typeof timeoutValue !== "number" || !Number.isSafeInteger(timeoutValue) || timeoutValue <= 0 || timeoutValue > MAX_WAIT_TIMEOUT_MS) {
|
|
1082
|
+
throw new Error(`Invalid subagent request: timeoutMs must be a positive integer no greater than ${MAX_WAIT_TIMEOUT_MS}.`);
|
|
1083
|
+
}
|
|
1084
|
+
return {
|
|
1085
|
+
kind: "wait",
|
|
1086
|
+
input: {
|
|
1087
|
+
action,
|
|
1088
|
+
...(hasThreadId ? { threadId: requireTextField(record, "threadId", 128) } : {}),
|
|
1089
|
+
...(hasAll || !hasThreadId ? { all: true as const } : {}),
|
|
1090
|
+
timeoutMs: timeoutValue,
|
|
1091
|
+
},
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (action === "resume") {
|
|
1096
|
+
requireOnlyFields(record, ["action", "threadId", "task"], action);
|
|
1097
|
+
return {
|
|
1098
|
+
kind: "resume",
|
|
1099
|
+
input: {
|
|
1100
|
+
action,
|
|
1101
|
+
threadId: requireTextField(record, "threadId", 128),
|
|
1102
|
+
...(Object.hasOwn(record, "task") ? { task: requireTextField(record, "task", limits.taskCharacters) } : {}),
|
|
1103
|
+
},
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
requireOnlyFields(record, ["action", "threadId", "all"], action);
|
|
1044
1108
|
const hasThreadId = Object.hasOwn(record, "threadId");
|
|
1045
1109
|
const hasAll = Object.hasOwn(record, "all");
|
|
1046
1110
|
if (Number(hasThreadId) + Number(hasAll) !== 1 || hasAll && record.all !== true) {
|
|
@@ -1077,25 +1141,35 @@ function prepareSubagentRequest(
|
|
|
1077
1141
|
}
|
|
1078
1142
|
|
|
1079
1143
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1144
|
+
const inlineAgentSchema = Type.Object({
|
|
1145
|
+
name: Type.String({ minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$" }),
|
|
1146
|
+
description: Type.String({ minLength: 1, maxLength: 500 }),
|
|
1147
|
+
access: StringEnum(["read", "write"] as const),
|
|
1148
|
+
tools: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, uniqueItems: true }),
|
|
1149
|
+
}, { additionalProperties: false, description: "Ephemeral role for this spawn only; tools must be active for the parent" });
|
|
1150
|
+
const agentSchema = Type.Union([
|
|
1151
|
+
Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
1152
|
+
inlineAgentSchema,
|
|
1153
|
+
]);
|
|
1154
|
+
const taskSchema = Type.Object({
|
|
1155
|
+
agent: agentSchema,
|
|
1156
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task for the role" }),
|
|
1157
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1158
|
+
}, { additionalProperties: false });
|
|
1159
|
+
const chainTaskSchema = Type.Object({
|
|
1160
|
+
agent: agentSchema,
|
|
1161
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous} handoff placeholder" }),
|
|
1162
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1163
|
+
}, { additionalProperties: false });
|
|
1090
1164
|
const threadId = Type.String({ minLength: 1, maxLength: 128, description: "Existing child thread ID; omit when spawning because KillerOS creates it" });
|
|
1091
1165
|
return Type.Object({
|
|
1092
|
-
action: Type.Optional(StringEnum(SUBAGENT_ACTIONS, { default: SUBAGENT_ACTION.spawn, description: "Spawn, list, inspect, steer, interrupt, collect, wait, resume, or close. Omit threadId when spawning" })),
|
|
1093
|
-
threadId: Type.Optional(threadId),
|
|
1094
|
-
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1095
|
-
message: Type.Optional(Type.String({ minLength: 1, maxLength: 4_000, description: "
|
|
1096
|
-
all: Type.Optional(Type.Literal(true, { description: "Target every active or queued child thread for interrupt or wait" })),
|
|
1097
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_WAIT_TIMEOUT_MS, description: "Wait timeout in milliseconds; defaults to 30 seconds" })),
|
|
1098
|
-
agent: Type.Optional(
|
|
1166
|
+
action: Type.Optional(StringEnum(SUBAGENT_ACTIONS, { default: SUBAGENT_ACTION.spawn, description: "Spawn, list, inspect, steer, interrupt, collect, wait, resume, or close. Omit threadId when spawning" })),
|
|
1167
|
+
threadId: Type.Optional(threadId),
|
|
1168
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1169
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: Math.max(4_000, limits.taskCharacters), description: `Spawn task alias up to ${limits.taskCharacters.toLocaleString("en-US")} characters; steer message up to 4,000 characters` })),
|
|
1170
|
+
all: Type.Optional(Type.Literal(true, { description: "Target every active or queued child thread for interrupt or wait" })),
|
|
1171
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_WAIT_TIMEOUT_MS, description: "Wait timeout in milliseconds; defaults to 30 seconds" })),
|
|
1172
|
+
agent: Type.Optional(agentSchema),
|
|
1099
1173
|
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
1100
1174
|
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only batches run concurrently up to ${limits.maxReadConcurrency}; batches with writers use one shared slot by default. Set writerConcurrency to opt into a larger shared pool; concurrent writers share the parent worktree, so callers must prove path ownership` })),
|
|
1101
1175
|
writerConcurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: limits.maxTasks, description: `Optional shared-pool cap for parallel tasks that include writers. Defaults to 1 when writers are selected; values above 1 opt into concurrent shared-worktree writes. Concurrent writers must prove path ownership` })),
|
|
@@ -1111,11 +1185,6 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
1111
1185
|
|
|
1112
1186
|
type ToolUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: SubagentDetails }) => void;
|
|
1113
1187
|
|
|
1114
|
-
function requestedAgents(params: { agent?: string; tasks?: TaskInput[]; chain?: TaskInput[] }): string[] {
|
|
1115
|
-
if (params.agent) return [params.agent];
|
|
1116
|
-
return (params.tasks ?? params.chain ?? []).map((task) => task.agent);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
1188
|
function clipCharacters(text: string, maxCharacters: number, fromEnd = false): string {
|
|
1120
1189
|
const characters = [...text];
|
|
1121
1190
|
if (characters.length <= maxCharacters) return text;
|
|
@@ -1157,22 +1226,22 @@ function expandChainTask(template: string, previous: string, maxCharacters: numb
|
|
|
1157
1226
|
return pieces.join("");
|
|
1158
1227
|
}
|
|
1159
1228
|
|
|
1160
|
-
function buildSteeredTask(task: string, steering: readonly string[], maxCharacters: number): string {
|
|
1161
|
-
const steeringLabel = "\n\nParent steering:\n";
|
|
1162
|
-
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
1163
|
-
const required = [...steeringLabel, ...steeringText].length;
|
|
1164
|
-
const taskText = clipCharacters(task, Math.max(0, maxCharacters - required));
|
|
1165
|
-
return `${taskText}${steeringLabel}${steeringText}`;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
export type TaskInput = { agent:
|
|
1169
|
-
|
|
1170
|
-
function steeredTaskWouldExceedLimit(task: string, steering: readonly string[], maxCharacters: number): boolean {
|
|
1171
|
-
if (!steering.length) return false;
|
|
1172
|
-
return codePointLength(task)
|
|
1173
|
-
+ codePointLength("\n\nParent steering:\n")
|
|
1174
|
-
+ codePointLength(steering.join("\n")) > maxCharacters;
|
|
1175
|
-
}
|
|
1229
|
+
function buildSteeredTask(task: string, steering: readonly string[], maxCharacters: number): string {
|
|
1230
|
+
const steeringLabel = "\n\nParent steering:\n";
|
|
1231
|
+
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
1232
|
+
const required = [...steeringLabel, ...steeringText].length;
|
|
1233
|
+
const taskText = clipCharacters(task, Math.max(0, maxCharacters - required));
|
|
1234
|
+
return `${taskText}${steeringLabel}${steeringText}`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
export type TaskInput = { agent: AgentSpec; task: string; name?: string };
|
|
1238
|
+
|
|
1239
|
+
function steeredTaskWouldExceedLimit(task: string, steering: readonly string[], maxCharacters: number): boolean {
|
|
1240
|
+
if (!steering.length) return false;
|
|
1241
|
+
return codePointLength(task)
|
|
1242
|
+
+ codePointLength("\n\nParent steering:\n")
|
|
1243
|
+
+ codePointLength(steering.join("\n")) > maxCharacters;
|
|
1244
|
+
}
|
|
1176
1245
|
|
|
1177
1246
|
function formatUsage(usage: SubagentUsage): string {
|
|
1178
1247
|
const parts = [`${usage.turns} turn${usage.turns === 1 ? "" : "s"}`, `${usage.totalTokens} tokens`];
|
|
@@ -1210,39 +1279,39 @@ function statusIcon(status: SubagentStatus): string {
|
|
|
1210
1279
|
return "!";
|
|
1211
1280
|
}
|
|
1212
1281
|
|
|
1213
|
-
interface ActiveThreadRuntime {
|
|
1214
|
-
controller: AbortController;
|
|
1215
|
-
handle?: SubagentProcessHandle;
|
|
1216
|
-
handles: Set<SubagentProcessHandle>;
|
|
1217
|
-
task: string;
|
|
1218
|
-
steering: string[];
|
|
1219
|
-
restarting: boolean;
|
|
1220
|
-
traceCount: number;
|
|
1221
|
-
startedAt: number;
|
|
1222
|
-
sessionGeneration: number;
|
|
1223
|
-
aggregate?: SubagentTaskResult;
|
|
1224
|
-
requestedReason?: string;
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
interface ChildSession {
|
|
1228
|
-
id: string;
|
|
1229
|
-
directory: string;
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
interface ThreadMetadata {
|
|
1233
|
-
displayName: string;
|
|
1234
|
-
attempt: number;
|
|
1235
|
-
session: ChildSession;
|
|
1236
|
-
persistentSession: boolean;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
type ThreadSnapshot = SubagentThread & {
|
|
1240
|
-
displayName?: string;
|
|
1241
|
-
attempt?: number;
|
|
1242
|
-
session?: ChildSession;
|
|
1243
|
-
};
|
|
1244
|
-
|
|
1245
|
-
function parentThreadId(ctx: ExtensionContext): string {
|
|
1282
|
+
interface ActiveThreadRuntime {
|
|
1283
|
+
controller: AbortController;
|
|
1284
|
+
handle?: SubagentProcessHandle;
|
|
1285
|
+
handles: Set<SubagentProcessHandle>;
|
|
1286
|
+
task: string;
|
|
1287
|
+
steering: string[];
|
|
1288
|
+
restarting: boolean;
|
|
1289
|
+
traceCount: number;
|
|
1290
|
+
startedAt: number;
|
|
1291
|
+
sessionGeneration: number;
|
|
1292
|
+
aggregate?: SubagentTaskResult;
|
|
1293
|
+
requestedReason?: string;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
interface ChildSession {
|
|
1297
|
+
id: string;
|
|
1298
|
+
directory: string;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
interface ThreadMetadata {
|
|
1302
|
+
displayName: string;
|
|
1303
|
+
attempt: number;
|
|
1304
|
+
session: ChildSession;
|
|
1305
|
+
persistentSession: boolean;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
type ThreadSnapshot = SubagentThread & {
|
|
1309
|
+
displayName?: string;
|
|
1310
|
+
attempt?: number;
|
|
1311
|
+
session?: ChildSession;
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
function parentThreadId(ctx: ExtensionContext): string {
|
|
1246
1315
|
try {
|
|
1247
1316
|
const id = ctx.sessionManager?.getSessionId?.();
|
|
1248
1317
|
if (id) return `main:${id}`;
|
|
@@ -1250,61 +1319,61 @@ function parentThreadId(ctx: ExtensionContext): string {
|
|
|
1250
1319
|
// Test and RPC contexts may not expose a session manager.
|
|
1251
1320
|
}
|
|
1252
1321
|
return "main";
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
function defaultThreadName(role: string, existing: readonly ThreadSnapshot[]): string {
|
|
1256
|
-
const names = new Set(existing.map((thread) => thread.displayName?.toLocaleLowerCase() ?? thread.role.toLocaleLowerCase()));
|
|
1257
|
-
const base = role.length <= 48 ? role : role.slice(0, 48);
|
|
1258
|
-
if (!names.has(base.toLocaleLowerCase()) && THREAD_NAME_RE.test(base)) return base;
|
|
1259
|
-
for (let suffix = 2; suffix < 10_000; suffix += 1) {
|
|
1260
|
-
const suffixText = `-${suffix}`;
|
|
1261
|
-
const candidate = `${base.slice(0, 48 - suffixText.length)}${suffixText}`;
|
|
1262
|
-
if (THREAD_NAME_RE.test(candidate) && !names.has(candidate.toLocaleLowerCase())) return candidate;
|
|
1263
|
-
}
|
|
1264
|
-
throw new Error(`Could not allocate a unique display name for role ${JSON.stringify(role)}`);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function safeSessionId(value: string): string {
|
|
1268
|
-
return value.replace(/[^A-Za-z0-9_.-]/gu, "_");
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
function childSessionPath(ctx: ExtensionContext, threadId: string): ChildSession | undefined {
|
|
1272
|
-
try {
|
|
1273
|
-
const directoryRoot = ctx.sessionManager?.getSessionDir?.();
|
|
1274
|
-
if (typeof directoryRoot !== "string" || !directoryRoot) return undefined;
|
|
1275
|
-
const rawParentId = ctx.sessionManager?.getSessionId?.() ?? parentThreadId(ctx);
|
|
1276
|
-
const id = `killeros-${safeSessionId(threadId)}`;
|
|
1277
|
-
return {
|
|
1278
|
-
id,
|
|
1279
|
-
directory: path.join(directoryRoot, "killeros-subagents", safeSessionId(rawParentId), safeSessionId(threadId)),
|
|
1280
|
-
};
|
|
1281
|
-
} catch {
|
|
1282
|
-
return undefined;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
function threadDisplayName(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): string {
|
|
1287
|
-
return thread.displayName ?? metadata.get(thread.id)?.displayName ?? thread.role;
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function threadAttempt(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): number {
|
|
1291
|
-
return thread.attempt ?? metadata.get(thread.id)?.attempt ?? 1;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function threadSession(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): ChildSession | undefined {
|
|
1295
|
-
return thread.session ?? metadata.get(thread.id)?.session;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function threadView(thread: SubagentThread, metadata: Map<string, ThreadMetadata>): ThreadSnapshot {
|
|
1299
|
-
const view = { ...thread } as ThreadSnapshot;
|
|
1300
|
-
const known = metadata.get(thread.id);
|
|
1301
|
-
if (view.displayName === undefined && known) view.displayName = known.displayName;
|
|
1302
|
-
if (view.attempt === undefined && known) view.attempt = known.attempt;
|
|
1303
|
-
const isPendingSession = view.session?.id === "killeros-pending"
|
|
1304
|
-
|| view.session?.directory === path.join(os.tmpdir(), "killeros-subagent-pending");
|
|
1305
|
-
if ((view.session === undefined || isPendingSession) && known) view.session = { ...known.session };
|
|
1306
|
-
return view;
|
|
1307
|
-
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function defaultThreadName(role: string, existing: readonly ThreadSnapshot[]): string {
|
|
1325
|
+
const names = new Set(existing.map((thread) => thread.displayName?.toLocaleLowerCase() ?? thread.role.toLocaleLowerCase()));
|
|
1326
|
+
const base = role.length <= 48 ? role : role.slice(0, 48);
|
|
1327
|
+
if (!names.has(base.toLocaleLowerCase()) && THREAD_NAME_RE.test(base)) return base;
|
|
1328
|
+
for (let suffix = 2; suffix < 10_000; suffix += 1) {
|
|
1329
|
+
const suffixText = `-${suffix}`;
|
|
1330
|
+
const candidate = `${base.slice(0, 48 - suffixText.length)}${suffixText}`;
|
|
1331
|
+
if (THREAD_NAME_RE.test(candidate) && !names.has(candidate.toLocaleLowerCase())) return candidate;
|
|
1332
|
+
}
|
|
1333
|
+
throw new Error(`Could not allocate a unique display name for role ${JSON.stringify(role)}`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function safeSessionId(value: string): string {
|
|
1337
|
+
return value.replace(/[^A-Za-z0-9_.-]/gu, "_");
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function childSessionPath(ctx: ExtensionContext, threadId: string): ChildSession | undefined {
|
|
1341
|
+
try {
|
|
1342
|
+
const directoryRoot = ctx.sessionManager?.getSessionDir?.();
|
|
1343
|
+
if (typeof directoryRoot !== "string" || !directoryRoot) return undefined;
|
|
1344
|
+
const rawParentId = ctx.sessionManager?.getSessionId?.() ?? parentThreadId(ctx);
|
|
1345
|
+
const id = `killeros-${safeSessionId(threadId)}`;
|
|
1346
|
+
return {
|
|
1347
|
+
id,
|
|
1348
|
+
directory: path.join(directoryRoot, "killeros-subagents", safeSessionId(rawParentId), safeSessionId(threadId)),
|
|
1349
|
+
};
|
|
1350
|
+
} catch {
|
|
1351
|
+
return undefined;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function threadDisplayName(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): string {
|
|
1356
|
+
return thread.displayName ?? metadata.get(thread.id)?.displayName ?? thread.role;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function threadAttempt(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): number {
|
|
1360
|
+
return thread.attempt ?? metadata.get(thread.id)?.attempt ?? 1;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function threadSession(thread: ThreadSnapshot, metadata: Map<string, ThreadMetadata>): ChildSession | undefined {
|
|
1364
|
+
return thread.session ?? metadata.get(thread.id)?.session;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function threadView(thread: SubagentThread, metadata: Map<string, ThreadMetadata>): ThreadSnapshot {
|
|
1368
|
+
const view = { ...thread } as ThreadSnapshot;
|
|
1369
|
+
const known = metadata.get(thread.id);
|
|
1370
|
+
if (view.displayName === undefined && known) view.displayName = known.displayName;
|
|
1371
|
+
if (view.attempt === undefined && known) view.attempt = known.attempt;
|
|
1372
|
+
const isPendingSession = view.session?.id === "killeros-pending"
|
|
1373
|
+
|| view.session?.directory === path.join(os.tmpdir(), "killeros-subagent-pending");
|
|
1374
|
+
if ((view.session === undefined || isPendingSession) && known) view.session = { ...known.session };
|
|
1375
|
+
return view;
|
|
1376
|
+
}
|
|
1308
1377
|
|
|
1309
1378
|
function threadCapabilityBoundary(agent: AgentRole): {
|
|
1310
1379
|
filesystem: "read" | "write";
|
|
@@ -1332,24 +1401,24 @@ function threadUsage(usage: SubagentUsage): SubagentThread["usage"] {
|
|
|
1332
1401
|
};
|
|
1333
1402
|
}
|
|
1334
1403
|
|
|
1335
|
-
function legacyStatus(state: SubagentThreadState): SubagentStatus {
|
|
1404
|
+
function legacyStatus(state: SubagentThreadState): SubagentStatus {
|
|
1336
1405
|
if (state === "active") return "running";
|
|
1337
1406
|
if (state === "done" || state === "closed") return "complete";
|
|
1338
1407
|
if (state === "failed") return "failed";
|
|
1339
|
-
if (state === "stopped") return "cancelled";
|
|
1340
|
-
if (state === "orphaned") return "orphaned";
|
|
1341
|
-
return "queued";
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
function threadResult(thread: SubagentThread, source?: SubagentTaskResult): SubagentTaskResult {
|
|
1345
|
-
if (source) {
|
|
1346
|
-
const result = cloneResult(source);
|
|
1347
|
-
if (thread.state === "queued") result.status = "queued";
|
|
1348
|
-
else if (thread.state === "active") result.status = "running";
|
|
1349
|
-
else if (thread.state === "orphaned") result.status = "orphaned";
|
|
1350
|
-
return result;
|
|
1351
|
-
}
|
|
1352
|
-
return {
|
|
1408
|
+
if (state === "stopped") return "cancelled";
|
|
1409
|
+
if (state === "orphaned") return "orphaned";
|
|
1410
|
+
return "queued";
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function threadResult(thread: SubagentThread, source?: SubagentTaskResult): SubagentTaskResult {
|
|
1414
|
+
if (source) {
|
|
1415
|
+
const result = cloneResult(source);
|
|
1416
|
+
if (thread.state === "queued") result.status = "queued";
|
|
1417
|
+
else if (thread.state === "active") result.status = "running";
|
|
1418
|
+
else if (thread.state === "orphaned") result.status = "orphaned";
|
|
1419
|
+
return result;
|
|
1420
|
+
}
|
|
1421
|
+
return {
|
|
1353
1422
|
...makeQueuedResult(thread.id, thread.role, thread.prompt),
|
|
1354
1423
|
status: legacyStatus(thread.state),
|
|
1355
1424
|
agentSource: "unknown",
|
|
@@ -1365,81 +1434,84 @@ function threadResult(thread: SubagentThread, source?: SubagentTaskResult): Suba
|
|
|
1365
1434
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: thread.usage.costUsd },
|
|
1366
1435
|
turns: thread.usage.turns,
|
|
1367
1436
|
},
|
|
1368
|
-
output: thread.result ?? "",
|
|
1369
|
-
toolCallCount: 0,
|
|
1370
|
-
exitConfirmed: false,
|
|
1371
|
-
terminationReason: thread.stopReason,
|
|
1372
|
-
};
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
function restorePersistedResult(value: unknown, thread: ThreadSnapshot): SubagentTaskResult | undefined {
|
|
1376
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1437
|
+
output: thread.result ?? "",
|
|
1438
|
+
toolCallCount: 0,
|
|
1439
|
+
exitConfirmed: false,
|
|
1440
|
+
terminationReason: thread.stopReason,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function restorePersistedResult(value: unknown, thread: ThreadSnapshot): SubagentTaskResult | undefined {
|
|
1445
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1377
1446
|
const source = value as Record<string, any>;
|
|
1378
1447
|
const statuses = new Set<SubagentStatus>(["queued", "running", "complete", "failed", "cancelled", "limited"]);
|
|
1448
|
+
const agentSources = new Set<SubagentTaskResult["agentSource"]>(["bundled", "personal", "project", "inline", "unknown"]);
|
|
1379
1449
|
if (typeof source.id !== "string" || source.id !== thread.id) return undefined;
|
|
1380
|
-
const status = source.status ?? (thread.state === "done" ? "complete" : thread.state === "failed" ? "failed" : "cancelled");
|
|
1381
|
-
if (typeof status !== "string" || !statuses.has(status as SubagentStatus)) return undefined;
|
|
1382
|
-
if (thread.state === "done" && status !== "complete") return undefined;
|
|
1383
|
-
if (thread.state === "failed" && status !== "failed") return undefined;
|
|
1384
|
-
if (thread.state === "stopped" && !["cancelled", "limited"].includes(status)) return undefined;
|
|
1385
|
-
if (thread.state === "closed") return undefined;
|
|
1386
|
-
const rawOutput = source.output === undefined ? thread.result ?? "" : source.output;
|
|
1387
|
-
if (typeof rawOutput !== "string") return undefined;
|
|
1388
|
-
const output = truncateUtf8(rawOutput, 256 * 1024).text;
|
|
1389
|
-
const usage = source.usage;
|
|
1390
|
-
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return undefined;
|
|
1391
|
-
const usageFields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "turns"];
|
|
1392
|
-
if (usageFields.some((field) => typeof usage[field] !== "number" || !Number.isFinite(usage[field]) || usage[field] < 0)) return undefined;
|
|
1393
|
-
if (!usage.cost || typeof usage.cost !== "object" || Array.isArray(usage.cost)) return undefined;
|
|
1394
|
-
const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
|
|
1395
|
-
if (costFields.some((field) => typeof usage.cost[field] !== "number" || !Number.isFinite(usage.cost[field]) || usage.cost[field] < 0)) return undefined;
|
|
1396
|
-
const outputBytes = source.outputBytes === undefined ? Buffer.byteLength(output, "utf8") : source.outputBytes;
|
|
1397
|
-
const outputTruncatedBytes = source.outputTruncatedBytes === undefined ? 0 : source.outputTruncatedBytes;
|
|
1398
|
-
const durationMs = source.durationMs === undefined ? 0 : source.durationMs;
|
|
1399
|
-
if (![outputBytes, outputTruncatedBytes, durationMs].every((item) => typeof item === "number" && Number.isFinite(item) && item >= 0)) return undefined;
|
|
1400
|
-
const exitCode = source.exitCode === undefined || source.exitCode === null ? null : source.exitCode;
|
|
1401
|
-
if (exitCode !== null && (!Number.isSafeInteger(exitCode) || exitCode < 0)) return undefined;
|
|
1402
|
-
if (source.terminationReason !== undefined && typeof source.terminationReason !== "string") return undefined;
|
|
1450
|
+
const status = source.status ?? (thread.state === "done" ? "complete" : thread.state === "failed" ? "failed" : "cancelled");
|
|
1451
|
+
if (typeof status !== "string" || !statuses.has(status as SubagentStatus)) return undefined;
|
|
1452
|
+
if (thread.state === "done" && status !== "complete") return undefined;
|
|
1453
|
+
if (thread.state === "failed" && status !== "failed") return undefined;
|
|
1454
|
+
if (thread.state === "stopped" && !["cancelled", "limited"].includes(status)) return undefined;
|
|
1455
|
+
if (thread.state === "closed") return undefined;
|
|
1456
|
+
const rawOutput = source.output === undefined ? thread.result ?? "" : source.output;
|
|
1457
|
+
if (typeof rawOutput !== "string") return undefined;
|
|
1458
|
+
const output = truncateUtf8(rawOutput, 256 * 1024).text;
|
|
1459
|
+
const usage = source.usage;
|
|
1460
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return undefined;
|
|
1461
|
+
const usageFields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens", "turns"];
|
|
1462
|
+
if (usageFields.some((field) => typeof usage[field] !== "number" || !Number.isFinite(usage[field]) || usage[field] < 0)) return undefined;
|
|
1463
|
+
if (!usage.cost || typeof usage.cost !== "object" || Array.isArray(usage.cost)) return undefined;
|
|
1464
|
+
const costFields = ["input", "output", "cacheRead", "cacheWrite", "total"];
|
|
1465
|
+
if (costFields.some((field) => typeof usage.cost[field] !== "number" || !Number.isFinite(usage.cost[field]) || usage.cost[field] < 0)) return undefined;
|
|
1466
|
+
const outputBytes = source.outputBytes === undefined ? Buffer.byteLength(output, "utf8") : source.outputBytes;
|
|
1467
|
+
const outputTruncatedBytes = source.outputTruncatedBytes === undefined ? 0 : source.outputTruncatedBytes;
|
|
1468
|
+
const durationMs = source.durationMs === undefined ? 0 : source.durationMs;
|
|
1469
|
+
if (![outputBytes, outputTruncatedBytes, durationMs].every((item) => typeof item === "number" && Number.isFinite(item) && item >= 0)) return undefined;
|
|
1470
|
+
const exitCode = source.exitCode === undefined || source.exitCode === null ? null : source.exitCode;
|
|
1471
|
+
if (exitCode !== null && (!Number.isSafeInteger(exitCode) || exitCode < 0)) return undefined;
|
|
1472
|
+
if (source.terminationReason !== undefined && typeof source.terminationReason !== "string") return undefined;
|
|
1403
1473
|
if (source.errorMessage !== undefined && typeof source.errorMessage !== "string") return undefined;
|
|
1404
1474
|
if (source.exitConfirmed !== undefined && typeof source.exitConfirmed !== "boolean") return undefined;
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
typeof source.
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1475
|
+
if (source.agentSource !== undefined && (typeof source.agentSource !== "string" || !agentSources.has(source.agentSource as SubagentTaskResult["agentSource"]))) return undefined;
|
|
1476
|
+
const attempt = Number.isSafeInteger(source.attempt) && source.attempt > 0 ? source.attempt : thread.attempt;
|
|
1477
|
+
const result = makeQueuedResult(
|
|
1478
|
+
thread.id,
|
|
1479
|
+
typeof source.agent === "string" && source.agent ? clipCharacters(source.agent, 64) : thread.role,
|
|
1480
|
+
typeof source.task === "string" && source.task ? clipCharacters(source.task, 20_000) : thread.prompt,
|
|
1481
|
+
undefined,
|
|
1482
|
+
typeof source.name === "string" && source.name ? clipCharacters(source.name, 48) : thread.displayName,
|
|
1483
|
+
attempt,
|
|
1484
|
+
);
|
|
1414
1485
|
result.status = status as SubagentStatus;
|
|
1415
|
-
result.
|
|
1416
|
-
result.
|
|
1417
|
-
result.
|
|
1418
|
-
result.
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
result.
|
|
1429
|
-
result.
|
|
1430
|
-
result.
|
|
1431
|
-
result.
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1486
|
+
result.agentSource = source.agentSource ?? "unknown";
|
|
1487
|
+
result.output = output;
|
|
1488
|
+
result.outputBytes = outputBytes;
|
|
1489
|
+
result.outputTruncatedBytes = outputTruncatedBytes;
|
|
1490
|
+
result.usage = {
|
|
1491
|
+
input: usage.input,
|
|
1492
|
+
output: usage.output,
|
|
1493
|
+
cacheRead: usage.cacheRead,
|
|
1494
|
+
cacheWrite: usage.cacheWrite,
|
|
1495
|
+
totalTokens: usage.totalTokens,
|
|
1496
|
+
cost: { input: usage.cost.input, output: usage.cost.output, cacheRead: usage.cost.cacheRead, cacheWrite: usage.cost.cacheWrite, total: usage.cost.total },
|
|
1497
|
+
turns: usage.turns,
|
|
1498
|
+
};
|
|
1499
|
+
result.terminationReason = source.terminationReason;
|
|
1500
|
+
result.errorMessage = source.errorMessage;
|
|
1501
|
+
result.durationMs = durationMs;
|
|
1502
|
+
result.exitCode = exitCode;
|
|
1503
|
+
result.exitConfirmed = source.exitConfirmed === true;
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function threadBoardRecord(result: SubagentTaskResult): ThreadBoardRecord {
|
|
1508
|
+
return {
|
|
1509
|
+
id: result.id,
|
|
1510
|
+
displayName: result.name,
|
|
1511
|
+
attempt: result.attempt,
|
|
1512
|
+
agent: result.agent,
|
|
1441
1513
|
task: result.task,
|
|
1442
|
-
status: result.status as ThreadBoardRecord["status"],
|
|
1514
|
+
status: result.status as ThreadBoardRecord["status"],
|
|
1443
1515
|
usage: {
|
|
1444
1516
|
input: result.usage.input,
|
|
1445
1517
|
output: result.usage.output,
|
|
@@ -1460,244 +1532,245 @@ function threadBoardRecord(result: SubagentTaskResult): ThreadBoardRecord {
|
|
|
1460
1532
|
};
|
|
1461
1533
|
}
|
|
1462
1534
|
|
|
1463
|
-
export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeOptions = {}): SubagentControlApi {
|
|
1464
|
-
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
1465
|
-
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1466
|
-
let threads = new SubagentThreadRegistry();
|
|
1467
|
-
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
1468
|
-
const threadMetadata = new Map<string, ThreadMetadata>();
|
|
1469
|
-
const threadResources = new Map<string, { directory: string; persistent: boolean; handles: Set<SubagentProcessHandle> }>();
|
|
1470
|
-
const backgroundBatches = new Set<Promise<unknown>>();
|
|
1471
|
-
const savedResults = new Map<string, SubagentTaskResult>();
|
|
1472
|
-
const evictedThreadParents = new Map<string, string | undefined>();
|
|
1473
|
-
let sessionGeneration = 0;
|
|
1474
|
-
let persistenceWarning: string | undefined;
|
|
1475
|
-
|
|
1476
|
-
const persistText = (value: string | undefined, maxBytes: number): string | undefined => {
|
|
1477
|
-
if (value === undefined) return undefined;
|
|
1478
|
-
return truncateUtf8(value, maxBytes).text;
|
|
1479
|
-
};
|
|
1480
|
-
const persistenceAppend = (record: Record<string, unknown>): void => {
|
|
1481
|
-
const appendEntry = (pi as unknown as { appendEntry?: (type: string, data: unknown) => void }).appendEntry;
|
|
1482
|
-
if (!appendEntry) return;
|
|
1483
|
-
try {
|
|
1484
|
-
appendEntry.call(pi, SUBAGENT_PERSISTENCE_TYPE, record);
|
|
1485
|
-
} catch (error) {
|
|
1486
|
-
persistenceWarning ??= `Subagent persistence is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1487
|
-
}
|
|
1488
|
-
};
|
|
1489
|
-
const persistedThread = (thread: ThreadSnapshot): Record<string, unknown> => {
|
|
1490
|
-
const session = threadSession(thread, threadMetadata) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
1491
|
-
return {
|
|
1492
|
-
id: thread.id,
|
|
1493
|
-
parentId: thread.parentId,
|
|
1494
|
-
displayName: threadDisplayName(thread, threadMetadata),
|
|
1495
|
-
attempt: threadAttempt(thread, threadMetadata),
|
|
1496
|
-
role: thread.role,
|
|
1497
|
-
prompt: clipCharacters(thread.prompt, limits.taskCharacters),
|
|
1498
|
-
model: thread.model,
|
|
1499
|
-
tools: thread.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1500
|
-
capabilityBoundary: { ...thread.capabilityBoundary },
|
|
1501
|
-
session: { ...session },
|
|
1502
|
-
state: thread.state,
|
|
1503
|
-
usage: { ...thread.usage },
|
|
1504
|
-
handoff: thread.handoff
|
|
1505
|
-
? { ...thread.handoff, summary: persistText(thread.handoff.summary, 256 * 1024) }
|
|
1506
|
-
: undefined,
|
|
1507
|
-
result: persistText(thread.result, 256 * 1024),
|
|
1508
|
-
failure: thread.failure ? { ...thread.failure, message: clipCharacters(thread.failure.message, 512) } : undefined,
|
|
1509
|
-
stopReason: thread.stopReason,
|
|
1510
|
-
evicted: thread.evicted,
|
|
1511
|
-
timestamps: { ...thread.timestamps },
|
|
1512
|
-
version: thread.version,
|
|
1513
|
-
trace: thread.trace.slice(-64).map((entry) => ({
|
|
1514
|
-
...entry,
|
|
1515
|
-
message: persistText(entry.message, 64 * 1024),
|
|
1516
|
-
})),
|
|
1517
|
-
steering: thread.steering.slice(-20).map((entry) => ({ ...entry, message: clipCharacters(entry.message, 4_000) })),
|
|
1518
|
-
};
|
|
1519
|
-
};
|
|
1520
|
-
const recordSpawn = (thread: SubagentThread): void => {
|
|
1521
|
-
const view = threadView(thread, threadMetadata);
|
|
1522
|
-
persistenceAppend({ version: 1, event: "spawn", parentId: view.parentId, thread: persistedThread(view) });
|
|
1523
|
-
};
|
|
1524
|
-
const recordSnapshot = (thread: SubagentThread, result?: SubagentTaskResult): void => {
|
|
1525
|
-
const view = threadView(thread, threadMetadata);
|
|
1526
|
-
persistenceAppend({
|
|
1527
|
-
version: 1,
|
|
1528
|
-
event: "snapshot",
|
|
1529
|
-
parentId: view.parentId,
|
|
1530
|
-
id: view.id,
|
|
1531
|
-
thread: persistedThread(view),
|
|
1532
|
-
...(result ? {
|
|
1533
|
-
result: {
|
|
1534
|
-
id: result.id,
|
|
1535
|
-
name: result.name,
|
|
1535
|
+
export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeOptions = {}): SubagentControlApi {
|
|
1536
|
+
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
1537
|
+
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1538
|
+
let threads = new SubagentThreadRegistry();
|
|
1539
|
+
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
1540
|
+
const threadMetadata = new Map<string, ThreadMetadata>();
|
|
1541
|
+
const threadResources = new Map<string, { directory: string; persistent: boolean; handles: Set<SubagentProcessHandle> }>();
|
|
1542
|
+
const backgroundBatches = new Set<Promise<unknown>>();
|
|
1543
|
+
const savedResults = new Map<string, SubagentTaskResult>();
|
|
1544
|
+
const evictedThreadParents = new Map<string, string | undefined>();
|
|
1545
|
+
let sessionGeneration = 0;
|
|
1546
|
+
let persistenceWarning: string | undefined;
|
|
1547
|
+
|
|
1548
|
+
const persistText = (value: string | undefined, maxBytes: number): string | undefined => {
|
|
1549
|
+
if (value === undefined) return undefined;
|
|
1550
|
+
return truncateUtf8(value, maxBytes).text;
|
|
1551
|
+
};
|
|
1552
|
+
const persistenceAppend = (record: Record<string, unknown>): void => {
|
|
1553
|
+
const appendEntry = (pi as unknown as { appendEntry?: (type: string, data: unknown) => void }).appendEntry;
|
|
1554
|
+
if (!appendEntry) return;
|
|
1555
|
+
try {
|
|
1556
|
+
appendEntry.call(pi, SUBAGENT_PERSISTENCE_TYPE, record);
|
|
1557
|
+
} catch (error) {
|
|
1558
|
+
persistenceWarning ??= `Subagent persistence is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const persistedThread = (thread: ThreadSnapshot): Record<string, unknown> => {
|
|
1562
|
+
const session = threadSession(thread, threadMetadata) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
1563
|
+
return {
|
|
1564
|
+
id: thread.id,
|
|
1565
|
+
parentId: thread.parentId,
|
|
1566
|
+
displayName: threadDisplayName(thread, threadMetadata),
|
|
1567
|
+
attempt: threadAttempt(thread, threadMetadata),
|
|
1568
|
+
role: thread.role,
|
|
1569
|
+
prompt: clipCharacters(thread.prompt, limits.taskCharacters),
|
|
1570
|
+
model: thread.model,
|
|
1571
|
+
tools: thread.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1572
|
+
capabilityBoundary: { ...thread.capabilityBoundary },
|
|
1573
|
+
session: { ...session },
|
|
1574
|
+
state: thread.state,
|
|
1575
|
+
usage: { ...thread.usage },
|
|
1576
|
+
handoff: thread.handoff
|
|
1577
|
+
? { ...thread.handoff, summary: persistText(thread.handoff.summary, 256 * 1024) }
|
|
1578
|
+
: undefined,
|
|
1579
|
+
result: persistText(thread.result, 256 * 1024),
|
|
1580
|
+
failure: thread.failure ? { ...thread.failure, message: clipCharacters(thread.failure.message, 512) } : undefined,
|
|
1581
|
+
stopReason: thread.stopReason,
|
|
1582
|
+
evicted: thread.evicted,
|
|
1583
|
+
timestamps: { ...thread.timestamps },
|
|
1584
|
+
version: thread.version,
|
|
1585
|
+
trace: thread.trace.slice(-64).map((entry) => ({
|
|
1586
|
+
...entry,
|
|
1587
|
+
message: persistText(entry.message, 64 * 1024),
|
|
1588
|
+
})),
|
|
1589
|
+
steering: thread.steering.slice(-20).map((entry) => ({ ...entry, message: clipCharacters(entry.message, 4_000) })),
|
|
1590
|
+
};
|
|
1591
|
+
};
|
|
1592
|
+
const recordSpawn = (thread: SubagentThread): void => {
|
|
1593
|
+
const view = threadView(thread, threadMetadata);
|
|
1594
|
+
persistenceAppend({ version: 1, event: "spawn", parentId: view.parentId, thread: persistedThread(view) });
|
|
1595
|
+
};
|
|
1596
|
+
const recordSnapshot = (thread: SubagentThread, result?: SubagentTaskResult): void => {
|
|
1597
|
+
const view = threadView(thread, threadMetadata);
|
|
1598
|
+
persistenceAppend({
|
|
1599
|
+
version: 1,
|
|
1600
|
+
event: "snapshot",
|
|
1601
|
+
parentId: view.parentId,
|
|
1602
|
+
id: view.id,
|
|
1603
|
+
thread: persistedThread(view),
|
|
1604
|
+
...(result ? {
|
|
1605
|
+
result: {
|
|
1606
|
+
id: result.id,
|
|
1607
|
+
name: result.name,
|
|
1536
1608
|
agent: result.agent,
|
|
1609
|
+
agentSource: result.agentSource,
|
|
1537
1610
|
task: clipCharacters(result.task, limits.taskCharacters),
|
|
1538
|
-
status: result.status,
|
|
1539
|
-
output: persistText(result.output, 256 * 1024),
|
|
1540
|
-
outputBytes: result.outputBytes,
|
|
1541
|
-
outputTruncatedBytes: result.outputTruncatedBytes,
|
|
1542
|
-
usage: result.usage,
|
|
1543
|
-
terminationReason: result.terminationReason,
|
|
1544
|
-
errorMessage: persistText(result.errorMessage, 8 * 1024),
|
|
1545
|
-
durationMs: result.durationMs,
|
|
1546
|
-
exitCode: result.exitCode,
|
|
1547
|
-
exitConfirmed: result.exitConfirmed,
|
|
1548
|
-
attempt: result.attempt,
|
|
1549
|
-
},
|
|
1550
|
-
} : {}),
|
|
1551
|
-
});
|
|
1552
|
-
};
|
|
1553
|
-
const recordClose = (thread: SubagentThread): void => {
|
|
1554
|
-
const view = threadView(thread, threadMetadata);
|
|
1555
|
-
persistenceAppend({ version: 1, event: "close", parentId: view.parentId, id: view.id, closedAt: view.timestamps.closedAt ?? Date.now() });
|
|
1556
|
-
};
|
|
1557
|
-
let unsubscribePersistence = (): void => {};
|
|
1558
|
-
const attachPersistence = (): void => {
|
|
1559
|
-
unsubscribePersistence = threads.subscribe((change) => {
|
|
1560
|
-
if (["complete", "fail", "stop", "interrupt", "resume"].includes(change.type)) {
|
|
1561
|
-
recordSnapshot(change.thread, savedResults.get(change.thread.id));
|
|
1562
|
-
} else if (change.type === "close") {
|
|
1563
|
-
recordClose(change.thread);
|
|
1564
|
-
}
|
|
1565
|
-
});
|
|
1566
|
-
};
|
|
1567
|
-
|
|
1568
|
-
const restoreRecords = (
|
|
1569
|
-
entries: readonly unknown[],
|
|
1570
|
-
parentId: string,
|
|
1571
|
-
expectedSession?: (threadId: string) => ChildSession | undefined,
|
|
1572
|
-
): Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }> => {
|
|
1573
|
-
const records = new Map<string, { thread: ThreadSnapshot; result?: SubagentTaskResult }>();
|
|
1574
|
-
for (const entry of entries) {
|
|
1575
|
-
try {
|
|
1576
|
-
if (!entry || typeof entry !== "object") continue;
|
|
1577
|
-
const candidate = entry as Record<string, unknown>;
|
|
1578
|
-
if (candidate.type !== "custom" || candidate.customType !== SUBAGENT_PERSISTENCE_TYPE) continue;
|
|
1579
|
-
const data = candidate.data;
|
|
1580
|
-
if (!data || typeof data !== "object") continue;
|
|
1581
|
-
const record = data as Record<string, any>;
|
|
1582
|
-
if (record.version !== 1 || typeof record.parentId !== "string" || record.parentId !== parentId) continue;
|
|
1583
|
-
if (record.event === "spawn" || record.event === "snapshot") {
|
|
1584
|
-
const rawThread = record.thread;
|
|
1585
|
-
if (!rawThread || typeof rawThread !== "object" || typeof rawThread.id !== "string" || rawThread.parentId !== parentId) continue;
|
|
1586
|
-
if (typeof rawThread.role !== "string" || typeof rawThread.prompt !== "string" || typeof rawThread.model !== "string") continue;
|
|
1587
|
-
const thread = { ...rawThread } as ThreadSnapshot;
|
|
1588
|
-
thread.prompt = clipCharacters(thread.prompt, limits.taskCharacters);
|
|
1589
|
-
thread.result = typeof rawThread.result === "string" ? persistText(rawThread.result, 256 * 1024) : rawThread.result;
|
|
1590
|
-
thread.tools = Array.isArray(rawThread.tools) ? rawThread.tools.slice(0, 32) : [];
|
|
1591
|
-
thread.trace = Array.isArray(rawThread.trace) ? rawThread.trace.slice(-64) : [];
|
|
1592
|
-
thread.steering = Array.isArray(rawThread.steering) ? rawThread.steering.slice(-20) : [];
|
|
1593
|
-
thread.displayName = typeof rawThread.displayName === "string" ? rawThread.displayName : rawThread.role;
|
|
1594
|
-
thread.attempt = Number.isSafeInteger(rawThread.attempt) && rawThread.attempt > 0 ? rawThread.attempt : 1;
|
|
1595
|
-
const trustedSession = expectedSession?.(thread.id);
|
|
1596
|
-
const rawSession = rawThread.session && typeof rawThread.session === "object" ? rawThread.session : undefined;
|
|
1597
|
-
if (trustedSession && rawSession
|
|
1598
|
-
&& (String(rawSession.id ?? trustedSession.id) !== trustedSession.id
|
|
1599
|
-
|| String(rawSession.directory ?? trustedSession.directory) !== trustedSession.directory)) continue;
|
|
1600
|
-
thread.session = trustedSession ?? {
|
|
1601
|
-
id: `killeros-${safeSessionId(thread.id)}`,
|
|
1602
|
-
directory: "",
|
|
1603
|
-
};
|
|
1604
|
-
if (thread.state === "queued" || thread.state === "active") {
|
|
1605
|
-
thread.state = "orphaned" as SubagentThreadState;
|
|
1606
|
-
thread.stopReason = "parent_restarted";
|
|
1607
|
-
}
|
|
1608
|
-
const result = record.result === undefined ? undefined : restorePersistedResult(record.result, thread);
|
|
1609
|
-
if (record.result !== undefined && !result) continue;
|
|
1610
|
-
records.set(thread.id, { thread, result });
|
|
1611
|
-
} else if (record.event === "close" && typeof record.id === "string") {
|
|
1612
|
-
const previous = records.get(record.id);
|
|
1613
|
-
if (!previous) continue;
|
|
1614
|
-
if (typeof record.closedAt !== "number" || !Number.isFinite(record.closedAt) || record.closedAt < 0) continue;
|
|
1615
|
-
previous.thread.state = "closed";
|
|
1616
|
-
previous.thread.evicted = true;
|
|
1617
|
-
previous.thread.prompt = "[closed thread prompt evicted]";
|
|
1618
|
-
previous.thread.result = undefined;
|
|
1619
|
-
previous.thread.trace = [];
|
|
1620
|
-
previous.thread.steering = [];
|
|
1621
|
-
previous.thread.handoff = undefined;
|
|
1622
|
-
previous.thread.timestamps = { ...previous.thread.timestamps, closedAt: record.closedAt };
|
|
1623
|
-
previous.result = undefined;
|
|
1624
|
-
}
|
|
1625
|
-
} catch {
|
|
1626
|
-
// A malformed custom entry must not prevent the parent session from starting.
|
|
1627
|
-
continue;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
return [...records.values()];
|
|
1631
|
-
};
|
|
1632
|
-
|
|
1633
|
-
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }>): void => {
|
|
1634
|
-
if (!restored.length) return;
|
|
1635
|
-
const ids = [...restored.map(({ thread }) => thread.id)];
|
|
1636
|
-
let idIndex = 0;
|
|
1637
|
-
const oldThreads = threads;
|
|
1638
|
-
unsubscribePersistence();
|
|
1639
|
-
threads = new SubagentThreadRegistry({ createId: () => ids[idIndex++] ?? `subagent-${Date.now()}` });
|
|
1640
|
-
attachPersistence();
|
|
1641
|
-
for (const entry of restored) {
|
|
1642
|
-
try {
|
|
1643
|
-
const thread = entry.thread;
|
|
1644
|
-
const created = (threads as any).hydrate
|
|
1645
|
-
? (threads as any).hydrate(thread)
|
|
1646
|
-
: threads.spawn({
|
|
1647
|
-
parentId: thread.parentId as SubagentThreadId,
|
|
1648
|
-
role: thread.role,
|
|
1649
|
-
prompt: thread.prompt,
|
|
1650
|
-
model: thread.model,
|
|
1651
|
-
tools: thread.tools,
|
|
1652
|
-
capabilityBoundary: thread.capabilityBoundary,
|
|
1653
|
-
displayName: thread.displayName,
|
|
1654
|
-
attempt: thread.attempt,
|
|
1655
|
-
session: thread.session,
|
|
1656
|
-
} as any);
|
|
1657
|
-
threadMetadata.set(created.id, {
|
|
1658
|
-
displayName: thread.displayName ?? thread.role,
|
|
1659
|
-
attempt: thread.attempt ?? 1,
|
|
1660
|
-
session: thread.session ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" },
|
|
1661
|
-
persistentSession: Boolean(thread.session?.directory),
|
|
1662
|
-
});
|
|
1663
|
-
if (entry.result) saveResult(created.id, entry.result);
|
|
1664
|
-
if (!(threads as any).hydrate) {
|
|
1665
|
-
if (thread.state === "done") {
|
|
1666
|
-
threads.begin(created.id);
|
|
1667
|
-
threads.complete(created.id, { result: thread.result ?? entry.result?.output });
|
|
1668
|
-
} else if (thread.state === "failed") {
|
|
1669
|
-
threads.begin(created.id);
|
|
1670
|
-
threads.fail(created.id, { message: thread.failure?.message ?? entry.result?.errorMessage ?? "restored failure" });
|
|
1671
|
-
} else if (thread.state === "stopped" || thread.state === "orphaned") {
|
|
1672
|
-
threads.stop(created.id, { reason: thread.stopReason ?? "parent_restarted" });
|
|
1673
|
-
}
|
|
1674
|
-
if (thread.state === "closed") {
|
|
1675
|
-
if (threads.inspect(created.id)?.state === "queued") threads.begin(created.id);
|
|
1676
|
-
if (threads.inspect(created.id)?.state === "active") threads.stop(created.id, { reason: thread.stopReason ?? "closed" });
|
|
1677
|
-
threads.close(created.id);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
} catch {
|
|
1681
|
-
// Skip malformed or conflicting records and keep the rest of the board usable.
|
|
1682
|
-
continue;
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
if (oldThreads !== threads && !oldThreads.isDisposed) oldThreads.dispose();
|
|
1686
|
-
};
|
|
1687
|
-
|
|
1688
|
-
attachPersistence();
|
|
1689
|
-
const maxClosedThreads = Number.isSafeInteger(limits.threadRetentionRecords) && limits.threadRetentionRecords > 0
|
|
1690
|
-
? limits.threadRetentionRecords
|
|
1691
|
-
: SUBAGENT_LIMITS.threadRetentionRecords;
|
|
1692
|
-
|
|
1693
|
-
const stopActiveRuntimes = (reason: string): void => {
|
|
1694
|
-
for (const runtime of activeRuntimes.values()) {
|
|
1695
|
-
runtime.restarting = false;
|
|
1696
|
-
runtime.requestedReason = reason;
|
|
1697
|
-
runtime.handle?.stop(reason);
|
|
1698
|
-
runtime.controller.abort();
|
|
1699
|
-
}
|
|
1700
|
-
};
|
|
1611
|
+
status: result.status,
|
|
1612
|
+
output: persistText(result.output, 256 * 1024),
|
|
1613
|
+
outputBytes: result.outputBytes,
|
|
1614
|
+
outputTruncatedBytes: result.outputTruncatedBytes,
|
|
1615
|
+
usage: result.usage,
|
|
1616
|
+
terminationReason: result.terminationReason,
|
|
1617
|
+
errorMessage: persistText(result.errorMessage, 8 * 1024),
|
|
1618
|
+
durationMs: result.durationMs,
|
|
1619
|
+
exitCode: result.exitCode,
|
|
1620
|
+
exitConfirmed: result.exitConfirmed,
|
|
1621
|
+
attempt: result.attempt,
|
|
1622
|
+
},
|
|
1623
|
+
} : {}),
|
|
1624
|
+
});
|
|
1625
|
+
};
|
|
1626
|
+
const recordClose = (thread: SubagentThread): void => {
|
|
1627
|
+
const view = threadView(thread, threadMetadata);
|
|
1628
|
+
persistenceAppend({ version: 1, event: "close", parentId: view.parentId, id: view.id, closedAt: view.timestamps.closedAt ?? Date.now() });
|
|
1629
|
+
};
|
|
1630
|
+
let unsubscribePersistence = (): void => {};
|
|
1631
|
+
const attachPersistence = (): void => {
|
|
1632
|
+
unsubscribePersistence = threads.subscribe((change) => {
|
|
1633
|
+
if (["complete", "fail", "stop", "interrupt", "resume"].includes(change.type)) {
|
|
1634
|
+
recordSnapshot(change.thread, savedResults.get(change.thread.id));
|
|
1635
|
+
} else if (change.type === "close") {
|
|
1636
|
+
recordClose(change.thread);
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
const restoreRecords = (
|
|
1642
|
+
entries: readonly unknown[],
|
|
1643
|
+
parentId: string,
|
|
1644
|
+
expectedSession?: (threadId: string) => ChildSession | undefined,
|
|
1645
|
+
): Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }> => {
|
|
1646
|
+
const records = new Map<string, { thread: ThreadSnapshot; result?: SubagentTaskResult }>();
|
|
1647
|
+
for (const entry of entries) {
|
|
1648
|
+
try {
|
|
1649
|
+
if (!entry || typeof entry !== "object") continue;
|
|
1650
|
+
const candidate = entry as Record<string, unknown>;
|
|
1651
|
+
if (candidate.type !== "custom" || candidate.customType !== SUBAGENT_PERSISTENCE_TYPE) continue;
|
|
1652
|
+
const data = candidate.data;
|
|
1653
|
+
if (!data || typeof data !== "object") continue;
|
|
1654
|
+
const record = data as Record<string, any>;
|
|
1655
|
+
if (record.version !== 1 || typeof record.parentId !== "string" || record.parentId !== parentId) continue;
|
|
1656
|
+
if (record.event === "spawn" || record.event === "snapshot") {
|
|
1657
|
+
const rawThread = record.thread;
|
|
1658
|
+
if (!rawThread || typeof rawThread !== "object" || typeof rawThread.id !== "string" || rawThread.parentId !== parentId) continue;
|
|
1659
|
+
if (typeof rawThread.role !== "string" || typeof rawThread.prompt !== "string" || typeof rawThread.model !== "string") continue;
|
|
1660
|
+
const thread = { ...rawThread } as ThreadSnapshot;
|
|
1661
|
+
thread.prompt = clipCharacters(thread.prompt, limits.taskCharacters);
|
|
1662
|
+
thread.result = typeof rawThread.result === "string" ? persistText(rawThread.result, 256 * 1024) : rawThread.result;
|
|
1663
|
+
thread.tools = Array.isArray(rawThread.tools) ? rawThread.tools.slice(0, 32) : [];
|
|
1664
|
+
thread.trace = Array.isArray(rawThread.trace) ? rawThread.trace.slice(-64) : [];
|
|
1665
|
+
thread.steering = Array.isArray(rawThread.steering) ? rawThread.steering.slice(-20) : [];
|
|
1666
|
+
thread.displayName = typeof rawThread.displayName === "string" ? rawThread.displayName : rawThread.role;
|
|
1667
|
+
thread.attempt = Number.isSafeInteger(rawThread.attempt) && rawThread.attempt > 0 ? rawThread.attempt : 1;
|
|
1668
|
+
const trustedSession = expectedSession?.(thread.id);
|
|
1669
|
+
const rawSession = rawThread.session && typeof rawThread.session === "object" ? rawThread.session : undefined;
|
|
1670
|
+
if (trustedSession && rawSession
|
|
1671
|
+
&& (String(rawSession.id ?? trustedSession.id) !== trustedSession.id
|
|
1672
|
+
|| String(rawSession.directory ?? trustedSession.directory) !== trustedSession.directory)) continue;
|
|
1673
|
+
thread.session = trustedSession ?? {
|
|
1674
|
+
id: `killeros-${safeSessionId(thread.id)}`,
|
|
1675
|
+
directory: "",
|
|
1676
|
+
};
|
|
1677
|
+
if (thread.state === "queued" || thread.state === "active") {
|
|
1678
|
+
thread.state = "orphaned" as SubagentThreadState;
|
|
1679
|
+
thread.stopReason = "parent_restarted";
|
|
1680
|
+
}
|
|
1681
|
+
const result = record.result === undefined ? undefined : restorePersistedResult(record.result, thread);
|
|
1682
|
+
if (record.result !== undefined && !result) continue;
|
|
1683
|
+
records.set(thread.id, { thread, result });
|
|
1684
|
+
} else if (record.event === "close" && typeof record.id === "string") {
|
|
1685
|
+
const previous = records.get(record.id);
|
|
1686
|
+
if (!previous) continue;
|
|
1687
|
+
if (typeof record.closedAt !== "number" || !Number.isFinite(record.closedAt) || record.closedAt < 0) continue;
|
|
1688
|
+
previous.thread.state = "closed";
|
|
1689
|
+
previous.thread.evicted = true;
|
|
1690
|
+
previous.thread.prompt = "[closed thread prompt evicted]";
|
|
1691
|
+
previous.thread.result = undefined;
|
|
1692
|
+
previous.thread.trace = [];
|
|
1693
|
+
previous.thread.steering = [];
|
|
1694
|
+
previous.thread.handoff = undefined;
|
|
1695
|
+
previous.thread.timestamps = { ...previous.thread.timestamps, closedAt: record.closedAt };
|
|
1696
|
+
previous.result = undefined;
|
|
1697
|
+
}
|
|
1698
|
+
} catch {
|
|
1699
|
+
// A malformed custom entry must not prevent the parent session from starting.
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
return [...records.values()];
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }>): void => {
|
|
1707
|
+
if (!restored.length) return;
|
|
1708
|
+
const ids = [...restored.map(({ thread }) => thread.id)];
|
|
1709
|
+
let idIndex = 0;
|
|
1710
|
+
const oldThreads = threads;
|
|
1711
|
+
unsubscribePersistence();
|
|
1712
|
+
threads = new SubagentThreadRegistry({ createId: () => ids[idIndex++] ?? `subagent-${Date.now()}` });
|
|
1713
|
+
attachPersistence();
|
|
1714
|
+
for (const entry of restored) {
|
|
1715
|
+
try {
|
|
1716
|
+
const thread = entry.thread;
|
|
1717
|
+
const created = (threads as any).hydrate
|
|
1718
|
+
? (threads as any).hydrate(thread)
|
|
1719
|
+
: threads.spawn({
|
|
1720
|
+
parentId: thread.parentId as SubagentThreadId,
|
|
1721
|
+
role: thread.role,
|
|
1722
|
+
prompt: thread.prompt,
|
|
1723
|
+
model: thread.model,
|
|
1724
|
+
tools: thread.tools,
|
|
1725
|
+
capabilityBoundary: thread.capabilityBoundary,
|
|
1726
|
+
displayName: thread.displayName,
|
|
1727
|
+
attempt: thread.attempt,
|
|
1728
|
+
session: thread.session,
|
|
1729
|
+
} as any);
|
|
1730
|
+
threadMetadata.set(created.id, {
|
|
1731
|
+
displayName: thread.displayName ?? thread.role,
|
|
1732
|
+
attempt: thread.attempt ?? 1,
|
|
1733
|
+
session: thread.session ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" },
|
|
1734
|
+
persistentSession: Boolean(thread.session?.directory),
|
|
1735
|
+
});
|
|
1736
|
+
if (entry.result) saveResult(created.id, entry.result);
|
|
1737
|
+
if (!(threads as any).hydrate) {
|
|
1738
|
+
if (thread.state === "done") {
|
|
1739
|
+
threads.begin(created.id);
|
|
1740
|
+
threads.complete(created.id, { result: thread.result ?? entry.result?.output });
|
|
1741
|
+
} else if (thread.state === "failed") {
|
|
1742
|
+
threads.begin(created.id);
|
|
1743
|
+
threads.fail(created.id, { message: thread.failure?.message ?? entry.result?.errorMessage ?? "restored failure" });
|
|
1744
|
+
} else if (thread.state === "stopped" || thread.state === "orphaned") {
|
|
1745
|
+
threads.stop(created.id, { reason: thread.stopReason ?? "parent_restarted" });
|
|
1746
|
+
}
|
|
1747
|
+
if (thread.state === "closed") {
|
|
1748
|
+
if (threads.inspect(created.id)?.state === "queued") threads.begin(created.id);
|
|
1749
|
+
if (threads.inspect(created.id)?.state === "active") threads.stop(created.id, { reason: thread.stopReason ?? "closed" });
|
|
1750
|
+
threads.close(created.id);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
} catch {
|
|
1754
|
+
// Skip malformed or conflicting records and keep the rest of the board usable.
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
if (oldThreads !== threads && !oldThreads.isDisposed) oldThreads.dispose();
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
attachPersistence();
|
|
1762
|
+
const maxClosedThreads = Number.isSafeInteger(limits.threadRetentionRecords) && limits.threadRetentionRecords > 0
|
|
1763
|
+
? limits.threadRetentionRecords
|
|
1764
|
+
: SUBAGENT_LIMITS.threadRetentionRecords;
|
|
1765
|
+
|
|
1766
|
+
const stopActiveRuntimes = (reason: string): void => {
|
|
1767
|
+
for (const runtime of activeRuntimes.values()) {
|
|
1768
|
+
runtime.restarting = false;
|
|
1769
|
+
runtime.requestedReason = reason;
|
|
1770
|
+
runtime.handle?.stop(reason);
|
|
1771
|
+
runtime.controller.abort();
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1701
1774
|
|
|
1702
1775
|
const rememberEvictedThreads = (threadsToRemember: readonly SubagentThread[]): void => {
|
|
1703
1776
|
for (const thread of threadsToRemember) evictedThreadParents.set(thread.id, thread.parentId);
|
|
@@ -1738,55 +1811,55 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1738
1811
|
trimSavedResults();
|
|
1739
1812
|
};
|
|
1740
1813
|
|
|
1741
|
-
const detailsFor = (
|
|
1814
|
+
const detailsFor = (
|
|
1742
1815
|
parentId: string,
|
|
1743
1816
|
mode: SubagentDetails["mode"] = "single",
|
|
1744
1817
|
scope: AgentScope = "user",
|
|
1745
1818
|
projectAgentsDir: string | null = null,
|
|
1746
1819
|
selectedThreadId?: string,
|
|
1747
|
-
): SubagentDetails => {
|
|
1748
|
-
const all = threads.listAll().filter((thread) => thread.parentId === parentId);
|
|
1749
|
-
const visible = all.filter((thread) => thread.state !== "closed").map((thread) => threadView(thread, threadMetadata));
|
|
1750
|
-
const selectedClosed = selectedThreadId
|
|
1751
|
-
? all.find((thread) => thread.id === selectedThreadId && thread.state === "closed")
|
|
1752
|
-
: undefined;
|
|
1753
|
-
const listed = selectedClosed ? [...visible, threadView(selectedClosed, threadMetadata)] : visible;
|
|
1754
|
-
const results = visible.map((thread) => {
|
|
1755
|
-
const result = threadResult(thread, savedResults.get(thread.id));
|
|
1756
|
-
result.name = threadDisplayName(thread, threadMetadata);
|
|
1757
|
-
result.attempt = threadAttempt(thread, threadMetadata);
|
|
1758
|
-
return result;
|
|
1759
|
-
});
|
|
1760
|
-
return {
|
|
1761
|
-
...cloneDetails(mode, scope, projectAgentsDir, results),
|
|
1762
|
-
parentId,
|
|
1763
|
-
executionNote: persistenceWarning,
|
|
1764
|
-
threads: listed,
|
|
1765
|
-
activeThreads: visible.filter((thread) => thread.state === "active"),
|
|
1766
|
-
doneThreads: visible.filter((thread) => ["done", "failed", "stopped", "orphaned"].includes(thread.state)),
|
|
1767
|
-
selectedThreadId,
|
|
1768
|
-
};
|
|
1769
|
-
};
|
|
1820
|
+
): SubagentDetails => {
|
|
1821
|
+
const all = threads.listAll().filter((thread) => thread.parentId === parentId);
|
|
1822
|
+
const visible = all.filter((thread) => thread.state !== "closed").map((thread) => threadView(thread, threadMetadata));
|
|
1823
|
+
const selectedClosed = selectedThreadId
|
|
1824
|
+
? all.find((thread) => thread.id === selectedThreadId && thread.state === "closed")
|
|
1825
|
+
: undefined;
|
|
1826
|
+
const listed = selectedClosed ? [...visible, threadView(selectedClosed, threadMetadata)] : visible;
|
|
1827
|
+
const results = visible.map((thread) => {
|
|
1828
|
+
const result = threadResult(thread, savedResults.get(thread.id));
|
|
1829
|
+
result.name = threadDisplayName(thread, threadMetadata);
|
|
1830
|
+
result.attempt = threadAttempt(thread, threadMetadata);
|
|
1831
|
+
return result;
|
|
1832
|
+
});
|
|
1833
|
+
return {
|
|
1834
|
+
...cloneDetails(mode, scope, projectAgentsDir, results),
|
|
1835
|
+
parentId,
|
|
1836
|
+
executionNote: persistenceWarning,
|
|
1837
|
+
threads: listed,
|
|
1838
|
+
activeThreads: visible.filter((thread) => thread.state === "active"),
|
|
1839
|
+
doneThreads: visible.filter((thread) => ["done", "failed", "stopped", "orphaned"].includes(thread.state)),
|
|
1840
|
+
selectedThreadId,
|
|
1841
|
+
};
|
|
1842
|
+
};
|
|
1770
1843
|
|
|
1771
1844
|
const threadBoardText = (parentId: string, selectedThreadId?: string): string => {
|
|
1772
1845
|
const details = detailsFor(parentId, "single", "user", null, selectedThreadId);
|
|
1773
1846
|
const active = details.activeThreads ?? [];
|
|
1774
1847
|
const done = details.doneThreads ?? [];
|
|
1775
|
-
const row = (thread: ThreadSnapshot): string => `- ${threadDisplayName(thread, threadMetadata)} · ${thread.role} · ${thread.id} · ${thread.state} · ${thread.prompt}`;
|
|
1848
|
+
const row = (thread: ThreadSnapshot): string => `- ${threadDisplayName(thread, threadMetadata)} · ${thread.role} · ${thread.id} · ${thread.state} · ${thread.prompt}`;
|
|
1776
1849
|
const lines = [
|
|
1777
1850
|
`parent ${parentId}`,
|
|
1778
1851
|
`Active (${active.length})`,
|
|
1779
1852
|
...(active.length ? active.map(row) : ["- none"]),
|
|
1780
1853
|
`Done (${done.length})`,
|
|
1781
1854
|
...(done.length ? done.map(row) : ["- none"]),
|
|
1782
|
-
"Controls: inspect · wait · steer · interrupt · collect · resume · close",
|
|
1855
|
+
"Controls: inspect · wait · steer · interrupt · collect · resume · close",
|
|
1783
1856
|
];
|
|
1784
1857
|
if (selectedThreadId) {
|
|
1785
1858
|
const selected = details.threads?.find((thread) => thread.id === selectedThreadId);
|
|
1786
1859
|
if (selected) {
|
|
1787
|
-
lines.push(`Inspect ${selected.id}: ${selected.state}`);
|
|
1788
|
-
lines.push(`Name: ${threadDisplayName(selected, threadMetadata)}`);
|
|
1789
|
-
lines.push(`Attempt: ${threadAttempt(selected, threadMetadata)}`);
|
|
1860
|
+
lines.push(`Inspect ${selected.id}: ${selected.state}`);
|
|
1861
|
+
lines.push(`Name: ${threadDisplayName(selected, threadMetadata)}`);
|
|
1862
|
+
lines.push(`Attempt: ${threadAttempt(selected, threadMetadata)}`);
|
|
1790
1863
|
lines.push(`Role: ${selected.role}`);
|
|
1791
1864
|
lines.push(`Model: ${selected.model}`);
|
|
1792
1865
|
lines.push(`Tools: ${selected.tools.join(", ")}`);
|
|
@@ -1799,11 +1872,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1799
1872
|
return boundedText(lines.join("\n"), limits.toolOutputBytes, "\n\n[Thread board truncated; inspect a child thread for its bounded detail.]");
|
|
1800
1873
|
};
|
|
1801
1874
|
|
|
1802
|
-
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
1803
|
-
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
1804
|
-
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
1805
|
-
if (runtime && runtime.sessionGeneration !== sessionGeneration) return effective;
|
|
1806
|
-
saveResult(threadId, effective);
|
|
1875
|
+
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
1876
|
+
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
1877
|
+
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
1878
|
+
if (runtime && runtime.sessionGeneration !== sessionGeneration) return effective;
|
|
1879
|
+
saveResult(threadId, effective);
|
|
1807
1880
|
let thread = threads.inspect(threadId);
|
|
1808
1881
|
if (!thread || threads.isDisposed) return effective;
|
|
1809
1882
|
if (thread.state === "queued" && next.status === "running") {
|
|
@@ -1845,193 +1918,193 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1845
1918
|
reason: effective.terminationReason ?? (effective.status === "limited" ? "resource_limit" : "interrupted"),
|
|
1846
1919
|
});
|
|
1847
1920
|
}
|
|
1848
|
-
return effective;
|
|
1849
|
-
};
|
|
1850
|
-
|
|
1851
|
-
const resolveOwnedThread = (reference: string, parentId: string): ThreadSnapshot | undefined => {
|
|
1852
|
-
const registry = threads as any;
|
|
1853
|
-
const resolved = typeof registry.resolve === "function" ? registry.resolve(reference, parentId as SubagentThreadId) : undefined;
|
|
1854
|
-
if (resolved && resolved.parentId === parentId) return threadView(resolved, threadMetadata);
|
|
1855
|
-
const exact = threads.inspect(reference as SubagentThreadId);
|
|
1856
|
-
if (exact && exact.parentId === parentId) return threadView(exact, threadMetadata);
|
|
1857
|
-
const folded = reference.toLocaleLowerCase();
|
|
1858
|
-
return threads.listAll()
|
|
1859
|
-
.filter((thread) => thread.parentId === parentId)
|
|
1860
|
-
.map((thread) => threadView(thread, threadMetadata))
|
|
1861
|
-
.find((thread) => threadDisplayName(thread, threadMetadata).toLocaleLowerCase() === folded);
|
|
1862
|
-
};
|
|
1863
|
-
|
|
1864
|
-
const terminalThread = (thread: ThreadSnapshot): boolean => ["done", "failed", "stopped", "orphaned", "closed"].includes(thread.state as string);
|
|
1865
|
-
|
|
1866
|
-
const waitForThreads = async (ids: readonly string[], timeoutMs: number): Promise<SubagentWaitSummary> => {
|
|
1867
|
-
const startedAt = Date.now();
|
|
1868
|
-
const targetIds = [...ids];
|
|
1869
|
-
const status = (): { completed: string[]; pending: string[] } => {
|
|
1870
|
-
const completed: string[] = [];
|
|
1871
|
-
const pending: string[] = [];
|
|
1872
|
-
for (const id of targetIds) {
|
|
1873
|
-
const thread = threads.inspect(id as SubagentThreadId);
|
|
1874
|
-
if (thread && terminalThread(threadView(thread, threadMetadata))) completed.push(id);
|
|
1875
|
-
else pending.push(id);
|
|
1876
|
-
}
|
|
1877
|
-
return { completed, pending };
|
|
1878
|
-
};
|
|
1879
|
-
const initial = status();
|
|
1880
|
-
if (!initial.pending.length) {
|
|
1881
|
-
return { targetThreadIds: targetIds, completedThreadIds: initial.completed, pendingThreadIds: [], timedOut: false, waitedMs: 0 };
|
|
1882
|
-
}
|
|
1883
|
-
const registry = threads as any;
|
|
1884
|
-
if (typeof registry.waitForTerminal === "function") {
|
|
1885
|
-
const result = await registry.waitForTerminal(targetIds as SubagentThreadId[], timeoutMs);
|
|
1886
|
-
return {
|
|
1887
|
-
targetThreadIds: targetIds,
|
|
1888
|
-
completedThreadIds: [...(result.completedThreadIds ?? [])],
|
|
1889
|
-
pendingThreadIds: [...(result.pendingThreadIds ?? [])],
|
|
1890
|
-
timedOut: result.timedOut === true,
|
|
1891
|
-
waitedMs: result.waitedMs ?? Date.now() - startedAt,
|
|
1892
|
-
};
|
|
1893
|
-
}
|
|
1894
|
-
return new Promise((resolve) => {
|
|
1895
|
-
let timer: NodeJS.Timeout | undefined;
|
|
1896
|
-
let unsubscribe = (): void => {};
|
|
1897
|
-
const finish = (timedOut: boolean): void => {
|
|
1898
|
-
if (timer) clearTimeout(timer);
|
|
1899
|
-
unsubscribe();
|
|
1900
|
-
const current = status();
|
|
1901
|
-
resolve({
|
|
1902
|
-
targetThreadIds: targetIds,
|
|
1903
|
-
completedThreadIds: current.completed,
|
|
1904
|
-
pendingThreadIds: current.pending,
|
|
1905
|
-
timedOut,
|
|
1906
|
-
waitedMs: Date.now() - startedAt,
|
|
1907
|
-
});
|
|
1908
|
-
};
|
|
1909
|
-
unsubscribe = threads.subscribe(() => {
|
|
1910
|
-
if (!status().pending.length) finish(false);
|
|
1911
|
-
});
|
|
1912
|
-
timer = setTimeout(() => finish(true), timeoutMs);
|
|
1913
|
-
if (!status().pending.length) finish(false);
|
|
1914
|
-
});
|
|
1915
|
-
};
|
|
1916
|
-
|
|
1917
|
-
const resumeThread = (target: ThreadSnapshot, prompt: string | undefined): ThreadSnapshot => {
|
|
1918
|
-
const registry = threads as any;
|
|
1919
|
-
const previousResult = savedResults.get(target.id);
|
|
1920
|
-
savedResults.delete(target.id);
|
|
1921
|
-
if (typeof registry.resume === "function") {
|
|
1922
|
-
try {
|
|
1923
|
-
const resumed = registry.resume(target.id as SubagentThreadId, prompt);
|
|
1924
|
-
const metadata = threadMetadata.get(target.id);
|
|
1925
|
-
if (metadata) metadata.attempt += 1;
|
|
1926
|
-
return threadView(resumed, threadMetadata);
|
|
1927
|
-
} catch (error) {
|
|
1928
|
-
if (previousResult) saveResult(target.id, previousResult);
|
|
1929
|
-
throw error;
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
try {
|
|
1933
|
-
const snapshots = threads.listAll().filter((thread) => thread.state !== "closed");
|
|
1934
|
-
const ids = snapshots.map((thread) => thread.id);
|
|
1935
|
-
let idIndex = 0;
|
|
1936
|
-
unsubscribePersistence();
|
|
1937
|
-
const previous = threads;
|
|
1938
|
-
threads = new SubagentThreadRegistry({ createId: () => ids[idIndex++]! });
|
|
1939
|
-
attachPersistence();
|
|
1940
|
-
let resumed: ThreadSnapshot | undefined;
|
|
1941
|
-
for (const snapshot of snapshots) {
|
|
1942
|
-
const metadata = threadMetadata.get(snapshot.id) ?? {
|
|
1943
|
-
displayName: threadDisplayName(snapshot, threadMetadata),
|
|
1944
|
-
attempt: threadAttempt(snapshot, threadMetadata),
|
|
1945
|
-
session: threadSession(snapshot, threadMetadata) ?? { id: `killeros-${safeSessionId(snapshot.id)}`, directory: "" },
|
|
1946
|
-
persistentSession: Boolean(threadSession(snapshot, threadMetadata)?.directory),
|
|
1947
|
-
};
|
|
1948
|
-
const nextPrompt = snapshot.id === target.id && prompt ? prompt : snapshot.prompt;
|
|
1949
|
-
const created = threads.spawn({
|
|
1950
|
-
parentId: snapshot.parentId,
|
|
1951
|
-
role: snapshot.role,
|
|
1952
|
-
prompt: nextPrompt,
|
|
1953
|
-
model: snapshot.model,
|
|
1954
|
-
tools: snapshot.tools,
|
|
1955
|
-
capabilityBoundary: snapshot.capabilityBoundary,
|
|
1956
|
-
displayName: metadata.displayName,
|
|
1957
|
-
attempt: snapshot.id === target.id ? metadata.attempt + 1 : metadata.attempt,
|
|
1958
|
-
session: metadata.session,
|
|
1959
|
-
} as any);
|
|
1960
|
-
metadata.attempt = snapshot.id === target.id ? metadata.attempt + 1 : metadata.attempt;
|
|
1961
|
-
threadMetadata.set(snapshot.id, metadata);
|
|
1962
|
-
if (snapshot.id === target.id) {
|
|
1963
|
-
resumed = threadView(created, threadMetadata);
|
|
1964
|
-
continue;
|
|
1965
|
-
}
|
|
1966
|
-
if (snapshot.state === "done") {
|
|
1967
|
-
threads.begin(created.id);
|
|
1968
|
-
threads.complete(created.id, { result: snapshot.result });
|
|
1969
|
-
} else if (snapshot.state === "failed") {
|
|
1970
|
-
threads.begin(created.id);
|
|
1971
|
-
threads.fail(created.id, { message: snapshot.failure?.message ?? "restored failure" });
|
|
1972
|
-
} else if (snapshot.state === "stopped" || snapshot.state === "orphaned") {
|
|
1973
|
-
threads.stop(created.id, { reason: snapshot.stopReason ?? "stopped" });
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
if (!resumed) throw new Error(`Unknown child thread ${JSON.stringify(target.id)}`);
|
|
1977
|
-
if (!previous.isDisposed) previous.dispose();
|
|
1978
|
-
return resumed;
|
|
1979
|
-
} catch (error) {
|
|
1980
|
-
if (previousResult) saveResult(target.id, previousResult);
|
|
1981
|
-
throw error;
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
|
|
1985
|
-
if (typeof pi.on === "function") {
|
|
1986
|
-
pi.on("session_start", (_event, ctx) => {
|
|
1987
|
-
sessionGeneration += 1;
|
|
1988
|
-
stopActiveRuntimes("session_start");
|
|
1989
|
-
threadResources.clear();
|
|
1990
|
-
persistenceWarning = undefined;
|
|
1991
|
-
unsubscribePersistence();
|
|
1992
|
-
threads.dispose();
|
|
1993
|
-
threads = new SubagentThreadRegistry();
|
|
1994
|
-
threadMetadata.clear();
|
|
1995
|
-
savedResults.clear();
|
|
1996
|
-
evictedThreadParents.clear();
|
|
1997
|
-
activeRuntimes.clear();
|
|
1998
|
-
attachPersistence();
|
|
1999
|
-
const entries = (ctx as ExtensionContext | undefined)?.sessionManager?.getEntries?.();
|
|
2000
|
-
if (Array.isArray(entries)) {
|
|
2001
|
-
const extensionContext = ctx as ExtensionContext;
|
|
2002
|
-
installRestoredThreads(restoreRecords(
|
|
2003
|
-
entries,
|
|
2004
|
-
parentThreadId(extensionContext),
|
|
2005
|
-
(threadId) => childSessionPath(extensionContext, threadId),
|
|
2006
|
-
));
|
|
2007
|
-
}
|
|
2008
|
-
});
|
|
2009
|
-
pi.on("session_shutdown", async () => {
|
|
2010
|
-
sessionGeneration += 1;
|
|
2011
|
-
stopActiveRuntimes("session_shutdown");
|
|
2012
|
-
await Promise.allSettled([...backgroundBatches]);
|
|
2013
|
-
unsubscribePersistence();
|
|
2014
|
-
for (const thread of threads.listAll()) {
|
|
2015
|
-
if (["done", "failed", "stopped", "orphaned"].includes(thread.state as string)) {
|
|
2016
|
-
recordSnapshot(thread, savedResults.get(thread.id));
|
|
2017
|
-
}
|
|
2018
|
-
}
|
|
2019
|
-
threadResources.clear();
|
|
2020
|
-
threads.dispose();
|
|
2021
|
-
threadMetadata.clear();
|
|
2022
|
-
savedResults.clear();
|
|
2023
|
-
evictedThreadParents.clear();
|
|
2024
|
-
});
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
const toolDefinition: Parameters<ExtensionAPI["registerTool"]>[0] = {
|
|
1921
|
+
return effective;
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1924
|
+
const resolveOwnedThread = (reference: string, parentId: string): ThreadSnapshot | undefined => {
|
|
1925
|
+
const registry = threads as any;
|
|
1926
|
+
const resolved = typeof registry.resolve === "function" ? registry.resolve(reference, parentId as SubagentThreadId) : undefined;
|
|
1927
|
+
if (resolved && resolved.parentId === parentId) return threadView(resolved, threadMetadata);
|
|
1928
|
+
const exact = threads.inspect(reference as SubagentThreadId);
|
|
1929
|
+
if (exact && exact.parentId === parentId) return threadView(exact, threadMetadata);
|
|
1930
|
+
const folded = reference.toLocaleLowerCase();
|
|
1931
|
+
return threads.listAll()
|
|
1932
|
+
.filter((thread) => thread.parentId === parentId)
|
|
1933
|
+
.map((thread) => threadView(thread, threadMetadata))
|
|
1934
|
+
.find((thread) => threadDisplayName(thread, threadMetadata).toLocaleLowerCase() === folded);
|
|
1935
|
+
};
|
|
1936
|
+
|
|
1937
|
+
const terminalThread = (thread: ThreadSnapshot): boolean => ["done", "failed", "stopped", "orphaned", "closed"].includes(thread.state as string);
|
|
1938
|
+
|
|
1939
|
+
const waitForThreads = async (ids: readonly string[], timeoutMs: number): Promise<SubagentWaitSummary> => {
|
|
1940
|
+
const startedAt = Date.now();
|
|
1941
|
+
const targetIds = [...ids];
|
|
1942
|
+
const status = (): { completed: string[]; pending: string[] } => {
|
|
1943
|
+
const completed: string[] = [];
|
|
1944
|
+
const pending: string[] = [];
|
|
1945
|
+
for (const id of targetIds) {
|
|
1946
|
+
const thread = threads.inspect(id as SubagentThreadId);
|
|
1947
|
+
if (thread && terminalThread(threadView(thread, threadMetadata))) completed.push(id);
|
|
1948
|
+
else pending.push(id);
|
|
1949
|
+
}
|
|
1950
|
+
return { completed, pending };
|
|
1951
|
+
};
|
|
1952
|
+
const initial = status();
|
|
1953
|
+
if (!initial.pending.length) {
|
|
1954
|
+
return { targetThreadIds: targetIds, completedThreadIds: initial.completed, pendingThreadIds: [], timedOut: false, waitedMs: 0 };
|
|
1955
|
+
}
|
|
1956
|
+
const registry = threads as any;
|
|
1957
|
+
if (typeof registry.waitForTerminal === "function") {
|
|
1958
|
+
const result = await registry.waitForTerminal(targetIds as SubagentThreadId[], timeoutMs);
|
|
1959
|
+
return {
|
|
1960
|
+
targetThreadIds: targetIds,
|
|
1961
|
+
completedThreadIds: [...(result.completedThreadIds ?? [])],
|
|
1962
|
+
pendingThreadIds: [...(result.pendingThreadIds ?? [])],
|
|
1963
|
+
timedOut: result.timedOut === true,
|
|
1964
|
+
waitedMs: result.waitedMs ?? Date.now() - startedAt,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
return new Promise((resolve) => {
|
|
1968
|
+
let timer: NodeJS.Timeout | undefined;
|
|
1969
|
+
let unsubscribe = (): void => {};
|
|
1970
|
+
const finish = (timedOut: boolean): void => {
|
|
1971
|
+
if (timer) clearTimeout(timer);
|
|
1972
|
+
unsubscribe();
|
|
1973
|
+
const current = status();
|
|
1974
|
+
resolve({
|
|
1975
|
+
targetThreadIds: targetIds,
|
|
1976
|
+
completedThreadIds: current.completed,
|
|
1977
|
+
pendingThreadIds: current.pending,
|
|
1978
|
+
timedOut,
|
|
1979
|
+
waitedMs: Date.now() - startedAt,
|
|
1980
|
+
});
|
|
1981
|
+
};
|
|
1982
|
+
unsubscribe = threads.subscribe(() => {
|
|
1983
|
+
if (!status().pending.length) finish(false);
|
|
1984
|
+
});
|
|
1985
|
+
timer = setTimeout(() => finish(true), timeoutMs);
|
|
1986
|
+
if (!status().pending.length) finish(false);
|
|
1987
|
+
});
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
const resumeThread = (target: ThreadSnapshot, prompt: string | undefined): ThreadSnapshot => {
|
|
1991
|
+
const registry = threads as any;
|
|
1992
|
+
const previousResult = savedResults.get(target.id);
|
|
1993
|
+
savedResults.delete(target.id);
|
|
1994
|
+
if (typeof registry.resume === "function") {
|
|
1995
|
+
try {
|
|
1996
|
+
const resumed = registry.resume(target.id as SubagentThreadId, prompt);
|
|
1997
|
+
const metadata = threadMetadata.get(target.id);
|
|
1998
|
+
if (metadata) metadata.attempt += 1;
|
|
1999
|
+
return threadView(resumed, threadMetadata);
|
|
2000
|
+
} catch (error) {
|
|
2001
|
+
if (previousResult) saveResult(target.id, previousResult);
|
|
2002
|
+
throw error;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
try {
|
|
2006
|
+
const snapshots = threads.listAll().filter((thread) => thread.state !== "closed");
|
|
2007
|
+
const ids = snapshots.map((thread) => thread.id);
|
|
2008
|
+
let idIndex = 0;
|
|
2009
|
+
unsubscribePersistence();
|
|
2010
|
+
const previous = threads;
|
|
2011
|
+
threads = new SubagentThreadRegistry({ createId: () => ids[idIndex++]! });
|
|
2012
|
+
attachPersistence();
|
|
2013
|
+
let resumed: ThreadSnapshot | undefined;
|
|
2014
|
+
for (const snapshot of snapshots) {
|
|
2015
|
+
const metadata = threadMetadata.get(snapshot.id) ?? {
|
|
2016
|
+
displayName: threadDisplayName(snapshot, threadMetadata),
|
|
2017
|
+
attempt: threadAttempt(snapshot, threadMetadata),
|
|
2018
|
+
session: threadSession(snapshot, threadMetadata) ?? { id: `killeros-${safeSessionId(snapshot.id)}`, directory: "" },
|
|
2019
|
+
persistentSession: Boolean(threadSession(snapshot, threadMetadata)?.directory),
|
|
2020
|
+
};
|
|
2021
|
+
const nextPrompt = snapshot.id === target.id && prompt ? prompt : snapshot.prompt;
|
|
2022
|
+
const created = threads.spawn({
|
|
2023
|
+
parentId: snapshot.parentId,
|
|
2024
|
+
role: snapshot.role,
|
|
2025
|
+
prompt: nextPrompt,
|
|
2026
|
+
model: snapshot.model,
|
|
2027
|
+
tools: snapshot.tools,
|
|
2028
|
+
capabilityBoundary: snapshot.capabilityBoundary,
|
|
2029
|
+
displayName: metadata.displayName,
|
|
2030
|
+
attempt: snapshot.id === target.id ? metadata.attempt + 1 : metadata.attempt,
|
|
2031
|
+
session: metadata.session,
|
|
2032
|
+
} as any);
|
|
2033
|
+
metadata.attempt = snapshot.id === target.id ? metadata.attempt + 1 : metadata.attempt;
|
|
2034
|
+
threadMetadata.set(snapshot.id, metadata);
|
|
2035
|
+
if (snapshot.id === target.id) {
|
|
2036
|
+
resumed = threadView(created, threadMetadata);
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (snapshot.state === "done") {
|
|
2040
|
+
threads.begin(created.id);
|
|
2041
|
+
threads.complete(created.id, { result: snapshot.result });
|
|
2042
|
+
} else if (snapshot.state === "failed") {
|
|
2043
|
+
threads.begin(created.id);
|
|
2044
|
+
threads.fail(created.id, { message: snapshot.failure?.message ?? "restored failure" });
|
|
2045
|
+
} else if (snapshot.state === "stopped" || snapshot.state === "orphaned") {
|
|
2046
|
+
threads.stop(created.id, { reason: snapshot.stopReason ?? "stopped" });
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (!resumed) throw new Error(`Unknown child thread ${JSON.stringify(target.id)}`);
|
|
2050
|
+
if (!previous.isDisposed) previous.dispose();
|
|
2051
|
+
return resumed;
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
if (previousResult) saveResult(target.id, previousResult);
|
|
2054
|
+
throw error;
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
if (typeof pi.on === "function") {
|
|
2059
|
+
pi.on("session_start", (_event, ctx) => {
|
|
2060
|
+
sessionGeneration += 1;
|
|
2061
|
+
stopActiveRuntimes("session_start");
|
|
2062
|
+
threadResources.clear();
|
|
2063
|
+
persistenceWarning = undefined;
|
|
2064
|
+
unsubscribePersistence();
|
|
2065
|
+
threads.dispose();
|
|
2066
|
+
threads = new SubagentThreadRegistry();
|
|
2067
|
+
threadMetadata.clear();
|
|
2068
|
+
savedResults.clear();
|
|
2069
|
+
evictedThreadParents.clear();
|
|
2070
|
+
activeRuntimes.clear();
|
|
2071
|
+
attachPersistence();
|
|
2072
|
+
const entries = (ctx as ExtensionContext | undefined)?.sessionManager?.getEntries?.();
|
|
2073
|
+
if (Array.isArray(entries)) {
|
|
2074
|
+
const extensionContext = ctx as ExtensionContext;
|
|
2075
|
+
installRestoredThreads(restoreRecords(
|
|
2076
|
+
entries,
|
|
2077
|
+
parentThreadId(extensionContext),
|
|
2078
|
+
(threadId) => childSessionPath(extensionContext, threadId),
|
|
2079
|
+
));
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
pi.on("session_shutdown", async () => {
|
|
2083
|
+
sessionGeneration += 1;
|
|
2084
|
+
stopActiveRuntimes("session_shutdown");
|
|
2085
|
+
await Promise.allSettled([...backgroundBatches]);
|
|
2086
|
+
unsubscribePersistence();
|
|
2087
|
+
for (const thread of threads.listAll()) {
|
|
2088
|
+
if (["done", "failed", "stopped", "orphaned"].includes(thread.state as string)) {
|
|
2089
|
+
recordSnapshot(thread, savedResults.get(thread.id));
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
threadResources.clear();
|
|
2093
|
+
threads.dispose();
|
|
2094
|
+
threadMetadata.clear();
|
|
2095
|
+
savedResults.clear();
|
|
2096
|
+
evictedThreadParents.clear();
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
const toolDefinition: Parameters<ExtensionAPI["registerTool"]>[0] = {
|
|
2028
2101
|
name: "subagent",
|
|
2029
2102
|
label: "Subagents",
|
|
2030
|
-
description: `Spawn and manage named child threads.
|
|
2103
|
+
description: `Spawn and manage named child threads. Bundled roles: debugger, documenter, planner, reviewer, scout, security, tester, worker. Agent accepts a role name or an inline { name, description, access, tools } role. On spawn, message aliases task. Parallel tasks with write-capable roles use one shared slot; read-only batches run concurrently up to ${limits.maxReadConcurrency}. Write-capable tasks are serialized in the shared parent worktree. Use action list, inspect, wait, steer, interrupt, collect, resume, and close to manage child handoffs.`,
|
|
2031
2104
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
2032
2105
|
promptGuidelines: [
|
|
2033
2106
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
2034
|
-
"Parallel tasks with write-capable roles use one shared slot because all children share the parent worktree.",
|
|
2107
|
+
"Parallel tasks with write-capable roles use one shared slot because all children share the parent worktree.",
|
|
2035
2108
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
2036
2109
|
"When the user names a model or thinking effort, pass model and thinking separately; use inherit when the active parent or role setting should decide.",
|
|
2037
2110
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
@@ -2053,57 +2126,57 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2053
2126
|
details,
|
|
2054
2127
|
usage: details.aggregateUsage,
|
|
2055
2128
|
};
|
|
2056
|
-
};
|
|
2057
|
-
|
|
2058
|
-
if (request.kind === "list") return actionResult(threadBoardText(parentId));
|
|
2059
|
-
if (request.kind === "inspect") {
|
|
2060
|
-
const { threadId } = request.input;
|
|
2061
|
-
const thread = resolveOwnedThread(threadId, parentId);
|
|
2062
|
-
if (!thread) {
|
|
2129
|
+
};
|
|
2130
|
+
|
|
2131
|
+
if (request.kind === "list") return actionResult(threadBoardText(parentId));
|
|
2132
|
+
if (request.kind === "inspect") {
|
|
2133
|
+
const { threadId } = request.input;
|
|
2134
|
+
const thread = resolveOwnedThread(threadId, parentId);
|
|
2135
|
+
if (!thread) {
|
|
2063
2136
|
if (evictedThreadParents.get(threadId) === parentId) {
|
|
2064
2137
|
return actionResult(`Thread ${threadId} was evicted from bounded retention; its heavy data is no longer available.`, threadId);
|
|
2065
2138
|
}
|
|
2066
2139
|
throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2067
2140
|
}
|
|
2068
|
-
return actionResult(threadBoardText(parentId, thread.id), thread.id);
|
|
2069
|
-
}
|
|
2070
|
-
if (request.kind === "steer") {
|
|
2071
|
-
const { threadId, message } = request.input;
|
|
2072
|
-
const thread = resolveOwnedThread(threadId, parentId);
|
|
2073
|
-
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2074
|
-
const brandedThreadId = thread.id as SubagentThreadId;
|
|
2075
|
-
const runtime = activeRuntimes.get(thread.id);
|
|
2076
|
-
const pendingCount = runtime ? runtime.steering.length : thread.steering.length;
|
|
2077
|
-
if (pendingCount >= MAX_RUNTIME_STEERING_MESSAGES) {
|
|
2078
|
-
throw new Error(`Steering queue is full (${MAX_RUNTIME_STEERING_MESSAGES} pending messages); wait for the child restart or interrupt the thread first`);
|
|
2079
|
-
}
|
|
2080
|
-
const pendingSteering = runtime
|
|
2081
|
-
? [...runtime.steering, message]
|
|
2082
|
-
: [...thread.steering.map((entry) => entry.message), message];
|
|
2083
|
-
const baseTask = runtime?.task ?? thread.prompt;
|
|
2084
|
-
if (steeredTaskWouldExceedLimit(baseTask, pendingSteering, limits.taskCharacters)) {
|
|
2085
|
-
throw new Error(`Steering would exceed the ${limits.taskCharacters}-character task limit; shorten the message or wait for the child restart`);
|
|
2086
|
-
}
|
|
2087
|
-
threads.steer(brandedThreadId, message);
|
|
2141
|
+
return actionResult(threadBoardText(parentId, thread.id), thread.id);
|
|
2142
|
+
}
|
|
2143
|
+
if (request.kind === "steer") {
|
|
2144
|
+
const { threadId, message } = request.input;
|
|
2145
|
+
const thread = resolveOwnedThread(threadId, parentId);
|
|
2146
|
+
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2147
|
+
const brandedThreadId = thread.id as SubagentThreadId;
|
|
2148
|
+
const runtime = activeRuntimes.get(thread.id);
|
|
2149
|
+
const pendingCount = runtime ? runtime.steering.length : thread.steering.length;
|
|
2150
|
+
if (pendingCount >= MAX_RUNTIME_STEERING_MESSAGES) {
|
|
2151
|
+
throw new Error(`Steering queue is full (${MAX_RUNTIME_STEERING_MESSAGES} pending messages); wait for the child restart or interrupt the thread first`);
|
|
2152
|
+
}
|
|
2153
|
+
const pendingSteering = runtime
|
|
2154
|
+
? [...runtime.steering, message]
|
|
2155
|
+
: [...thread.steering.map((entry) => entry.message), message];
|
|
2156
|
+
const baseTask = runtime?.task ?? thread.prompt;
|
|
2157
|
+
if (steeredTaskWouldExceedLimit(baseTask, pendingSteering, limits.taskCharacters)) {
|
|
2158
|
+
throw new Error(`Steering would exceed the ${limits.taskCharacters}-character task limit; shorten the message or wait for the child restart`);
|
|
2159
|
+
}
|
|
2160
|
+
threads.steer(brandedThreadId, message);
|
|
2088
2161
|
if (runtime) {
|
|
2089
2162
|
runtime.steering.push(message);
|
|
2090
2163
|
runtime.restarting = true;
|
|
2091
2164
|
runtime.requestedReason = "steer";
|
|
2092
2165
|
runtime.handle?.stop("steer");
|
|
2093
2166
|
}
|
|
2094
|
-
return actionResult(`Steering queued for ${threadDisplayName(thread, threadMetadata)} (${thread.id}). The child keeps the same thread and handoff record.`, thread.id);
|
|
2095
|
-
}
|
|
2096
|
-
if (request.kind === "interrupt-one" || request.kind === "interrupt-all") {
|
|
2097
|
-
let targets: SubagentThread[];
|
|
2167
|
+
return actionResult(`Steering queued for ${threadDisplayName(thread, threadMetadata)} (${thread.id}). The child keeps the same thread and handoff record.`, thread.id);
|
|
2168
|
+
}
|
|
2169
|
+
if (request.kind === "interrupt-one" || request.kind === "interrupt-all") {
|
|
2170
|
+
let targets: SubagentThread[];
|
|
2098
2171
|
if (request.kind === "interrupt-all") {
|
|
2099
2172
|
targets = threads.listAll().filter((thread) => thread.parentId === parentId && (thread.state === "active" || thread.state === "queued"));
|
|
2100
|
-
} else {
|
|
2101
|
-
const { threadId } = request.input;
|
|
2102
|
-
const target = resolveOwnedThread(threadId, parentId);
|
|
2103
|
-
if (!target) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2104
|
-
if (target.state !== "active" && target.state !== "queued") {
|
|
2105
|
-
throw new Error(`Cannot interrupt thread ${threadId} from ${target.state}`);
|
|
2106
|
-
}
|
|
2173
|
+
} else {
|
|
2174
|
+
const { threadId } = request.input;
|
|
2175
|
+
const target = resolveOwnedThread(threadId, parentId);
|
|
2176
|
+
if (!target) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2177
|
+
if (target.state !== "active" && target.state !== "queued") {
|
|
2178
|
+
throw new Error(`Cannot interrupt thread ${threadId} from ${target.state}`);
|
|
2179
|
+
}
|
|
2107
2180
|
targets = [target];
|
|
2108
2181
|
}
|
|
2109
2182
|
for (const thread of targets) {
|
|
@@ -2117,95 +2190,98 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2117
2190
|
} else if (thread.state === "active" || thread.state === "queued") {
|
|
2118
2191
|
threads.stop(thread.id, { reason: "interrupt" });
|
|
2119
2192
|
}
|
|
2120
|
-
}
|
|
2121
|
-
return actionResult(request.kind === "interrupt-all"
|
|
2122
|
-
? "Interrupt requested for all active and queued child threads."
|
|
2123
|
-
: `Interrupt requested for ${targets[0]?.id} (${threadDisplayName(targets[0] as ThreadSnapshot, threadMetadata)}).`);
|
|
2124
|
-
}
|
|
2125
|
-
if (request.kind === "collect") {
|
|
2126
|
-
const { threadId } = request.input;
|
|
2127
|
-
const thread = resolveOwnedThread(threadId, parentId);
|
|
2128
|
-
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2129
|
-
const collected = threads.collect(thread.id as SubagentThreadId);
|
|
2130
|
-
return actionResult(`Collected ${threadDisplayName(thread, threadMetadata)} (${thread.id}): ${collected.result ?? collected.failure?.message ?? collected.stopReason ?? "no handoff"}`, thread.id);
|
|
2131
|
-
}
|
|
2132
|
-
if (request.kind === "wait") {
|
|
2133
|
-
const target = request.input.threadId ? resolveOwnedThread(request.input.threadId, parentId) : undefined;
|
|
2134
|
-
if (request.input.threadId && !target) throw new Error(`Unknown child thread ${JSON.stringify(request.input.threadId)}`);
|
|
2135
|
-
const targets = target
|
|
2136
|
-
? [target]
|
|
2137
|
-
: threads.listAll().filter((thread) => thread.parentId === parentId && (thread.state === "queued" || thread.state === "active"));
|
|
2138
|
-
const wait = await waitForThreads(targets.map((thread) => thread.id), request.input.timeoutMs);
|
|
2139
|
-
const details = actionDetails(target?.id);
|
|
2140
|
-
details.wait = wait;
|
|
2141
|
-
return {
|
|
2142
|
-
content: [{ type: "text" as const, text: boundedText(
|
|
2143
|
-
wait.timedOut
|
|
2144
|
-
? `Wait timed out after ${wait.waitedMs}ms. Pending: ${wait.pendingThreadIds.join(", ") || "none"}.`
|
|
2145
|
-
: `Wait complete after ${wait.waitedMs}ms. Completed: ${wait.completedThreadIds.join(", ") || "none"}.`,
|
|
2146
|
-
limits.toolOutputBytes,
|
|
2147
|
-
"\n\n[Thread action output truncated.]",
|
|
2148
|
-
) }],
|
|
2149
|
-
details,
|
|
2150
|
-
usage: details.aggregateUsage,
|
|
2151
|
-
};
|
|
2152
|
-
}
|
|
2153
|
-
if (request.kind === "close") {
|
|
2154
|
-
const { threadId } = request.input;
|
|
2155
|
-
const thread = resolveOwnedThread(threadId, parentId);
|
|
2156
|
-
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2157
|
-
if (thread.state === "queued" || thread.state === "active") {
|
|
2158
|
-
throw new Error(`Cannot close thread ${thread.id} from ${thread.state}`);
|
|
2159
|
-
}
|
|
2160
|
-
const resource = threadResources.get(thread.id);
|
|
2161
|
-
const exits = resource ? await Promise.all([...resource.handles].map((handle) => waitForConfirmedProcessExit(handle, limits.processExitWaitMs))) : [];
|
|
2162
|
-
const exitConfirmed = exits.every(Boolean);
|
|
2163
|
-
if (!exitConfirmed) {
|
|
2164
|
-
const current = savedResults.get(thread.id);
|
|
2165
|
-
if (current) {
|
|
2166
|
-
const failed = cloneResult(current);
|
|
2167
|
-
failed.status = "failed";
|
|
2168
|
-
failed.terminationReason = "process_exit_unconfirmed";
|
|
2169
|
-
failed.exitConfirmed = false;
|
|
2170
|
-
failed.errorMessage = "Child process exit was not confirmed before close";
|
|
2171
|
-
saveResult(thread.id, failed);
|
|
2172
|
-
recordSnapshot(thread, failed);
|
|
2173
|
-
}
|
|
2174
|
-
} else {
|
|
2175
|
-
const session = threadSession(thread, threadMetadata);
|
|
2176
|
-
const directory = resource?.directory ?? session?.directory;
|
|
2177
|
-
const expectedDirectory = childSessionPath(ctx, thread.id)?.directory;
|
|
2178
|
-
const trustedRestoredDirectory = !resource && directory && expectedDirectory
|
|
2179
|
-
&& path.resolve(directory) === path.resolve(expectedDirectory);
|
|
2180
|
-
if ((resource?.persistent || trustedRestoredDirectory) && directory) {
|
|
2181
|
-
await rm(directory, { recursive: true, force: true });
|
|
2182
|
-
}
|
|
2183
|
-
}
|
|
2184
|
-
threads.close(thread.id as SubagentThreadId);
|
|
2185
|
-
if (exitConfirmed) threadResources.delete(thread.id);
|
|
2186
|
-
savedResults.delete(thread.id);
|
|
2187
|
-
pruneClosedThreads();
|
|
2188
|
-
return actionResult(exitConfirmed
|
|
2189
|
-
? `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}). Heavy trace and handoff data were evicted; a tombstone remains inspectable.`
|
|
2190
|
-
: `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}); process exit was not confirmed, so its session directory was retained.`, thread.id);
|
|
2191
|
-
}
|
|
2192
|
-
|
|
2193
|
-
let resumeTarget: ThreadSnapshot | undefined;
|
|
2194
|
-
let resumePrompt: string | undefined;
|
|
2195
|
-
const isResume = request.kind === "resume";
|
|
2193
|
+
}
|
|
2194
|
+
return actionResult(request.kind === "interrupt-all"
|
|
2195
|
+
? "Interrupt requested for all active and queued child threads."
|
|
2196
|
+
: `Interrupt requested for ${targets[0]?.id} (${threadDisplayName(targets[0] as ThreadSnapshot, threadMetadata)}).`);
|
|
2197
|
+
}
|
|
2198
|
+
if (request.kind === "collect") {
|
|
2199
|
+
const { threadId } = request.input;
|
|
2200
|
+
const thread = resolveOwnedThread(threadId, parentId);
|
|
2201
|
+
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2202
|
+
const collected = threads.collect(thread.id as SubagentThreadId);
|
|
2203
|
+
return actionResult(`Collected ${threadDisplayName(thread, threadMetadata)} (${thread.id}): ${collected.result ?? collected.failure?.message ?? collected.stopReason ?? "no handoff"}`, thread.id);
|
|
2204
|
+
}
|
|
2205
|
+
if (request.kind === "wait") {
|
|
2206
|
+
const target = request.input.threadId ? resolveOwnedThread(request.input.threadId, parentId) : undefined;
|
|
2207
|
+
if (request.input.threadId && !target) throw new Error(`Unknown child thread ${JSON.stringify(request.input.threadId)}`);
|
|
2208
|
+
const targets = target
|
|
2209
|
+
? [target]
|
|
2210
|
+
: threads.listAll().filter((thread) => thread.parentId === parentId && (thread.state === "queued" || thread.state === "active"));
|
|
2211
|
+
const wait = await waitForThreads(targets.map((thread) => thread.id), request.input.timeoutMs);
|
|
2212
|
+
const details = actionDetails(target?.id);
|
|
2213
|
+
details.wait = wait;
|
|
2214
|
+
return {
|
|
2215
|
+
content: [{ type: "text" as const, text: boundedText(
|
|
2216
|
+
wait.timedOut
|
|
2217
|
+
? `Wait timed out after ${wait.waitedMs}ms. Pending: ${wait.pendingThreadIds.join(", ") || "none"}.`
|
|
2218
|
+
: `Wait complete after ${wait.waitedMs}ms. Completed: ${wait.completedThreadIds.join(", ") || "none"}.`,
|
|
2219
|
+
limits.toolOutputBytes,
|
|
2220
|
+
"\n\n[Thread action output truncated.]",
|
|
2221
|
+
) }],
|
|
2222
|
+
details,
|
|
2223
|
+
usage: details.aggregateUsage,
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
if (request.kind === "close") {
|
|
2227
|
+
const { threadId } = request.input;
|
|
2228
|
+
const thread = resolveOwnedThread(threadId, parentId);
|
|
2229
|
+
if (!thread) throw new Error(`Unknown child thread ${JSON.stringify(threadId)}`);
|
|
2230
|
+
if (thread.state === "queued" || thread.state === "active") {
|
|
2231
|
+
throw new Error(`Cannot close thread ${thread.id} from ${thread.state}`);
|
|
2232
|
+
}
|
|
2233
|
+
const resource = threadResources.get(thread.id);
|
|
2234
|
+
const exits = resource ? await Promise.all([...resource.handles].map((handle) => waitForConfirmedProcessExit(handle, limits.processExitWaitMs))) : [];
|
|
2235
|
+
const exitConfirmed = exits.every(Boolean);
|
|
2236
|
+
if (!exitConfirmed) {
|
|
2237
|
+
const current = savedResults.get(thread.id);
|
|
2238
|
+
if (current) {
|
|
2239
|
+
const failed = cloneResult(current);
|
|
2240
|
+
failed.status = "failed";
|
|
2241
|
+
failed.terminationReason = "process_exit_unconfirmed";
|
|
2242
|
+
failed.exitConfirmed = false;
|
|
2243
|
+
failed.errorMessage = "Child process exit was not confirmed before close";
|
|
2244
|
+
saveResult(thread.id, failed);
|
|
2245
|
+
recordSnapshot(thread, failed);
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
const session = threadSession(thread, threadMetadata);
|
|
2249
|
+
const directory = resource?.directory ?? session?.directory;
|
|
2250
|
+
const expectedDirectory = childSessionPath(ctx, thread.id)?.directory;
|
|
2251
|
+
const trustedRestoredDirectory = !resource && directory && expectedDirectory
|
|
2252
|
+
&& path.resolve(directory) === path.resolve(expectedDirectory);
|
|
2253
|
+
if ((resource?.persistent || trustedRestoredDirectory) && directory) {
|
|
2254
|
+
await rm(directory, { recursive: true, force: true });
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
threads.close(thread.id as SubagentThreadId);
|
|
2258
|
+
if (exitConfirmed) threadResources.delete(thread.id);
|
|
2259
|
+
savedResults.delete(thread.id);
|
|
2260
|
+
pruneClosedThreads();
|
|
2261
|
+
return actionResult(exitConfirmed
|
|
2262
|
+
? `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}). Heavy trace and handoff data were evicted; a tombstone remains inspectable.`
|
|
2263
|
+
: `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}); process exit was not confirmed, so its session directory was retained.`, thread.id);
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
let resumeTarget: ThreadSnapshot | undefined;
|
|
2267
|
+
let resumePrompt: string | undefined;
|
|
2268
|
+
const isResume = request.kind === "resume";
|
|
2196
2269
|
if (isResume) {
|
|
2197
2270
|
const target = resolveOwnedThread(request.input.threadId, parentId);
|
|
2198
2271
|
if (!target) throw new Error(`Unknown child thread ${JSON.stringify(request.input.threadId)}`);
|
|
2199
2272
|
if (!terminalThread(target) || target.state === "closed") {
|
|
2200
2273
|
throw new Error(`Cannot resume thread ${target.id} from ${target.state}`);
|
|
2201
2274
|
}
|
|
2275
|
+
if (savedResults.get(target.id)?.agentSource === "inline") {
|
|
2276
|
+
throw new Error(`Cannot resume inline role ${JSON.stringify(target.role)}; inline roles are scoped to one spawn`);
|
|
2277
|
+
}
|
|
2202
2278
|
resumePrompt = request.input.task;
|
|
2203
|
-
resumeTarget = target;
|
|
2204
|
-
}
|
|
2205
|
-
const spawnRequest = (isResume
|
|
2206
|
-
? { kind: "spawn-single", input: { agent: resumeTarget!.role, task: resumePrompt ?? resumeTarget!.prompt } }
|
|
2207
|
-
: request) as Extract<NormalizedSubagentRequest, { kind: "spawn-single" | "spawn-parallel" | "spawn-chain" }>;
|
|
2208
|
-
const params = spawnRequest.input;
|
|
2279
|
+
resumeTarget = target;
|
|
2280
|
+
}
|
|
2281
|
+
const spawnRequest = (isResume
|
|
2282
|
+
? { kind: "spawn-single", input: { agent: resumeTarget!.role, task: resumePrompt ?? resumeTarget!.prompt } }
|
|
2283
|
+
: request) as Extract<NormalizedSubagentRequest, { kind: "spawn-single" | "spawn-parallel" | "spawn-chain" }>;
|
|
2284
|
+
const params = spawnRequest.input;
|
|
2209
2285
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
2210
2286
|
const hasParallel = spawnRequest.kind === "spawn-parallel";
|
|
2211
2287
|
const hasChain = spawnRequest.kind === "spawn-chain";
|
|
@@ -2213,15 +2289,28 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2213
2289
|
|
|
2214
2290
|
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
2215
2291
|
const roles = new Map(discovery.agents.map((agent) => [agent.name, agent]));
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2292
|
+
const rawInputs: TaskInput[] = spawnRequest.kind === "spawn-single"
|
|
2293
|
+
? [{ agent: spawnRequest.input.agent, task: spawnRequest.input.task, ...(spawnRequest.input.name ? { name: spawnRequest.input.name } : {}) }]
|
|
2294
|
+
: spawnRequest.kind === "spawn-parallel" ? spawnRequest.input.tasks : spawnRequest.input.chain;
|
|
2295
|
+
const rolesForInputs = rawInputs.map((input) => {
|
|
2296
|
+
if (typeof input.agent !== "string") {
|
|
2297
|
+
const parentTools = new Set(pi.getActiveTools());
|
|
2298
|
+
for (const tool of input.agent.tools) {
|
|
2299
|
+
if (!parentTools.has(tool)) throw new Error(`Inline role ${JSON.stringify(input.agent.name)} tool ${JSON.stringify(tool)} is not active for the parent`);
|
|
2300
|
+
}
|
|
2301
|
+
return inlineAgentRole(input.agent);
|
|
2302
|
+
}
|
|
2303
|
+
const selected = roles.get(input.agent);
|
|
2304
|
+
if (selected) return selected;
|
|
2305
|
+
const available = discovery.agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
|
|
2306
|
+
throw new Error(`Unknown subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2307
|
+
});
|
|
2308
|
+
const inputs: Array<Omit<TaskInput, "agent"> & { agent: string }> = rawInputs.map((input, index) => ({
|
|
2309
|
+
...input,
|
|
2310
|
+
agent: rolesForInputs[index]!.name,
|
|
2311
|
+
}));
|
|
2223
2312
|
|
|
2224
|
-
const projectRoles = [...new Set(
|
|
2313
|
+
const projectRoles = [...new Set(rolesForInputs.filter((role) => role.source === "project"))];
|
|
2225
2314
|
if (projectRoles.length) {
|
|
2226
2315
|
if (!ctx.hasUI) throw new Error("Project-local subagents require interactive confirmation");
|
|
2227
2316
|
const approved = await ctx.ui.confirm(
|
|
@@ -2231,80 +2320,107 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2231
2320
|
if (!approved) throw new Error("Project-local subagents were not approved");
|
|
2232
2321
|
}
|
|
2233
2322
|
|
|
2234
|
-
const resolvedModels =
|
|
2235
|
-
for (const name of new Set(requested)) {
|
|
2236
|
-
resolvedModels.set(name, resolveAgentModel(roles.get(name)!, ctx, params.model, params.thinking));
|
|
2237
|
-
}
|
|
2323
|
+
const resolvedModels = rolesForInputs.map((role) => resolveAgentModel(role, ctx, params.model, params.thinking));
|
|
2238
2324
|
|
|
2239
|
-
const mode: SubagentDetails["mode"] = hasParallel ? "parallel" : hasChain ? "chain" : "single";
|
|
2240
|
-
const inputs: TaskInput[] = spawnRequest.kind === "spawn-single"
|
|
2241
|
-
? [{ agent: spawnRequest.input.agent, task: spawnRequest.input.task, ...(spawnRequest.input.name ? { name: spawnRequest.input.name } : {}) }]
|
|
2242
|
-
: spawnRequest.kind === "spawn-parallel" ? spawnRequest.input.tasks : spawnRequest.input.chain;
|
|
2325
|
+
const mode: SubagentDetails["mode"] = hasParallel ? "parallel" : hasChain ? "chain" : "single";
|
|
2243
2326
|
if (inputs.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
2244
2327
|
const readIndexes = hasParallel
|
|
2245
|
-
? inputs.map((input, index) => ({ input, index })).filter(({
|
|
2328
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ index }) => rolesForInputs[index]!.access === "read")
|
|
2246
2329
|
: [];
|
|
2247
2330
|
const writerIndexes = hasParallel
|
|
2248
|
-
? inputs.map((input, index) => ({ input, index })).filter(({
|
|
2331
|
+
? inputs.map((input, index) => ({ input, index })).filter(({ index }) => rolesForInputs[index]!.access === "write").map(({ index }) => index)
|
|
2249
2332
|
: [];
|
|
2250
|
-
if (writerConcurrencyOverride !== undefined && writerIndexes.length === 0) {
|
|
2251
|
-
throw new Error("writerConcurrency requires at least one write-capable role");
|
|
2252
|
-
}
|
|
2253
|
-
if (writerIndexes.length > 0 && writerConcurrencyOverride !== undefined && writerConcurrencyOverride > 1) {
|
|
2254
|
-
throw new Error("writerConcurrency above 1 is not allowed for write-capable tasks because child threads share the parent worktree; use 1");
|
|
2255
|
-
}
|
|
2256
|
-
const writerConcurrency = writerConcurrencyOverride ?? (writerIndexes.length > 0 ? 1 : limits.maxReadConcurrency);
|
|
2333
|
+
if (writerConcurrencyOverride !== undefined && writerIndexes.length === 0) {
|
|
2334
|
+
throw new Error("writerConcurrency requires at least one write-capable role");
|
|
2335
|
+
}
|
|
2336
|
+
if (writerIndexes.length > 0 && writerConcurrencyOverride !== undefined && writerConcurrencyOverride > 1) {
|
|
2337
|
+
throw new Error("writerConcurrency above 1 is not allowed for write-capable tasks because child threads share the parent worktree; use 1");
|
|
2338
|
+
}
|
|
2339
|
+
const writerConcurrency = writerConcurrencyOverride ?? (writerIndexes.length > 0 ? 1 : limits.maxReadConcurrency);
|
|
2257
2340
|
const useSharedParallelPool = hasParallel && writerIndexes.length > 0;
|
|
2258
|
-
const
|
|
2341
|
+
const scheduleNote = hasParallel
|
|
2259
2342
|
? writerIndexes.length
|
|
2260
|
-
? `Parallel schedule: all tasks run through a shared pool of up to ${writerConcurrency}${writerConcurrencyOverride === undefined ? " (safe default)" : " (explicit)"}. Write-capable tasks are serialized in the shared parent worktree.`
|
|
2343
|
+
? `Parallel schedule: all tasks run through a shared pool of up to ${writerConcurrency}${writerConcurrencyOverride === undefined ? " (safe default)" : " (explicit)"}. Write-capable tasks are serialized in the shared parent worktree.`
|
|
2261
2344
|
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
2262
2345
|
: undefined;
|
|
2346
|
+
const executionNote = scheduleNote;
|
|
2263
2347
|
|
|
2264
2348
|
const inFlight = threads.listAll().filter((thread) => ["queued", "active"].includes(thread.state)).length;
|
|
2265
2349
|
if (inFlight + inputs.length > limits.maxTasks) {
|
|
2266
2350
|
throw new Error(`At most ${limits.maxTasks} child threads may be active at once`);
|
|
2267
2351
|
}
|
|
2268
2352
|
|
|
2269
|
-
const existingThreads = threads.listAll().filter((thread) => thread.parentId === parentId).map((thread) => threadView(thread, threadMetadata));
|
|
2270
|
-
const allocatedNames = new Set<string>();
|
|
2271
|
-
if (isResume) resumeTarget = resumeThread(resumeTarget!, resumePrompt);
|
|
2272
|
-
const threadRecords = isResume
|
|
2273
|
-
? [resumeTarget!]
|
|
2274
|
-
: inputs.map((input) => {
|
|
2275
|
-
const allocated = [...allocatedNames].map((name) => ({ role: name, displayName: name } as ThreadSnapshot));
|
|
2276
|
-
const displayName = input.name ?? defaultThreadName(input.agent, [...existingThreads, ...allocated]);
|
|
2277
|
-
validateThreadName(displayName);
|
|
2278
|
-
const duplicate = [...existingThreads, ...allocated]
|
|
2279
|
-
.some((thread) => threadDisplayName(thread, threadMetadata).toLocaleLowerCase() === displayName.toLocaleLowerCase());
|
|
2280
|
-
if (duplicate) throw new Error(`Child display name ${JSON.stringify(displayName)} already exists for this parent`);
|
|
2281
|
-
allocatedNames.add(displayName.toLocaleLowerCase());
|
|
2282
|
-
const thread = threads.spawn({
|
|
2283
|
-
parentId: parentId as SubagentThreadId,
|
|
2284
|
-
role: input.agent,
|
|
2285
|
-
prompt: input.task,
|
|
2286
|
-
model: resolvedModels
|
|
2287
|
-
tools:
|
|
2288
|
-
capabilityBoundary: threadCapabilityBoundary(
|
|
2289
|
-
displayName,
|
|
2290
|
-
attempt: 1,
|
|
2291
|
-
session: { id: "killeros-pending", directory: path.join(os.tmpdir(), "killeros-subagent-pending") },
|
|
2292
|
-
} as any);
|
|
2293
|
-
const session = childSessionPath(ctx, thread.id) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
2294
|
-
threadMetadata.set(thread.id, { displayName, attempt: 1, session, persistentSession: Boolean(session.directory) });
|
|
2295
|
-
recordSpawn(thread);
|
|
2296
|
-
return thread;
|
|
2297
|
-
});
|
|
2298
|
-
const results = threadRecords.map((thread, index) => {
|
|
2299
|
-
const metadata = threadMetadata.get(thread.id)!;
|
|
2300
|
-
return makeQueuedResult(thread.id, inputs[index]!.agent, inputs[index]!.task, hasChain ? index + 1 : undefined, metadata.displayName, metadata.attempt);
|
|
2301
|
-
});
|
|
2302
|
-
const batchSessionGeneration = sessionGeneration;
|
|
2303
|
-
|
|
2304
|
-
const
|
|
2305
|
-
|
|
2306
|
-
|
|
2353
|
+
const existingThreads = threads.listAll().filter((thread) => thread.parentId === parentId).map((thread) => threadView(thread, threadMetadata));
|
|
2354
|
+
const allocatedNames = new Set<string>();
|
|
2355
|
+
if (isResume) resumeTarget = resumeThread(resumeTarget!, resumePrompt);
|
|
2356
|
+
const threadRecords = isResume
|
|
2357
|
+
? [resumeTarget!]
|
|
2358
|
+
: inputs.map((input, index) => {
|
|
2359
|
+
const allocated = [...allocatedNames].map((name) => ({ role: name, displayName: name } as ThreadSnapshot));
|
|
2360
|
+
const displayName = input.name ?? defaultThreadName(input.agent, [...existingThreads, ...allocated]);
|
|
2361
|
+
validateThreadName(displayName);
|
|
2362
|
+
const duplicate = [...existingThreads, ...allocated]
|
|
2363
|
+
.some((thread) => threadDisplayName(thread, threadMetadata).toLocaleLowerCase() === displayName.toLocaleLowerCase());
|
|
2364
|
+
if (duplicate) throw new Error(`Child display name ${JSON.stringify(displayName)} already exists for this parent`);
|
|
2365
|
+
allocatedNames.add(displayName.toLocaleLowerCase());
|
|
2366
|
+
const thread = threads.spawn({
|
|
2367
|
+
parentId: parentId as SubagentThreadId,
|
|
2368
|
+
role: input.agent,
|
|
2369
|
+
prompt: input.task,
|
|
2370
|
+
model: resolvedModels[index]!.model,
|
|
2371
|
+
tools: rolesForInputs[index]!.tools,
|
|
2372
|
+
capabilityBoundary: threadCapabilityBoundary(rolesForInputs[index]!),
|
|
2373
|
+
displayName,
|
|
2374
|
+
attempt: 1,
|
|
2375
|
+
session: { id: "killeros-pending", directory: path.join(os.tmpdir(), "killeros-subagent-pending") },
|
|
2376
|
+
} as any);
|
|
2377
|
+
const session = childSessionPath(ctx, thread.id) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
2378
|
+
threadMetadata.set(thread.id, { displayName, attempt: 1, session, persistentSession: Boolean(session.directory) });
|
|
2379
|
+
recordSpawn(thread);
|
|
2380
|
+
return thread;
|
|
2381
|
+
});
|
|
2382
|
+
const results = threadRecords.map((thread, index) => {
|
|
2383
|
+
const metadata = threadMetadata.get(thread.id)!;
|
|
2384
|
+
return makeQueuedResult(thread.id, inputs[index]!.agent, inputs[index]!.task, hasChain ? index + 1 : undefined, metadata.displayName, metadata.attempt);
|
|
2385
|
+
});
|
|
2386
|
+
const batchSessionGeneration = sessionGeneration;
|
|
2387
|
+
const liveWidgetKey = `killeros-subagents:${results[0]!.id}`;
|
|
2388
|
+
const showLiveWidget = options.awaitSpawnCompletion !== true && ctx.hasUI;
|
|
2389
|
+
const updateLiveWidget = (currentResults: SubagentTaskResult[]): void => {
|
|
2390
|
+
if (!showLiveWidget) return;
|
|
2391
|
+
const board = formatThreadBoard({
|
|
2392
|
+
title: `Subagents · ${mode} · live`,
|
|
2393
|
+
threads: currentResults.map(threadBoardRecord),
|
|
2394
|
+
});
|
|
2395
|
+
const row = (task: (typeof board.active)[number]): string => `${task.state.label} · ${task.displayName ?? task.agent} · ${task.usage.text}`;
|
|
2396
|
+
try {
|
|
2397
|
+
ctx.ui.setWidget(liveWidgetKey, [
|
|
2398
|
+
board.title,
|
|
2399
|
+
`Active (${board.active.length})`,
|
|
2400
|
+
...(board.active.length ? board.active.map(row) : ["None"]),
|
|
2401
|
+
`Done (${board.done.length})`,
|
|
2402
|
+
...(board.done.length ? board.done.map(row) : ["None"]),
|
|
2403
|
+
`Total · ${formatUsage(aggregateUsage(currentResults))}`,
|
|
2404
|
+
]);
|
|
2405
|
+
} catch {
|
|
2406
|
+
// Live UI updates must not fail the child batch.
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
const clearLiveWidget = (): void => {
|
|
2410
|
+
if (!showLiveWidget) return;
|
|
2411
|
+
try {
|
|
2412
|
+
ctx.ui.setWidget(liveWidgetKey, undefined);
|
|
2413
|
+
} catch {
|
|
2414
|
+
// The session may close before the child batch settles.
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
let updatesOpen = true;
|
|
2418
|
+
const emit = (message = `${mode}: ${results.filter((result) => !["queued", "running"].includes(result.status)).length}/${results.length} settled`): void => {
|
|
2419
|
+
if (batchSessionGeneration !== sessionGeneration) return;
|
|
2420
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2307
2421
|
const currentResults = results.map(cloneResult);
|
|
2422
|
+
updateLiveWidget(currentResults);
|
|
2423
|
+
if (!updatesOpen) return;
|
|
2308
2424
|
try {
|
|
2309
2425
|
(onUpdate as ToolUpdate | undefined)?.({
|
|
2310
2426
|
content: [{ type: "text", text: message }],
|
|
@@ -2313,10 +2429,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2313
2429
|
} catch {
|
|
2314
2430
|
// Host update callbacks are telemetry; failures must not fail the batch.
|
|
2315
2431
|
}
|
|
2316
|
-
};
|
|
2317
|
-
const failQueuedTask = (index: number, reason: string, message: string): void => {
|
|
2318
|
-
if (batchSessionGeneration !== sessionGeneration) return;
|
|
2319
|
-
const threadId = threadRecords[index]!.id;
|
|
2432
|
+
};
|
|
2433
|
+
const failQueuedTask = (index: number, reason: string, message: string): void => {
|
|
2434
|
+
if (batchSessionGeneration !== sessionGeneration) return;
|
|
2435
|
+
const threadId = threadRecords[index]!.id;
|
|
2320
2436
|
const thread = threads.inspect(threadId);
|
|
2321
2437
|
if (thread?.state === "queued") threads.begin(threadId);
|
|
2322
2438
|
results[index] = {
|
|
@@ -2328,13 +2444,13 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2328
2444
|
if (threads.inspect(threadId)?.state === "active") threads.fail(threadId, { message, code: reason });
|
|
2329
2445
|
saveResult(threadId, results[index]!);
|
|
2330
2446
|
emit();
|
|
2331
|
-
};
|
|
2332
|
-
const runAt = async (index: number, task: string): Promise<void> => {
|
|
2333
|
-
if (batchSessionGeneration !== sessionGeneration) {
|
|
2334
|
-
results[index] = { ...results[index]!, status: "cancelled", terminationReason: "session_start" };
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
const threadId = threadRecords[index]!.id;
|
|
2447
|
+
};
|
|
2448
|
+
const runAt = async (index: number, task: string): Promise<void> => {
|
|
2449
|
+
if (batchSessionGeneration !== sessionGeneration) {
|
|
2450
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: "session_start" };
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
const threadId = threadRecords[index]!.id;
|
|
2338
2454
|
const initialThread = threads.inspect(threadId);
|
|
2339
2455
|
if (signal?.aborted) {
|
|
2340
2456
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: "abort" };
|
|
@@ -2357,29 +2473,29 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2357
2473
|
return;
|
|
2358
2474
|
}
|
|
2359
2475
|
if (initialThread.state !== "queued") return;
|
|
2360
|
-
const input = inputs[index]!;
|
|
2361
|
-
threads.begin(threadId);
|
|
2362
|
-
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
2363
|
-
if (codePointLength(task) > limits.taskCharacters) {
|
|
2364
|
-
failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
|
|
2365
|
-
return;
|
|
2366
|
-
}
|
|
2367
|
-
if (steeredTaskWouldExceedLimit(task, queuedSteering, limits.taskCharacters)) {
|
|
2368
|
-
failQueuedTask(index, "steering_task_limit", `Expanded task plus steering exceeds ${limits.taskCharacters} characters`);
|
|
2369
|
-
return;
|
|
2370
|
-
}
|
|
2371
|
-
const controller = new AbortController();
|
|
2372
|
-
const runtime: ActiveThreadRuntime = {
|
|
2373
|
-
controller,
|
|
2374
|
-
handles: new Set(),
|
|
2375
|
-
task,
|
|
2376
|
-
steering: [],
|
|
2377
|
-
restarting: false,
|
|
2378
|
-
traceCount: 0,
|
|
2379
|
-
startedAt: Date.now(),
|
|
2380
|
-
sessionGeneration: batchSessionGeneration,
|
|
2381
|
-
aggregate: isResume ? savedResults.get(threadId) && cloneResult(savedResults.get(threadId)!) : undefined,
|
|
2382
|
-
};
|
|
2476
|
+
const input = inputs[index]!;
|
|
2477
|
+
threads.begin(threadId);
|
|
2478
|
+
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
2479
|
+
if (codePointLength(task) > limits.taskCharacters) {
|
|
2480
|
+
failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
if (steeredTaskWouldExceedLimit(task, queuedSteering, limits.taskCharacters)) {
|
|
2484
|
+
failQueuedTask(index, "steering_task_limit", `Expanded task plus steering exceeds ${limits.taskCharacters} characters`);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const controller = new AbortController();
|
|
2488
|
+
const runtime: ActiveThreadRuntime = {
|
|
2489
|
+
controller,
|
|
2490
|
+
handles: new Set(),
|
|
2491
|
+
task,
|
|
2492
|
+
steering: [],
|
|
2493
|
+
restarting: false,
|
|
2494
|
+
traceCount: 0,
|
|
2495
|
+
startedAt: Date.now(),
|
|
2496
|
+
sessionGeneration: batchSessionGeneration,
|
|
2497
|
+
aggregate: isResume ? savedResults.get(threadId) && cloneResult(savedResults.get(threadId)!) : undefined,
|
|
2498
|
+
};
|
|
2383
2499
|
const abortFromParent = (): void => {
|
|
2384
2500
|
runtime.restarting = false;
|
|
2385
2501
|
runtime.requestedReason = "abort";
|
|
@@ -2389,30 +2505,30 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2389
2505
|
signal?.addEventListener("abort", abortFromParent, { once: true });
|
|
2390
2506
|
if (signal?.aborted) abortFromParent();
|
|
2391
2507
|
activeRuntimes.set(threadId, runtime);
|
|
2392
|
-
let sessionDirectory: string;
|
|
2393
|
-
let persistentSession = false;
|
|
2394
|
-
try {
|
|
2395
|
-
const metadata = threadMetadata.get(threadId)!;
|
|
2396
|
-
if (metadata.persistentSession && metadata.session.directory) {
|
|
2397
|
-
sessionDirectory = metadata.session.directory;
|
|
2398
|
-
await mkdir(sessionDirectory, { recursive: true, mode: 0o700 });
|
|
2399
|
-
persistentSession = true;
|
|
2400
|
-
} else {
|
|
2401
|
-
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
2402
|
-
}
|
|
2403
|
-
const existingResource = threadResources.get(threadId);
|
|
2404
|
-
threadResources.set(threadId, existingResource ?? { directory: sessionDirectory, persistent: persistentSession, handles: new Set() });
|
|
2405
|
-
const resource = threadResources.get(threadId)!;
|
|
2406
|
-
resource.directory = sessionDirectory;
|
|
2407
|
-
resource.persistent = persistentSession || resource.persistent;
|
|
2408
|
-
} catch (error) {
|
|
2409
|
-
signal?.removeEventListener("abort", abortFromParent);
|
|
2410
|
-
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2411
|
-
if (runtime.sessionGeneration !== sessionGeneration) {
|
|
2412
|
-
results[index] = { ...results[index]!, status: "cancelled", terminationReason: runtime.requestedReason ?? "session_start" };
|
|
2413
|
-
return;
|
|
2414
|
-
}
|
|
2415
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2508
|
+
let sessionDirectory: string;
|
|
2509
|
+
let persistentSession = false;
|
|
2510
|
+
try {
|
|
2511
|
+
const metadata = threadMetadata.get(threadId)!;
|
|
2512
|
+
if (metadata.persistentSession && metadata.session.directory) {
|
|
2513
|
+
sessionDirectory = metadata.session.directory;
|
|
2514
|
+
await mkdir(sessionDirectory, { recursive: true, mode: 0o700 });
|
|
2515
|
+
persistentSession = true;
|
|
2516
|
+
} else {
|
|
2517
|
+
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
2518
|
+
}
|
|
2519
|
+
const existingResource = threadResources.get(threadId);
|
|
2520
|
+
threadResources.set(threadId, existingResource ?? { directory: sessionDirectory, persistent: persistentSession, handles: new Set() });
|
|
2521
|
+
const resource = threadResources.get(threadId)!;
|
|
2522
|
+
resource.directory = sessionDirectory;
|
|
2523
|
+
resource.persistent = persistentSession || resource.persistent;
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
2526
|
+
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2527
|
+
if (runtime.sessionGeneration !== sessionGeneration) {
|
|
2528
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: runtime.requestedReason ?? "session_start" };
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2416
2532
|
results[index] = {
|
|
2417
2533
|
...results[index]!,
|
|
2418
2534
|
status: "failed",
|
|
@@ -2424,35 +2540,35 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2424
2540
|
emit();
|
|
2425
2541
|
return;
|
|
2426
2542
|
}
|
|
2427
|
-
const currentThread = threads.inspect(threadId);
|
|
2428
|
-
if (controller.signal.aborted || threads.isDisposed || currentThread?.state !== "active") {
|
|
2429
|
-
signal?.removeEventListener("abort", abortFromParent);
|
|
2430
|
-
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2431
|
-
if (!persistentSession) {
|
|
2432
|
-
try {
|
|
2433
|
-
await rm(sessionDirectory, { recursive: true, force: true });
|
|
2434
|
-
} catch {
|
|
2435
|
-
// Temporary child session cleanup is best effort before process startup.
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
if (runtime.sessionGeneration !== sessionGeneration) {
|
|
2439
|
-
results[index] = { ...results[index]!, status: "cancelled", terminationReason: runtime.requestedReason ?? "session_start" };
|
|
2440
|
-
return;
|
|
2441
|
-
}
|
|
2442
|
-
const reason = runtime.requestedReason ?? currentThread?.stopReason ?? (threads.isDisposed ? "session_shutdown" : "interrupted");
|
|
2543
|
+
const currentThread = threads.inspect(threadId);
|
|
2544
|
+
if (controller.signal.aborted || threads.isDisposed || currentThread?.state !== "active") {
|
|
2545
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
2546
|
+
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2547
|
+
if (!persistentSession) {
|
|
2548
|
+
try {
|
|
2549
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
2550
|
+
} catch {
|
|
2551
|
+
// Temporary child session cleanup is best effort before process startup.
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (runtime.sessionGeneration !== sessionGeneration) {
|
|
2555
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: runtime.requestedReason ?? "session_start" };
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const reason = runtime.requestedReason ?? currentThread?.stopReason ?? (threads.isDisposed ? "session_shutdown" : "interrupted");
|
|
2443
2559
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: reason };
|
|
2444
2560
|
if (!threads.isDisposed && currentThread?.state === "active") threads.stop(threadId, { reason });
|
|
2445
2561
|
saveResult(threadId, results[index]!);
|
|
2446
2562
|
emit();
|
|
2447
2563
|
return;
|
|
2448
2564
|
}
|
|
2449
|
-
const metadata = threadMetadata.get(threadId)!;
|
|
2450
|
-
const sessionId = metadata.session.id;
|
|
2451
|
-
const agent =
|
|
2452
|
-
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
2453
|
-
const stopForBudget = (reason: string, message: string): void => {
|
|
2454
|
-
if (runtime.sessionGeneration !== sessionGeneration) return;
|
|
2455
|
-
const limited = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2565
|
+
const metadata = threadMetadata.get(threadId)!;
|
|
2566
|
+
const sessionId = metadata.session.id;
|
|
2567
|
+
const agent = rolesForInputs[index]!;
|
|
2568
|
+
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
2569
|
+
const stopForBudget = (reason: string, message: string): void => {
|
|
2570
|
+
if (runtime.sessionGeneration !== sessionGeneration) return;
|
|
2571
|
+
const limited = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2456
2572
|
limited.status = "limited";
|
|
2457
2573
|
limited.terminationReason = reason;
|
|
2458
2574
|
limited.errorMessage = message;
|
|
@@ -2472,7 +2588,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2472
2588
|
try {
|
|
2473
2589
|
while (true) {
|
|
2474
2590
|
const aggregate = runtime.aggregate;
|
|
2475
|
-
const wallTimeMs = limits.wallTimeMs ?? agent.timeoutMs ?? limits.defaultWallTimeMs;
|
|
2591
|
+
const wallTimeMs = limits.wallTimeMs ?? agent.timeoutMs ?? limits.defaultWallTimeMs;
|
|
2476
2592
|
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
2477
2593
|
const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
|
|
2478
2594
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
@@ -2504,15 +2620,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2504
2620
|
break;
|
|
2505
2621
|
}
|
|
2506
2622
|
runtime.traceCount = 0;
|
|
2507
|
-
const next = await runTask({
|
|
2623
|
+
const next = await runTask({
|
|
2508
2624
|
cwd: ctx.cwd,
|
|
2509
|
-
agent
|
|
2625
|
+
agent,
|
|
2510
2626
|
task: currentTask,
|
|
2511
|
-
id: results[index]!.id,
|
|
2512
|
-
displayName: metadata.displayName,
|
|
2513
|
-
attempt: metadata.attempt,
|
|
2627
|
+
id: results[index]!.id,
|
|
2628
|
+
displayName: metadata.displayName,
|
|
2629
|
+
attempt: metadata.attempt,
|
|
2514
2630
|
step: results[index]!.step,
|
|
2515
|
-
model: resolvedModels
|
|
2631
|
+
model: resolvedModels[index]!,
|
|
2516
2632
|
signal: controller.signal,
|
|
2517
2633
|
webExtension: options.webExtension,
|
|
2518
2634
|
projectTrusted: ctx.isProjectTrusted(),
|
|
@@ -2528,60 +2644,60 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2528
2644
|
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
2529
2645
|
},
|
|
2530
2646
|
timeoutMs: remainingWallTimeMs,
|
|
2531
|
-
onHandle: (handle) => {
|
|
2532
|
-
runtime.handle = handle;
|
|
2533
|
-
runtime.handles.add(handle);
|
|
2534
|
-
threadResources.get(threadId)?.handles.add(handle);
|
|
2535
|
-
},
|
|
2647
|
+
onHandle: (handle) => {
|
|
2648
|
+
runtime.handle = handle;
|
|
2649
|
+
runtime.handles.add(handle);
|
|
2650
|
+
threadResources.get(threadId)?.handles.add(handle);
|
|
2651
|
+
},
|
|
2536
2652
|
onChange: (changed) => {
|
|
2537
2653
|
results[index] = syncThread(threadId, changed, runtime);
|
|
2538
2654
|
emit();
|
|
2539
2655
|
},
|
|
2540
|
-
});
|
|
2541
|
-
next.task = task;
|
|
2542
|
-
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
2543
|
-
runtime.aggregate.task = task;
|
|
2544
|
-
if (next.status === "cancelled" && runtime.requestedReason !== undefined) {
|
|
2545
|
-
runtime.aggregate.terminationReason = runtime.requestedReason;
|
|
2546
|
-
}
|
|
2547
|
-
results[index] = cloneResult(runtime.aggregate);
|
|
2548
|
-
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, runtime.aggregate);
|
|
2549
|
-
const shouldRestart = runtime.sessionGeneration === sessionGeneration
|
|
2550
|
-
&& runtime.steering.length > 0
|
|
2551
|
-
&& !controller.signal.aborted
|
|
2552
|
-
&& (runtime.restarting || next.status === "complete" || next.status === "cancelled");
|
|
2553
|
-
if (!shouldRestart) break;
|
|
2554
|
-
const previousHandle = runtime.handle;
|
|
2555
|
-
if (previousHandle && !(await waitForConfirmedProcessExit(previousHandle))) {
|
|
2556
|
-
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) {
|
|
2557
|
-
const cancelled = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2558
|
-
cancelled.status = "cancelled";
|
|
2559
|
-
cancelled.terminationReason = runtime.requestedReason ?? (threads.isDisposed ? "session_shutdown" : "abort");
|
|
2560
|
-
runtime.aggregate = cancelled;
|
|
2561
|
-
results[index] = cloneResult(cancelled);
|
|
2562
|
-
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2563
|
-
saveResult(threadId, cancelled);
|
|
2564
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2565
|
-
threads.stop(threadId, {
|
|
2566
|
-
usage: threadUsage(cancelled.usage),
|
|
2567
|
-
result: cancelled.output || undefined,
|
|
2568
|
-
handoff: cancelled.output ? { summary: cancelled.output } : undefined,
|
|
2569
|
-
reason: cancelled.terminationReason,
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
|
-
emit();
|
|
2573
|
-
}
|
|
2574
|
-
break;
|
|
2575
|
-
}
|
|
2576
|
-
const message = "Child process exit was not confirmed before the steering restart";
|
|
2656
|
+
});
|
|
2657
|
+
next.task = task;
|
|
2658
|
+
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
2659
|
+
runtime.aggregate.task = task;
|
|
2660
|
+
if (next.status === "cancelled" && runtime.requestedReason !== undefined) {
|
|
2661
|
+
runtime.aggregate.terminationReason = runtime.requestedReason;
|
|
2662
|
+
}
|
|
2663
|
+
results[index] = cloneResult(runtime.aggregate);
|
|
2664
|
+
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, runtime.aggregate);
|
|
2665
|
+
const shouldRestart = runtime.sessionGeneration === sessionGeneration
|
|
2666
|
+
&& runtime.steering.length > 0
|
|
2667
|
+
&& !controller.signal.aborted
|
|
2668
|
+
&& (runtime.restarting || next.status === "complete" || next.status === "cancelled");
|
|
2669
|
+
if (!shouldRestart) break;
|
|
2670
|
+
const previousHandle = runtime.handle;
|
|
2671
|
+
if (previousHandle && !(await waitForConfirmedProcessExit(previousHandle))) {
|
|
2672
|
+
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) {
|
|
2673
|
+
const cancelled = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2674
|
+
cancelled.status = "cancelled";
|
|
2675
|
+
cancelled.terminationReason = runtime.requestedReason ?? (threads.isDisposed ? "session_shutdown" : "abort");
|
|
2676
|
+
runtime.aggregate = cancelled;
|
|
2677
|
+
results[index] = cloneResult(cancelled);
|
|
2678
|
+
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2679
|
+
saveResult(threadId, cancelled);
|
|
2680
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2681
|
+
threads.stop(threadId, {
|
|
2682
|
+
usage: threadUsage(cancelled.usage),
|
|
2683
|
+
result: cancelled.output || undefined,
|
|
2684
|
+
handoff: cancelled.output ? { summary: cancelled.output } : undefined,
|
|
2685
|
+
reason: cancelled.terminationReason,
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
emit();
|
|
2689
|
+
}
|
|
2690
|
+
break;
|
|
2691
|
+
}
|
|
2692
|
+
const message = "Child process exit was not confirmed before the steering restart";
|
|
2577
2693
|
const unconfirmed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2578
2694
|
unconfirmed.status = "failed";
|
|
2579
2695
|
unconfirmed.terminationReason = "process_exit_unconfirmed";
|
|
2580
2696
|
unconfirmed.errorMessage = message;
|
|
2581
2697
|
runtime.aggregate = unconfirmed;
|
|
2582
2698
|
results[index] = cloneResult(unconfirmed);
|
|
2583
|
-
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, unconfirmed);
|
|
2584
|
-
if (runtime.sessionGeneration === sessionGeneration && !threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2699
|
+
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, unconfirmed);
|
|
2700
|
+
if (runtime.sessionGeneration === sessionGeneration && !threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2585
2701
|
threads.fail(threadId, {
|
|
2586
2702
|
usage: threadUsage(unconfirmed.usage),
|
|
2587
2703
|
result: unconfirmed.output || undefined,
|
|
@@ -2592,9 +2708,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2592
2708
|
}
|
|
2593
2709
|
emit();
|
|
2594
2710
|
break;
|
|
2595
|
-
}
|
|
2596
|
-
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) break;
|
|
2597
|
-
const steering = runtime.steering.splice(0);
|
|
2711
|
+
}
|
|
2712
|
+
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) break;
|
|
2713
|
+
const steering = runtime.steering.splice(0);
|
|
2598
2714
|
runtime.restarting = false;
|
|
2599
2715
|
runtime.requestedReason = undefined;
|
|
2600
2716
|
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
@@ -2604,88 +2720,88 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2604
2720
|
handoff: runtime.aggregate.output ? { summary: runtime.aggregate.output } : undefined,
|
|
2605
2721
|
});
|
|
2606
2722
|
}
|
|
2607
|
-
if (steeredTaskWouldExceedLimit(task, steering, limits.taskCharacters)) {
|
|
2608
|
-
const failed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2609
|
-
failed.status = "failed";
|
|
2610
|
-
failed.terminationReason = "steering_task_limit";
|
|
2611
|
-
failed.errorMessage = `Expanded task plus steering exceeds ${limits.taskCharacters} characters`;
|
|
2612
|
-
runtime.aggregate = failed;
|
|
2613
|
-
results[index] = cloneResult(failed);
|
|
2614
|
-
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2615
|
-
saveResult(threadId, failed);
|
|
2616
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2617
|
-
threads.fail(threadId, {
|
|
2618
|
-
usage: threadUsage(failed.usage),
|
|
2619
|
-
result: failed.output || undefined,
|
|
2620
|
-
handoff: failed.output ? { summary: failed.output } : undefined,
|
|
2621
|
-
message: failed.errorMessage,
|
|
2622
|
-
code: failed.terminationReason,
|
|
2623
|
-
});
|
|
2624
|
-
}
|
|
2625
|
-
emit();
|
|
2626
|
-
}
|
|
2627
|
-
break;
|
|
2628
|
-
}
|
|
2629
|
-
currentTask = buildSteeredTask(task, steering, limits.taskCharacters);
|
|
2630
|
-
}
|
|
2631
|
-
} finally {
|
|
2632
|
-
signal?.removeEventListener("abort", abortFromParent);
|
|
2633
|
-
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2634
|
-
const handles = [...runtime.handles];
|
|
2635
|
-
const exitStates = await Promise.all(handles.map((handle) => waitForConfirmedProcessExit(handle, limits.processExitWaitMs)));
|
|
2636
|
-
const allExited = exitStates.every(Boolean);
|
|
2637
|
-
if (!allExited) {
|
|
2638
|
-
const unconfirmed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2639
|
-
const requestedReason = runtime.requestedReason;
|
|
2640
|
-
const strongReasons = new Set(["abort", "interrupt", "session_start", "session_shutdown", "malformed_jsonl", "invalid_usage", "spawn_error"]);
|
|
2641
|
-
if (!requestedReason || !strongReasons.has(requestedReason)) {
|
|
2642
|
-
unconfirmed.status = "failed";
|
|
2643
|
-
unconfirmed.terminationReason = "process_exit_unconfirmed";
|
|
2644
|
-
unconfirmed.errorMessage = "Child process exit was not confirmed before cleanup";
|
|
2645
|
-
}
|
|
2646
|
-
unconfirmed.exitConfirmed = false;
|
|
2647
|
-
runtime.aggregate = unconfirmed;
|
|
2648
|
-
results[index] = cloneResult(unconfirmed);
|
|
2649
|
-
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2650
|
-
saveResult(threadId, unconfirmed);
|
|
2651
|
-
const current = threads.inspect(threadId);
|
|
2652
|
-
if (current?.state === "active") {
|
|
2653
|
-
if (unconfirmed.status === "failed") {
|
|
2654
|
-
threads.fail(threadId, {
|
|
2655
|
-
usage: threadUsage(unconfirmed.usage),
|
|
2656
|
-
result: unconfirmed.output || undefined,
|
|
2657
|
-
handoff: unconfirmed.output ? { summary: unconfirmed.output } : undefined,
|
|
2658
|
-
message: unconfirmed.errorMessage ?? "Child process exit was not confirmed before cleanup",
|
|
2659
|
-
code: unconfirmed.terminationReason,
|
|
2660
|
-
});
|
|
2661
|
-
} else {
|
|
2662
|
-
threads.stop(threadId, { reason: unconfirmed.terminationReason ?? "process_exit_unconfirmed" });
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
emit();
|
|
2666
|
-
}
|
|
2667
|
-
} else if (!persistentSession) {
|
|
2668
|
-
try {
|
|
2669
|
-
await rm(sessionDirectory, { recursive: true, force: true });
|
|
2670
|
-
} catch {
|
|
2671
|
-
// Temporary child session cleanup is best effort after process termination.
|
|
2672
|
-
}
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2723
|
+
if (steeredTaskWouldExceedLimit(task, steering, limits.taskCharacters)) {
|
|
2724
|
+
const failed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2725
|
+
failed.status = "failed";
|
|
2726
|
+
failed.terminationReason = "steering_task_limit";
|
|
2727
|
+
failed.errorMessage = `Expanded task plus steering exceeds ${limits.taskCharacters} characters`;
|
|
2728
|
+
runtime.aggregate = failed;
|
|
2729
|
+
results[index] = cloneResult(failed);
|
|
2730
|
+
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2731
|
+
saveResult(threadId, failed);
|
|
2732
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2733
|
+
threads.fail(threadId, {
|
|
2734
|
+
usage: threadUsage(failed.usage),
|
|
2735
|
+
result: failed.output || undefined,
|
|
2736
|
+
handoff: failed.output ? { summary: failed.output } : undefined,
|
|
2737
|
+
message: failed.errorMessage,
|
|
2738
|
+
code: failed.terminationReason,
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
emit();
|
|
2742
|
+
}
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
currentTask = buildSteeredTask(task, steering, limits.taskCharacters);
|
|
2746
|
+
}
|
|
2747
|
+
} finally {
|
|
2748
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
2749
|
+
if (activeRuntimes.get(threadId) === runtime) activeRuntimes.delete(threadId);
|
|
2750
|
+
const handles = [...runtime.handles];
|
|
2751
|
+
const exitStates = await Promise.all(handles.map((handle) => waitForConfirmedProcessExit(handle, limits.processExitWaitMs)));
|
|
2752
|
+
const allExited = exitStates.every(Boolean);
|
|
2753
|
+
if (!allExited) {
|
|
2754
|
+
const unconfirmed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
2755
|
+
const requestedReason = runtime.requestedReason;
|
|
2756
|
+
const strongReasons = new Set(["abort", "interrupt", "session_start", "session_shutdown", "malformed_jsonl", "invalid_usage", "spawn_error"]);
|
|
2757
|
+
if (!requestedReason || !strongReasons.has(requestedReason)) {
|
|
2758
|
+
unconfirmed.status = "failed";
|
|
2759
|
+
unconfirmed.terminationReason = "process_exit_unconfirmed";
|
|
2760
|
+
unconfirmed.errorMessage = "Child process exit was not confirmed before cleanup";
|
|
2761
|
+
}
|
|
2762
|
+
unconfirmed.exitConfirmed = false;
|
|
2763
|
+
runtime.aggregate = unconfirmed;
|
|
2764
|
+
results[index] = cloneResult(unconfirmed);
|
|
2765
|
+
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2766
|
+
saveResult(threadId, unconfirmed);
|
|
2767
|
+
const current = threads.inspect(threadId);
|
|
2768
|
+
if (current?.state === "active") {
|
|
2769
|
+
if (unconfirmed.status === "failed") {
|
|
2770
|
+
threads.fail(threadId, {
|
|
2771
|
+
usage: threadUsage(unconfirmed.usage),
|
|
2772
|
+
result: unconfirmed.output || undefined,
|
|
2773
|
+
handoff: unconfirmed.output ? { summary: unconfirmed.output } : undefined,
|
|
2774
|
+
message: unconfirmed.errorMessage ?? "Child process exit was not confirmed before cleanup",
|
|
2775
|
+
code: unconfirmed.terminationReason,
|
|
2776
|
+
});
|
|
2777
|
+
} else {
|
|
2778
|
+
threads.stop(threadId, { reason: unconfirmed.terminationReason ?? "process_exit_unconfirmed" });
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
emit();
|
|
2782
|
+
}
|
|
2783
|
+
} else if (!persistentSession) {
|
|
2784
|
+
try {
|
|
2785
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
2786
|
+
} catch {
|
|
2787
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2675
2791
|
emit();
|
|
2676
2792
|
};
|
|
2677
2793
|
|
|
2678
|
-
const settleQueued = (reason: string): void => {
|
|
2679
|
-
if (batchSessionGeneration !== sessionGeneration) {
|
|
2680
|
-
for (const result of results) {
|
|
2681
|
-
if (result.status === "queued") {
|
|
2682
|
-
result.status = "cancelled";
|
|
2683
|
-
result.terminationReason = "session_start";
|
|
2684
|
-
}
|
|
2685
|
-
}
|
|
2686
|
-
return;
|
|
2687
|
-
}
|
|
2688
|
-
for (let index = 0; index < results.length; index += 1) {
|
|
2794
|
+
const settleQueued = (reason: string): void => {
|
|
2795
|
+
if (batchSessionGeneration !== sessionGeneration) {
|
|
2796
|
+
for (const result of results) {
|
|
2797
|
+
if (result.status === "queued") {
|
|
2798
|
+
result.status = "cancelled";
|
|
2799
|
+
result.terminationReason = "session_start";
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
2689
2805
|
const result = results[index]!;
|
|
2690
2806
|
if (result.status !== "queued") continue;
|
|
2691
2807
|
const thread = threads.inspect(threadRecords[index]!.id);
|
|
@@ -2731,11 +2847,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2731
2847
|
await runAt(0, inputs[0]!.task);
|
|
2732
2848
|
}
|
|
2733
2849
|
|
|
2734
|
-
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2850
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2735
2851
|
const currentResults = results.map(cloneResult);
|
|
2736
2852
|
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
2853
|
+
const toolContent = buildToolContent(mode, details.results, limits.toolOutputBytes);
|
|
2737
2854
|
return {
|
|
2738
|
-
content: [{
|
|
2855
|
+
content: [{
|
|
2856
|
+
type: "text" as const,
|
|
2857
|
+
text: executionNote
|
|
2858
|
+
? boundedText(`${executionNote}\n\n${toolContent}`, limits.toolOutputBytes, "\n\n[Spawn output truncated.]")
|
|
2859
|
+
: toolContent,
|
|
2860
|
+
}],
|
|
2739
2861
|
details,
|
|
2740
2862
|
usage: details.aggregateUsage,
|
|
2741
2863
|
};
|
|
@@ -2752,7 +2874,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2752
2874
|
}
|
|
2753
2875
|
}
|
|
2754
2876
|
|
|
2755
|
-
const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2877
|
+
const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2756
2878
|
const queuedResults = results.map(cloneResult);
|
|
2757
2879
|
const queuedDetails: SubagentDetails = {
|
|
2758
2880
|
...queuedBoard,
|
|
@@ -2760,13 +2882,21 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2760
2882
|
results: queuedResults,
|
|
2761
2883
|
aggregateUsage: aggregateUsage(queuedResults),
|
|
2762
2884
|
};
|
|
2763
|
-
const threadList = threadRecords.map((thread) => `${threadDisplayName(threadView(thread, threadMetadata), threadMetadata)} (${thread.id})`).join(", ");
|
|
2885
|
+
const threadList = threadRecords.map((thread) => `${threadDisplayName(threadView(thread, threadMetadata), threadMetadata)} (${thread.id})`).join(", ");
|
|
2886
|
+
const reportUndeliveredFollowUp = (outcome: "settled" | "failed"): void => {
|
|
2887
|
+
if (!ctx.hasUI) return;
|
|
2888
|
+
try {
|
|
2889
|
+
ctx.ui.notify(`Subagent batch ${outcome}, but its follow-up could not be delivered. Use list or collect for the saved result.`, "warning");
|
|
2890
|
+
} catch {
|
|
2891
|
+
// The thread registry still retains the result when the UI is closing.
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2764
2894
|
updatesOpen = false;
|
|
2765
|
-
const backgroundBatch = finishBatch().then((completed) => {
|
|
2766
|
-
if (batchSessionGeneration !== sessionGeneration
|
|
2767
|
-
|| threads.isDisposed
|
|
2768
|
-
|| signal?.aborted
|
|
2769
|
-
|| completed.details.results.some((result) => result.terminationReason === "abort")) return;
|
|
2895
|
+
const backgroundBatch = finishBatch().then((completed) => {
|
|
2896
|
+
if (batchSessionGeneration !== sessionGeneration
|
|
2897
|
+
|| threads.isDisposed
|
|
2898
|
+
|| signal?.aborted
|
|
2899
|
+
|| completed.details.results.some((result) => result.terminationReason === "abort")) return;
|
|
2770
2900
|
try {
|
|
2771
2901
|
pi.sendMessage({
|
|
2772
2902
|
customType: "killeros-subagent-settled",
|
|
@@ -2774,10 +2904,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2774
2904
|
display: true,
|
|
2775
2905
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2776
2906
|
} catch {
|
|
2777
|
-
|
|
2907
|
+
reportUndeliveredFollowUp("settled");
|
|
2778
2908
|
}
|
|
2779
|
-
}).catch((error) => {
|
|
2780
|
-
if (batchSessionGeneration !== sessionGeneration || threads.isDisposed || signal?.aborted) return;
|
|
2909
|
+
}).catch((error) => {
|
|
2910
|
+
if (batchSessionGeneration !== sessionGeneration || threads.isDisposed || signal?.aborted) return;
|
|
2781
2911
|
const message = error instanceof Error ? error.message : String(error);
|
|
2782
2912
|
try {
|
|
2783
2913
|
pi.sendMessage({
|
|
@@ -2786,15 +2916,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2786
2916
|
display: true,
|
|
2787
2917
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
2788
2918
|
} catch {
|
|
2789
|
-
|
|
2919
|
+
reportUndeliveredFollowUp("failed");
|
|
2790
2920
|
}
|
|
2791
|
-
});
|
|
2921
|
+
}).finally(clearLiveWidget);
|
|
2792
2922
|
backgroundBatches.add(backgroundBatch);
|
|
2793
2923
|
void backgroundBatch.finally(() => backgroundBatches.delete(backgroundBatch));
|
|
2794
2924
|
return {
|
|
2795
2925
|
content: [{
|
|
2796
2926
|
type: "text",
|
|
2797
|
-
text: boundedText(`${isResume ? "Resumed" : "Started"} child threads: ${threadList}. They continue in the background; use list, inspect, wait, steer, interrupt, collect, resume, or close
|
|
2927
|
+
text: boundedText(`${executionNote ? `${executionNote}\n\n` : ""}${isResume ? "Resumed" : "Started"} child threads: ${threadList}. They continue in the background. Live progress appears above the editor while they run; use list, inspect, wait, steer, interrupt, collect, resume, or close for current details.`, limits.toolOutputBytes, "\n\n[Spawn output truncated.]"),
|
|
2798
2928
|
}],
|
|
2799
2929
|
details: queuedDetails,
|
|
2800
2930
|
usage: queuedDetails.aggregateUsage,
|
|
@@ -2824,7 +2954,8 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2824
2954
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${spawnRequest.input.tasks.length} · ${schedule}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2825
2955
|
}
|
|
2826
2956
|
if (spawnRequest.kind === "spawn-chain") return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `chain ${spawnRequest.input.chain.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2827
|
-
|
|
2957
|
+
const agentName = typeof spawnRequest.input.agent === "string" ? spawnRequest.input.agent : spawnRequest.input.agent.name;
|
|
2958
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", agentName)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2828
2959
|
},
|
|
2829
2960
|
|
|
2830
2961
|
renderResult(result, { expanded }, theme) {
|
|
@@ -2838,32 +2969,32 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2838
2969
|
threads: details.results.map(threadBoardRecord),
|
|
2839
2970
|
selectedThreadId: details.selectedThreadId,
|
|
2840
2971
|
});
|
|
2841
|
-
if (!expanded) {
|
|
2842
|
-
const lines = [
|
|
2843
|
-
theme.fg("toolTitle", theme.bold(`Active (${board.active.length})`)),
|
|
2844
|
-
...board.active.map((task) => `${theme.fg("accent", "✻")} ${theme.fg("toolTitle", theme.bold(task.displayName ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1} · ${task.state.label} · ${task.usage.text}`)}`),
|
|
2845
|
-
theme.fg("toolTitle", theme.bold(`Done (${board.done.length})`)),
|
|
2846
|
-
...board.done.map((task) => `${theme.fg(task.state.status === "complete" ? "success" : "warning", `${task.state.label}`)} ${theme.fg("toolTitle", theme.bold(task.displayName ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1} · ${task.usage.text}`)}`),
|
|
2847
|
-
];
|
|
2972
|
+
if (!expanded) {
|
|
2973
|
+
const lines = [
|
|
2974
|
+
theme.fg("toolTitle", theme.bold(`Active (${board.active.length})`)),
|
|
2975
|
+
...board.active.map((task) => `${theme.fg("accent", "✻")} ${theme.fg("toolTitle", theme.bold(task.displayName ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1} · ${task.state.label} · ${task.usage.text}`)}`),
|
|
2976
|
+
theme.fg("toolTitle", theme.bold(`Done (${board.done.length})`)),
|
|
2977
|
+
...board.done.map((task) => `${theme.fg(task.state.status === "complete" ? "success" : "warning", `${task.state.label}`)} ${theme.fg("toolTitle", theme.bold(task.displayName ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1} · ${task.usage.text}`)}`),
|
|
2978
|
+
];
|
|
2848
2979
|
if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
|
|
2849
2980
|
lines.push(theme.fg("dim", `Total · ${formatUsage(details.aggregateUsage)} · Ctrl+O to expand`));
|
|
2850
2981
|
return new Text(lines.join("\n"), 0, 0);
|
|
2851
2982
|
}
|
|
2852
2983
|
|
|
2853
|
-
const container = new Container();
|
|
2854
|
-
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`Subagents · ${details.mode}`)), 0, 0));
|
|
2855
|
-
container.addChild(new Text(theme.fg("dim", `Active ${board.active.length} · Done ${board.done.length} · Controls: Inspect · Steer · Interrupt · Wait · Collect · Resume · Close`), 0, 0));
|
|
2984
|
+
const container = new Container();
|
|
2985
|
+
container.addChild(new Text(theme.fg("toolTitle", theme.bold(`Subagents · ${details.mode}`)), 0, 0));
|
|
2986
|
+
container.addChild(new Text(theme.fg("dim", `Active ${board.active.length} · Done ${board.done.length} · Controls: Inspect · Steer · Interrupt · Wait · Collect · Resume · Close`), 0, 0));
|
|
2856
2987
|
if (details.executionNote) container.addChild(new Text(theme.fg("dim", details.executionNote), 0, 0));
|
|
2857
2988
|
if (board.selected) {
|
|
2858
2989
|
const inspection = formatThreadInspection(threadBoardRecord(details.results.find((task) => task.id === board.selected!.id)!));
|
|
2859
2990
|
container.addChild(new Spacer(1));
|
|
2860
|
-
container.addChild(new Text(theme.fg("accent", `Inspect ${inspection.displayName ?? inspection.agent} · ${inspection.state.label} · ${inspection.id} · ${inspection.usage.text}`), 0, 0));
|
|
2991
|
+
container.addChild(new Text(theme.fg("accent", `Inspect ${inspection.displayName ?? inspection.agent} · ${inspection.state.label} · ${inspection.id} · ${inspection.usage.text}`), 0, 0));
|
|
2861
2992
|
for (const entry of inspection.trace.entries) container.addChild(new Text(`${theme.fg("muted", "→ ")}${theme.fg("toolOutput", entry)}`, 0, 0));
|
|
2862
2993
|
}
|
|
2863
2994
|
for (const task of details.results) {
|
|
2864
2995
|
container.addChild(new Spacer(1));
|
|
2865
2996
|
const status = theme.fg(statusColor(task.status), `${statusIcon(task.status)} ${task.status}`);
|
|
2866
|
-
container.addChild(new Text(`${status} ${theme.fg("accent", task.name ?? task.agent)}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt} · ${task.agentSource}`)}`, 0, 0));
|
|
2997
|
+
container.addChild(new Text(`${status} ${theme.fg("accent", task.name ?? task.agent)}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt} · ${task.agentSource}`)}`, 0, 0));
|
|
2867
2998
|
container.addChild(new Text(theme.fg("dim", `${task.model ?? "no model"} · ${task.thinking ?? "off"} · ${task.tools.join(", ")} · ${formatUsage(task.usage)} · ${task.durationMs}ms`), 0, 0));
|
|
2868
2999
|
container.addChild(new Text(theme.fg("muted", `Task: ${task.task}`), 0, 0));
|
|
2869
3000
|
for (const entry of task.trace) container.addChild(new Text(`${theme.fg("muted", "→ ")}${theme.fg("toolOutput", entry)}`, 0, 0));
|
|
@@ -2877,18 +3008,18 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2877
3008
|
container.addChild(new Text(theme.fg("dim", `Total · ${formatUsage(details.aggregateUsage)}`), 0, 0));
|
|
2878
3009
|
return container;
|
|
2879
3010
|
},
|
|
2880
|
-
};
|
|
2881
|
-
pi.registerTool(toolDefinition);
|
|
2882
|
-
return {
|
|
2883
|
-
async execute(controlRequest: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult> {
|
|
2884
|
-
const result = await toolDefinition.execute("subagent-control", controlRequest, undefined, undefined, ctx);
|
|
2885
|
-
const first = result.content[0];
|
|
2886
|
-
const details = result.details as SubagentDetails;
|
|
2887
|
-
return {
|
|
2888
|
-
text: first?.type === "text" ? first.text : "",
|
|
2889
|
-
details,
|
|
2890
|
-
usage: (result.usage as SubagentUsage | undefined) ?? details.aggregateUsage,
|
|
2891
|
-
};
|
|
2892
|
-
},
|
|
2893
|
-
};
|
|
2894
|
-
}
|
|
3011
|
+
};
|
|
3012
|
+
pi.registerTool(toolDefinition);
|
|
3013
|
+
return {
|
|
3014
|
+
async execute(controlRequest: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult> {
|
|
3015
|
+
const result = await toolDefinition.execute("subagent-control", controlRequest, undefined, undefined, ctx);
|
|
3016
|
+
const first = result.content[0];
|
|
3017
|
+
const details = result.details as SubagentDetails;
|
|
3018
|
+
return {
|
|
3019
|
+
text: first?.type === "text" ? first.text : "",
|
|
3020
|
+
details,
|
|
3021
|
+
usage: (result.usage as SubagentUsage | undefined) ?? details.aggregateUsage,
|
|
3022
|
+
};
|
|
3023
|
+
},
|
|
3024
|
+
};
|
|
3025
|
+
}
|