killeros 1.5.6 → 1.5.8
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 +26 -0
- package/README.md +16 -27
- package/killeros/goals.ts +9 -8
- package/killeros/question.ts +19 -4
- package/killeros/subagent-process.ts +20 -13
- package/killeros/subagent-ui.ts +7 -5
- package/killeros/subagents.ts +541 -309
- package/package.json +2 -3
- package/agents/debugger.md +0 -50
- package/agents/documenter.md +0 -49
- package/agents/planner.md +0 -53
- package/agents/reviewer.md +0 -58
- package/agents/scout.md +0 -56
- package/agents/security.md +0 -54
- package/agents/tester.md +0 -50
- package/agents/worker.md +0 -54
package/killeros/subagents.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { closeSync, openSync, readSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import { getSupportedThinkingLevels, StringEnum, type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { getSupportedThinkingLevels, StringEnum, type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
8
7
|
import {
|
|
9
8
|
CONFIG_DIR_NAME,
|
|
10
9
|
getAgentDir,
|
|
@@ -29,9 +28,11 @@ export const SUBAGENT_LIMITS = {
|
|
|
29
28
|
threadRetentionRecords: 64,
|
|
30
29
|
threadRetentionBytes: 128 * 1024 * 1024,
|
|
31
30
|
roleFileBytes: 64 * 1024,
|
|
32
|
-
taskCharacters: 20_000,
|
|
33
|
-
killGraceMs: 5_000,
|
|
34
|
-
|
|
31
|
+
taskCharacters: 20_000,
|
|
32
|
+
killGraceMs: 5_000,
|
|
33
|
+
defaultMaxTurns: 64,
|
|
34
|
+
defaultQuotaTokens: 2_000_000,
|
|
35
|
+
defaultWallTimeMs: 1_800_000,
|
|
35
36
|
processExitWaitMs: 10_000,
|
|
36
37
|
} as const;
|
|
37
38
|
|
|
@@ -46,7 +47,7 @@ const ROLE_FIELDS = new Set(["name", "description", "access", "tools", "model",
|
|
|
46
47
|
|
|
47
48
|
type ThinkingLevel = ModelThinkingLevel;
|
|
48
49
|
export type AgentAccess = "read" | "write";
|
|
49
|
-
export type AgentSource = "bundled" | "personal" | "project" | "inline";
|
|
50
|
+
export type AgentSource = "bundled" | "personal" | "project" | "inline" | "legacy";
|
|
50
51
|
export type AgentScope = "user" | "project" | "both";
|
|
51
52
|
export type SubagentStatus = "queued" | "running" | "complete" | "failed" | "cancelled" | "limited" | "orphaned";
|
|
52
53
|
|
|
@@ -136,8 +137,9 @@ export interface SubagentDetails {
|
|
|
136
137
|
threads?: SubagentThread[];
|
|
137
138
|
activeThreads?: SubagentThread[];
|
|
138
139
|
doneThreads?: SubagentThread[];
|
|
139
|
-
selectedThreadId?: string;
|
|
140
|
-
wait?: SubagentWaitSummary;
|
|
140
|
+
selectedThreadId?: string;
|
|
141
|
+
wait?: SubagentWaitSummary;
|
|
142
|
+
backgroundStarted?: boolean;
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
export interface SubagentWaitSummary {
|
|
@@ -192,9 +194,10 @@ interface SpawnedProcess {
|
|
|
192
194
|
once(event: "close", listener: (code: number | null) => void): this;
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
196
|
-
wallTimeMs?: number;
|
|
197
|
-
|
|
197
|
+
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
198
|
+
wallTimeMs?: number;
|
|
199
|
+
maxTurns?: number;
|
|
200
|
+
jsonlLineBytes?: number;
|
|
198
201
|
traceBytes?: number;
|
|
199
202
|
stderrBytes?: number;
|
|
200
203
|
taskOutputBytes?: number;
|
|
@@ -202,9 +205,10 @@ type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
|
202
205
|
quotaUsd?: number;
|
|
203
206
|
};
|
|
204
207
|
|
|
205
|
-
export interface SubagentRuntimeOptions {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
export interface SubagentRuntimeOptions {
|
|
209
|
+
/** Explicit test or embedding role directory; KillerOS ships no bundled roles. */
|
|
210
|
+
bundledAgentsDir?: string;
|
|
211
|
+
userAgentsDir?: string;
|
|
208
212
|
webExtension?: string;
|
|
209
213
|
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
210
214
|
limits?: Partial<SubagentLimits>;
|
|
@@ -396,20 +400,21 @@ function findProjectAgentsDir(cwd: string): string | null {
|
|
|
396
400
|
}
|
|
397
401
|
}
|
|
398
402
|
|
|
399
|
-
export function discoverAgentRoles(
|
|
400
|
-
cwd: string,
|
|
401
|
-
scope: AgentScope,
|
|
402
|
-
projectTrusted: boolean,
|
|
403
|
-
options: Pick<SubagentRuntimeOptions, "bundledAgentsDir" | "userAgentsDir" | "limits"> = {},
|
|
404
|
-
): AgentDiscoveryResult {
|
|
405
|
-
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
406
|
-
const bundledDir = options.bundledAgentsDir
|
|
407
|
-
const userDir = options.userAgentsDir ?? path.join(getAgentDir(), "agents");
|
|
403
|
+
export function discoverAgentRoles(
|
|
404
|
+
cwd: string,
|
|
405
|
+
scope: AgentScope,
|
|
406
|
+
projectTrusted: boolean,
|
|
407
|
+
options: Pick<SubagentRuntimeOptions, "bundledAgentsDir" | "userAgentsDir" | "limits"> = {},
|
|
408
|
+
): AgentDiscoveryResult {
|
|
409
|
+
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
410
|
+
const bundledDir = options.bundledAgentsDir;
|
|
411
|
+
const userDir = options.userAgentsDir ?? path.join(getAgentDir(), "agents");
|
|
408
412
|
const wantsProject = scope === "project" || scope === "both";
|
|
409
413
|
if (wantsProject && !projectTrusted) throw new Error("Project agents require a trusted project");
|
|
410
414
|
const projectAgentsDir = wantsProject ? findProjectAgentsDir(cwd) : null;
|
|
411
415
|
|
|
412
|
-
const layers: Array<{ dir: string; source: AgentSource }> = [
|
|
416
|
+
const layers: Array<{ dir: string; source: AgentSource }> = [];
|
|
417
|
+
if (bundledDir) layers.push({ dir: bundledDir, source: "bundled" });
|
|
413
418
|
if (scope === "user" || scope === "both") layers.push({ dir: userDir, source: "personal" });
|
|
414
419
|
if (wantsProject && projectAgentsDir) layers.push({ dir: projectAgentsDir, source: "project" });
|
|
415
420
|
|
|
@@ -723,9 +728,10 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
723
728
|
cwd: options.cwd,
|
|
724
729
|
signal: options.signal,
|
|
725
730
|
spawnProcess: options.spawnProcess,
|
|
726
|
-
limits: {
|
|
727
|
-
...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
|
|
728
|
-
...(limits.
|
|
731
|
+
limits: {
|
|
732
|
+
...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
|
|
733
|
+
...(limits.maxTurns === undefined ? {} : { maxTurns: limits.maxTurns }),
|
|
734
|
+
...(limits.jsonlLineBytes === undefined ? {} : { jsonlLineBytes: limits.jsonlLineBytes }),
|
|
729
735
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes }),
|
|
730
736
|
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes }),
|
|
731
737
|
...(limits.taskOutputBytes === undefined ? {} : { outputBytes: limits.taskOutputBytes }),
|
|
@@ -811,11 +817,11 @@ type SpawnOptions = {
|
|
|
811
817
|
agentScope?: AgentScope;
|
|
812
818
|
};
|
|
813
819
|
|
|
814
|
-
type SpawnSingleRequest = SpawnOptions & {
|
|
815
|
-
action?: "spawn";
|
|
816
|
-
agent
|
|
817
|
-
task: string;
|
|
818
|
-
name?: string;
|
|
820
|
+
type SpawnSingleRequest = SpawnOptions & {
|
|
821
|
+
action?: "spawn";
|
|
822
|
+
agent?: AgentSpec;
|
|
823
|
+
task: string;
|
|
824
|
+
name?: string;
|
|
819
825
|
};
|
|
820
826
|
|
|
821
827
|
type SpawnParallelRequest = SpawnOptions & {
|
|
@@ -900,14 +906,15 @@ function requireOnlyFields(record: Record<string, unknown>, allowed: readonly st
|
|
|
900
906
|
if (invalid) throw new Error(`Invalid subagent request: field ${JSON.stringify(invalid)} is not valid with action ${JSON.stringify(action)}.`);
|
|
901
907
|
}
|
|
902
908
|
|
|
903
|
-
function parseAgentSpec(value: unknown): AgentSpec {
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
if (
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
909
|
+
function parseAgentSpec(value: unknown): AgentSpec | undefined {
|
|
910
|
+
if (value === undefined) return undefined;
|
|
911
|
+
if (typeof value === "string") {
|
|
912
|
+
if (!value.length) throw new Error("Invalid subagent request: agent must be a non-empty role name or inline role.");
|
|
913
|
+
if ([...value].length > 64) throw new Error("Invalid subagent request: agent must be no longer than 64 characters.");
|
|
914
|
+
return value;
|
|
915
|
+
}
|
|
916
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
917
|
+
throw new Error("Invalid subagent request: agent must be a role name, inline role, or omitted.");
|
|
911
918
|
}
|
|
912
919
|
const role = requireRecord(value, "inline role");
|
|
913
920
|
for (const field of Object.keys(role)) {
|
|
@@ -943,28 +950,119 @@ function parseAgentSpec(value: unknown): AgentSpec {
|
|
|
943
950
|
return { name, description, access, tools };
|
|
944
951
|
}
|
|
945
952
|
|
|
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
|
-
|
|
955
|
-
function
|
|
953
|
+
function inlineAgentRole(role: InlineAgentRole): AgentRole {
|
|
954
|
+
return {
|
|
955
|
+
...role,
|
|
956
|
+
prompt: role.description,
|
|
957
|
+
source: "inline",
|
|
958
|
+
filePath: `inline:${role.name}`,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function activeParentTools(pi: ExtensionAPI): Set<string> | undefined {
|
|
963
|
+
const getActiveTools = (pi as unknown as { getActiveTools?: () => string[] }).getActiveTools;
|
|
964
|
+
return typeof getActiveTools === "function" ? new Set(getActiveTools.call(pi)) : undefined;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function validateAgentTools(agent: AgentRole, parentTools: ReadonlySet<string> | undefined): void {
|
|
968
|
+
if (!parentTools) return;
|
|
969
|
+
const unavailable = agent.tools.find((tool) => !parentTools.has(tool));
|
|
970
|
+
if (unavailable) throw new Error(`Role ${JSON.stringify(agent.name)} tool ${JSON.stringify(unavailable)} is not active for the parent`);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function genericAgentRole(parentTools: ReadonlySet<string> | undefined): AgentRole {
|
|
974
|
+
const tools = [...READ_TOOLS].filter((tool) => !parentTools || parentTools.has(tool));
|
|
975
|
+
if (tools.length === 0) throw new Error("No read-only child tools are active for the parent");
|
|
976
|
+
return {
|
|
977
|
+
name: "generic",
|
|
978
|
+
description: "Complete the assigned task and return the result to the parent.",
|
|
979
|
+
access: "read",
|
|
980
|
+
tools,
|
|
981
|
+
prompt: "Complete the assigned task and return the result to the parent.",
|
|
982
|
+
source: "inline",
|
|
983
|
+
filePath: "inline:generic",
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function cloneAgentRole(agent: AgentRole): AgentRole {
|
|
988
|
+
return { ...agent, tools: [...agent.tools] };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function restorePersistedAgent(value: unknown): AgentRole | undefined {
|
|
992
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
993
|
+
const role = value as Record<string, unknown>;
|
|
994
|
+
const name = role.name;
|
|
995
|
+
const description = role.description;
|
|
996
|
+
const access = role.access;
|
|
997
|
+
const tools = role.tools;
|
|
998
|
+
const prompt = role.prompt;
|
|
999
|
+
const source = role.source;
|
|
1000
|
+
const filePath = role.filePath;
|
|
1001
|
+
if (typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u.test(name)) return undefined;
|
|
1002
|
+
if (typeof description !== "string" || !description.trim() || description.length > 500) return undefined;
|
|
1003
|
+
if (access !== "read" && access !== "write") return undefined;
|
|
1004
|
+
if (!Array.isArray(tools) || tools.length === 0 || tools.some((tool) => typeof tool !== "string" || !KNOWN_TOOLS.has(tool))) return undefined;
|
|
1005
|
+
const uniqueTools = [...new Set(tools as string[])];
|
|
1006
|
+
if (uniqueTools.length !== tools.length || access === "read" && uniqueTools.some((tool) => WRITE_TOOLS.has(tool))) return undefined;
|
|
1007
|
+
if (typeof prompt !== "string" || !prompt.trim() || prompt.length > SUBAGENT_LIMITS.roleFileBytes) return undefined;
|
|
1008
|
+
if (source !== "bundled" && source !== "personal" && source !== "project" && source !== "inline" && source !== "legacy") return undefined;
|
|
1009
|
+
if (typeof filePath !== "string" || !filePath) return undefined;
|
|
1010
|
+
const optionalString = (field: string): string | undefined => {
|
|
1011
|
+
const item = role[field];
|
|
1012
|
+
return item === undefined ? undefined : typeof item === "string" && item.trim() ? item : undefined;
|
|
1013
|
+
};
|
|
1014
|
+
const timeoutMs = role.timeoutMs;
|
|
1015
|
+
if (timeoutMs !== undefined && (typeof timeoutMs !== "number" || !Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_NODE_TIMER_MS)) return undefined;
|
|
1016
|
+
return {
|
|
1017
|
+
name,
|
|
1018
|
+
description,
|
|
1019
|
+
access,
|
|
1020
|
+
tools: uniqueTools,
|
|
1021
|
+
model: optionalString("model"),
|
|
1022
|
+
thinking: optionalString("thinking"),
|
|
1023
|
+
timeoutMs: typeof timeoutMs === "number" ? timeoutMs : undefined,
|
|
1024
|
+
prompt,
|
|
1025
|
+
source,
|
|
1026
|
+
filePath,
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function restoreLegacyAgent(thread: ThreadSnapshot, sourceHint?: SubagentTaskResult["agentSource"]): AgentRole | undefined {
|
|
1031
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u.test(thread.role)) return undefined;
|
|
1032
|
+
if (!Array.isArray(thread.tools) || thread.tools.length === 0) return undefined;
|
|
1033
|
+
const tools = [...new Set(thread.tools)];
|
|
1034
|
+
if (tools.length !== thread.tools.length || tools.some((tool) => !KNOWN_TOOLS.has(tool))) return undefined;
|
|
1035
|
+
const access = thread.capabilityBoundary?.filesystem;
|
|
1036
|
+
if (access !== "read" && access !== "write") return undefined;
|
|
1037
|
+
if (access === "read" && tools.some((tool) => WRITE_TOOLS.has(tool))) return undefined;
|
|
1038
|
+
const source = sourceHint === "bundled" || sourceHint === "personal" || sourceHint === "project"
|
|
1039
|
+
? sourceHint
|
|
1040
|
+
: "legacy";
|
|
1041
|
+
return {
|
|
1042
|
+
name: thread.role,
|
|
1043
|
+
description: "Persisted role contract from an earlier KillerOS release.",
|
|
1044
|
+
access,
|
|
1045
|
+
tools,
|
|
1046
|
+
prompt: "Continue the saved child task. Use only the retained role tools and return a handoff to the parent.",
|
|
1047
|
+
source,
|
|
1048
|
+
filePath: `legacy:${source}:${thread.role}`,
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function parseTaskInputs(value: unknown, field: "tasks" | "chain", limits: Pick<SubagentLimits, "maxTasks" | "taskCharacters">): TaskInput[] {
|
|
956
1053
|
if (!Array.isArray(value) || value.length === 0) {
|
|
957
1054
|
throw new Error(`Invalid subagent request: ${field} must be a non-empty array.`);
|
|
958
1055
|
}
|
|
959
1056
|
if (value.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
960
1057
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1058
|
+
const task = requireRecord(entry, `${field}[${index}]`);
|
|
1059
|
+
requireOnlyFields(task, ["agent", "task", "name"], `spawn ${field}`);
|
|
1060
|
+
const name = optionalThreadName(task);
|
|
1061
|
+
const agent = parseAgentSpec(task.agent);
|
|
1062
|
+
return {
|
|
1063
|
+
...(agent === undefined ? {} : { agent }),
|
|
1064
|
+
task: requireTextField(task, "task", limits.taskCharacters),
|
|
1065
|
+
...(name === undefined ? {} : { name }),
|
|
968
1066
|
};
|
|
969
1067
|
});
|
|
970
1068
|
}
|
|
@@ -1141,25 +1239,25 @@ function prepareSubagentRequest(
|
|
|
1141
1239
|
}
|
|
1142
1240
|
|
|
1143
1241
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
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: "
|
|
1150
|
-
const agentSchema = Type.Union([
|
|
1151
|
-
Type.String({ minLength: 1, maxLength: 64, description: "
|
|
1152
|
-
inlineAgentSchema,
|
|
1153
|
-
]);
|
|
1154
|
-
const taskSchema = Type.Object({
|
|
1155
|
-
agent: agentSchema,
|
|
1156
|
-
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task
|
|
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}
|
|
1162
|
-
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1242
|
+
const inlineAgentSchema = Type.Object({
|
|
1243
|
+
name: Type.String({ minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$" }),
|
|
1244
|
+
description: Type.String({ minLength: 1, maxLength: 500 }),
|
|
1245
|
+
access: StringEnum(["read", "write"] as const),
|
|
1246
|
+
tools: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, uniqueItems: true }),
|
|
1247
|
+
}, { additionalProperties: false, description: "Optional role definition for this spawn; tools must be active for the parent" });
|
|
1248
|
+
const agentSchema = Type.Union([
|
|
1249
|
+
Type.String({ minLength: 1, maxLength: 64, description: "Optional custom role name from an approved agents folder" }),
|
|
1250
|
+
inlineAgentSchema,
|
|
1251
|
+
]);
|
|
1252
|
+
const taskSchema = Type.Object({
|
|
1253
|
+
agent: Type.Optional(agentSchema),
|
|
1254
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task; omitted agent uses the generic read-only child" }),
|
|
1255
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1256
|
+
}, { additionalProperties: false });
|
|
1257
|
+
const chainTaskSchema = Type.Object({
|
|
1258
|
+
agent: Type.Optional(agentSchema),
|
|
1259
|
+
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous}; omitted agent uses the generic read-only child" }),
|
|
1260
|
+
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1163
1261
|
}, { additionalProperties: false });
|
|
1164
1262
|
const threadId = Type.String({ minLength: 1, maxLength: 128, description: "Existing child thread ID; omit when spawning because KillerOS creates it" });
|
|
1165
1263
|
return Type.Object({
|
|
@@ -1174,12 +1272,12 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
1174
1272
|
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` })),
|
|
1175
1273
|
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` })),
|
|
1176
1274
|
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
1177
|
-
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "
|
|
1178
|
-
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "
|
|
1179
|
-
agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const, {
|
|
1180
|
-
default: "user",
|
|
1181
|
-
description: "
|
|
1182
|
-
})),
|
|
1275
|
+
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Optional model for every task as provider/model; omitted or inherit uses the selected role default, then the active parent. Set this when the user requires one model for the batch" })),
|
|
1276
|
+
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Optional thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit. Omitted or inherit uses the selected role default, then the active parent" })),
|
|
1277
|
+
agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const, {
|
|
1278
|
+
default: "user",
|
|
1279
|
+
description: "Custom role sources: user includes personal; project includes trusted project; both includes both",
|
|
1280
|
+
})),
|
|
1183
1281
|
}, { additionalProperties: false });
|
|
1184
1282
|
}
|
|
1185
1283
|
|
|
@@ -1234,7 +1332,7 @@ function buildSteeredTask(task: string, steering: readonly string[], maxCharacte
|
|
|
1234
1332
|
return `${taskText}${steeringLabel}${steeringText}`;
|
|
1235
1333
|
}
|
|
1236
1334
|
|
|
1237
|
-
export type TaskInput = { agent
|
|
1335
|
+
export type TaskInput = { agent?: AgentSpec; task: string; name?: string };
|
|
1238
1336
|
|
|
1239
1337
|
function steeredTaskWouldExceedLimit(task: string, steering: readonly string[], maxCharacters: number): boolean {
|
|
1240
1338
|
if (!steering.length) return false;
|
|
@@ -1298,12 +1396,14 @@ interface ChildSession {
|
|
|
1298
1396
|
directory: string;
|
|
1299
1397
|
}
|
|
1300
1398
|
|
|
1301
|
-
interface ThreadMetadata {
|
|
1302
|
-
displayName: string;
|
|
1303
|
-
attempt: number;
|
|
1304
|
-
session: ChildSession;
|
|
1305
|
-
persistentSession: boolean;
|
|
1306
|
-
|
|
1399
|
+
interface ThreadMetadata {
|
|
1400
|
+
displayName: string;
|
|
1401
|
+
attempt: number;
|
|
1402
|
+
session: ChildSession;
|
|
1403
|
+
persistentSession: boolean;
|
|
1404
|
+
agent?: AgentRole;
|
|
1405
|
+
thinking?: ThinkingLevel;
|
|
1406
|
+
}
|
|
1307
1407
|
|
|
1308
1408
|
type ThreadSnapshot = SubagentThread & {
|
|
1309
1409
|
displayName?: string;
|
|
@@ -1445,7 +1545,7 @@ function restorePersistedResult(value: unknown, thread: ThreadSnapshot): Subagen
|
|
|
1445
1545
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1446
1546
|
const source = value as Record<string, any>;
|
|
1447
1547
|
const statuses = new Set<SubagentStatus>(["queued", "running", "complete", "failed", "cancelled", "limited"]);
|
|
1448
|
-
const agentSources = new Set<SubagentTaskResult["agentSource"]>(["bundled", "personal", "project", "inline", "unknown"]);
|
|
1548
|
+
const agentSources = new Set<SubagentTaskResult["agentSource"]>(["bundled", "personal", "project", "inline", "legacy", "unknown"]);
|
|
1449
1549
|
if (typeof source.id !== "string" || source.id !== thread.id) return undefined;
|
|
1450
1550
|
const status = source.status ?? (thread.state === "done" ? "complete" : thread.state === "failed" ? "failed" : "cancelled");
|
|
1451
1551
|
if (typeof status !== "string" || !statuses.has(status as SubagentStatus)) return undefined;
|
|
@@ -1533,23 +1633,26 @@ function threadBoardRecord(result: SubagentTaskResult): ThreadBoardRecord {
|
|
|
1533
1633
|
}
|
|
1534
1634
|
|
|
1535
1635
|
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
|
-
|
|
1542
|
-
const
|
|
1543
|
-
const
|
|
1544
|
-
const
|
|
1545
|
-
|
|
1636
|
+
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
1637
|
+
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1638
|
+
let threads = new SubagentThreadRegistry();
|
|
1639
|
+
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
1640
|
+
const threadMetadata = new Map<string, ThreadMetadata>();
|
|
1641
|
+
type ThreadResource = { directory: string; persistent: boolean; handles: Set<SubagentProcessHandle> };
|
|
1642
|
+
const threadResources = new Map<string, ThreadResource>();
|
|
1643
|
+
const backgroundBatches = new Set<Promise<unknown>>();
|
|
1644
|
+
const savedResults = new Map<string, SubagentTaskResult>();
|
|
1645
|
+
const evictedThreadParents = new Map<string, string | undefined>();
|
|
1646
|
+
let terminalCleanupQueue: Promise<void> = Promise.resolve();
|
|
1647
|
+
let sessionGeneration = 0;
|
|
1546
1648
|
let persistenceWarning: string | undefined;
|
|
1547
1649
|
|
|
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
|
|
1650
|
+
const persistText = (value: string | undefined, maxBytes: number): string | undefined => {
|
|
1651
|
+
if (value === undefined) return undefined;
|
|
1652
|
+
return truncateUtf8(value, maxBytes).text;
|
|
1653
|
+
};
|
|
1654
|
+
const lifecycleOutput = (value: string | undefined): string | undefined => value?.trim() ? value : undefined;
|
|
1655
|
+
const persistenceAppend = (record: Record<string, unknown>): void => {
|
|
1553
1656
|
const appendEntry = (pi as unknown as { appendEntry?: (type: string, data: unknown) => void }).appendEntry;
|
|
1554
1657
|
if (!appendEntry) return;
|
|
1555
1658
|
try {
|
|
@@ -1558,9 +1661,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1558
1661
|
persistenceWarning ??= `Subagent persistence is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1559
1662
|
}
|
|
1560
1663
|
};
|
|
1561
|
-
const persistedThread = (thread: ThreadSnapshot): Record<string, unknown> => {
|
|
1562
|
-
const session = threadSession(thread, threadMetadata) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
1563
|
-
|
|
1664
|
+
const persistedThread = (thread: ThreadSnapshot): Record<string, unknown> => {
|
|
1665
|
+
const session = threadSession(thread, threadMetadata) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
1666
|
+
const metadata = threadMetadata.get(thread.id);
|
|
1667
|
+
const agent = metadata?.agent;
|
|
1668
|
+
return {
|
|
1564
1669
|
id: thread.id,
|
|
1565
1670
|
parentId: thread.parentId,
|
|
1566
1671
|
displayName: threadDisplayName(thread, threadMetadata),
|
|
@@ -1568,8 +1673,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1568
1673
|
role: thread.role,
|
|
1569
1674
|
prompt: clipCharacters(thread.prompt, limits.taskCharacters),
|
|
1570
1675
|
model: thread.model,
|
|
1571
|
-
tools: thread.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1572
|
-
|
|
1676
|
+
tools: thread.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1677
|
+
...(agent ? {
|
|
1678
|
+
roleDefinition: {
|
|
1679
|
+
name: agent.name,
|
|
1680
|
+
description: persistText(agent.description, 500),
|
|
1681
|
+
access: agent.access,
|
|
1682
|
+
tools: agent.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1683
|
+
model: agent.model,
|
|
1684
|
+
thinking: agent.thinking,
|
|
1685
|
+
timeoutMs: agent.timeoutMs,
|
|
1686
|
+
prompt: persistText(agent.prompt, limits.roleFileBytes),
|
|
1687
|
+
source: agent.source,
|
|
1688
|
+
filePath: persistText(agent.filePath, 4_000),
|
|
1689
|
+
},
|
|
1690
|
+
thinking: metadata?.thinking,
|
|
1691
|
+
} : {}),
|
|
1692
|
+
capabilityBoundary: { ...thread.capabilityBoundary },
|
|
1573
1693
|
session: { ...session },
|
|
1574
1694
|
state: thread.state,
|
|
1575
1695
|
usage: { ...thread.usage },
|
|
@@ -1638,12 +1758,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1638
1758
|
});
|
|
1639
1759
|
};
|
|
1640
1760
|
|
|
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 }>();
|
|
1761
|
+
const restoreRecords = (
|
|
1762
|
+
entries: readonly unknown[],
|
|
1763
|
+
parentId: string,
|
|
1764
|
+
expectedSession?: (threadId: string) => ChildSession | undefined,
|
|
1765
|
+
): Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult; agent?: AgentRole; thinking?: ThinkingLevel }> => {
|
|
1766
|
+
const records = new Map<string, { thread: ThreadSnapshot; result?: SubagentTaskResult; agent?: AgentRole; thinking?: ThinkingLevel }>();
|
|
1647
1767
|
for (const entry of entries) {
|
|
1648
1768
|
try {
|
|
1649
1769
|
if (!entry || typeof entry !== "object") continue;
|
|
@@ -1654,10 +1774,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1654
1774
|
const record = data as Record<string, any>;
|
|
1655
1775
|
if (record.version !== 1 || typeof record.parentId !== "string" || record.parentId !== parentId) continue;
|
|
1656
1776
|
if (record.event === "spawn" || record.event === "snapshot") {
|
|
1657
|
-
const rawThread = record.thread;
|
|
1777
|
+
const rawThread = record.thread;
|
|
1658
1778
|
if (!rawThread || typeof rawThread !== "object" || typeof rawThread.id !== "string" || rawThread.parentId !== parentId) continue;
|
|
1659
1779
|
if (typeof rawThread.role !== "string" || typeof rawThread.prompt !== "string" || typeof rawThread.model !== "string") continue;
|
|
1660
|
-
const thread = { ...rawThread } as ThreadSnapshot;
|
|
1780
|
+
const thread = { ...rawThread } as ThreadSnapshot;
|
|
1781
|
+
const thinking = rawThread.thinking === undefined
|
|
1782
|
+
? undefined
|
|
1783
|
+
: typeof rawThread.thinking === "string" && rawThread.thinking.trim() ? rawThread.thinking as ThinkingLevel : undefined;
|
|
1784
|
+
if (rawThread.thinking !== undefined && thinking === undefined) continue;
|
|
1661
1785
|
thread.prompt = clipCharacters(thread.prompt, limits.taskCharacters);
|
|
1662
1786
|
thread.result = typeof rawThread.result === "string" ? persistText(rawThread.result, 256 * 1024) : rawThread.result;
|
|
1663
1787
|
thread.tools = Array.isArray(rawThread.tools) ? rawThread.tools.slice(0, 32) : [];
|
|
@@ -1674,13 +1798,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1674
1798
|
id: `killeros-${safeSessionId(thread.id)}`,
|
|
1675
1799
|
directory: "",
|
|
1676
1800
|
};
|
|
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
|
-
|
|
1801
|
+
if (thread.state === "queued" || thread.state === "active") {
|
|
1802
|
+
thread.state = "orphaned" as SubagentThreadState;
|
|
1803
|
+
thread.stopReason = "parent_restarted";
|
|
1804
|
+
}
|
|
1805
|
+
const result = record.result === undefined ? undefined : restorePersistedResult(record.result, thread);
|
|
1806
|
+
if (record.result !== undefined && !result) continue;
|
|
1807
|
+
const agent = rawThread.roleDefinition === undefined
|
|
1808
|
+
? restoreLegacyAgent(thread, result?.agentSource)
|
|
1809
|
+
: restorePersistedAgent(rawThread.roleDefinition);
|
|
1810
|
+
if (rawThread.roleDefinition !== undefined && !agent) continue;
|
|
1811
|
+
records.set(thread.id, { thread, result, agent, thinking });
|
|
1684
1812
|
} else if (record.event === "close" && typeof record.id === "string") {
|
|
1685
1813
|
const previous = records.get(record.id);
|
|
1686
1814
|
if (!previous) continue;
|
|
@@ -1703,7 +1831,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1703
1831
|
return [...records.values()];
|
|
1704
1832
|
};
|
|
1705
1833
|
|
|
1706
|
-
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }>): void => {
|
|
1834
|
+
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult; agent?: AgentRole; thinking?: ThinkingLevel }>): void => {
|
|
1707
1835
|
if (!restored.length) return;
|
|
1708
1836
|
const ids = [...restored.map(({ thread }) => thread.id)];
|
|
1709
1837
|
let idIndex = 0;
|
|
@@ -1727,17 +1855,20 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1727
1855
|
attempt: thread.attempt,
|
|
1728
1856
|
session: thread.session,
|
|
1729
1857
|
} 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
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1858
|
+
threadMetadata.set(created.id, {
|
|
1859
|
+
displayName: thread.displayName ?? thread.role,
|
|
1860
|
+
attempt: thread.attempt ?? 1,
|
|
1861
|
+
session: thread.session ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" },
|
|
1862
|
+
persistentSession: Boolean(thread.session?.directory),
|
|
1863
|
+
...(entry.agent ? { agent: cloneAgentRole(entry.agent) } : {}),
|
|
1864
|
+
...(entry.thinking ? { thinking: entry.thinking } : {}),
|
|
1865
|
+
});
|
|
1866
|
+
if (entry.result) saveResult(created.id, entry.result);
|
|
1867
|
+
if (!(threads as any).hydrate) {
|
|
1868
|
+
if (thread.state === "done") {
|
|
1869
|
+
threads.begin(created.id);
|
|
1870
|
+
const output = lifecycleOutput(thread.result ?? entry.result?.output);
|
|
1871
|
+
threads.complete(created.id, { result: output });
|
|
1741
1872
|
} else if (thread.state === "failed") {
|
|
1742
1873
|
threads.begin(created.id);
|
|
1743
1874
|
threads.fail(created.id, { message: thread.failure?.message ?? entry.result?.errorMessage ?? "restored failure" });
|
|
@@ -1772,8 +1903,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1772
1903
|
}
|
|
1773
1904
|
};
|
|
1774
1905
|
|
|
1775
|
-
const rememberEvictedThreads = (threadsToRemember: readonly SubagentThread[]): void => {
|
|
1776
|
-
for (const thread of threadsToRemember)
|
|
1906
|
+
const rememberEvictedThreads = (threadsToRemember: readonly SubagentThread[]): void => {
|
|
1907
|
+
for (const thread of threadsToRemember) {
|
|
1908
|
+
evictedThreadParents.set(thread.id, thread.parentId);
|
|
1909
|
+
threadMetadata.delete(thread.id);
|
|
1910
|
+
}
|
|
1777
1911
|
while (evictedThreadParents.size > maxClosedThreads) {
|
|
1778
1912
|
const oldest = evictedThreadParents.keys().next().value;
|
|
1779
1913
|
if (oldest === undefined) break;
|
|
@@ -1785,23 +1919,62 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1785
1919
|
rememberEvictedThreads(threads.pruneClosed(maxClosedThreads));
|
|
1786
1920
|
};
|
|
1787
1921
|
|
|
1788
|
-
const resultBytes = (result: SubagentTaskResult): number => Buffer.byteLength([
|
|
1789
|
-
result.task,
|
|
1790
|
-
...result.trace,
|
|
1922
|
+
const resultBytes = (result: SubagentTaskResult): number => Buffer.byteLength([
|
|
1923
|
+
result.task,
|
|
1924
|
+
...result.trace,
|
|
1791
1925
|
result.stderr,
|
|
1792
1926
|
result.output,
|
|
1793
|
-
result.errorMessage ?? "",
|
|
1794
|
-
].join("\n"), "utf8");
|
|
1795
|
-
const
|
|
1927
|
+
result.errorMessage ?? "",
|
|
1928
|
+
].join("\n"), "utf8");
|
|
1929
|
+
const cleanupTerminalThread = async (
|
|
1930
|
+
threadId: string,
|
|
1931
|
+
resource: ThreadResource | undefined,
|
|
1932
|
+
metadata: ThreadMetadata | undefined,
|
|
1933
|
+
): Promise<boolean> => {
|
|
1934
|
+
const exits = resource
|
|
1935
|
+
? await Promise.all([...resource.handles].map((handle) => waitForConfirmedProcessExit(handle, limits.processExitWaitMs)))
|
|
1936
|
+
: [];
|
|
1937
|
+
if (!exits.every(Boolean)) {
|
|
1938
|
+
for (const handle of resource?.handles ?? []) {
|
|
1939
|
+
if (!handle.hasExited) {
|
|
1940
|
+
void handle.exited.then(() => queueTerminalCleanup(threadId)).catch(() => {});
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
return false;
|
|
1944
|
+
}
|
|
1945
|
+
const directory = resource?.directory ?? metadata?.session.directory;
|
|
1946
|
+
if ((resource?.persistent || metadata?.persistentSession) && directory) {
|
|
1947
|
+
await rm(directory, { recursive: true, force: true });
|
|
1948
|
+
}
|
|
1949
|
+
if (threadResources.get(threadId) === resource) threadResources.delete(threadId);
|
|
1950
|
+
return true;
|
|
1951
|
+
};
|
|
1952
|
+
const queueTerminalCleanup = (threadId: string): Promise<boolean> => {
|
|
1953
|
+
const resource = threadResources.get(threadId);
|
|
1954
|
+
const metadata = threadMetadata.get(threadId);
|
|
1955
|
+
const generation = sessionGeneration;
|
|
1956
|
+
const cleanup = terminalCleanupQueue.then(() => generation === sessionGeneration
|
|
1957
|
+
? cleanupTerminalThread(threadId, resource, metadata)
|
|
1958
|
+
: false);
|
|
1959
|
+
terminalCleanupQueue = cleanup.then(() => undefined, () => undefined);
|
|
1960
|
+
return cleanup;
|
|
1961
|
+
};
|
|
1962
|
+
const waitForTerminalCleanup = async (): Promise<void> => {
|
|
1963
|
+
await terminalCleanupQueue;
|
|
1964
|
+
};
|
|
1965
|
+
const trimSavedResults = (): void => {
|
|
1796
1966
|
const candidates = threads.listAll()
|
|
1797
1967
|
.filter((thread) => ["done", "failed", "stopped"].includes(thread.state))
|
|
1798
1968
|
.sort((left, right) => left.timestamps.createdAt - right.timestamps.createdAt);
|
|
1799
1969
|
const retainedBytes = (): number => [...savedResults.values()].reduce((total, result) => total + resultBytes(result), 0);
|
|
1800
1970
|
while ((savedResults.size > limits.threadRetentionRecords || retainedBytes() > limits.threadRetentionBytes) && candidates.length) {
|
|
1801
1971
|
const candidate = candidates.shift()!;
|
|
1802
|
-
savedResults.delete(candidate.id);
|
|
1803
|
-
const current = threads.inspect(candidate.id);
|
|
1804
|
-
if (current && ["done", "failed", "stopped"].includes(current.state))
|
|
1972
|
+
savedResults.delete(candidate.id);
|
|
1973
|
+
const current = threads.inspect(candidate.id);
|
|
1974
|
+
if (current && ["done", "failed", "stopped"].includes(current.state)) {
|
|
1975
|
+
threads.close(candidate.id);
|
|
1976
|
+
void queueTerminalCleanup(candidate.id).catch(() => {});
|
|
1977
|
+
}
|
|
1805
1978
|
}
|
|
1806
1979
|
pruneClosedThreads();
|
|
1807
1980
|
};
|
|
@@ -1896,27 +2069,28 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1896
2069
|
}
|
|
1897
2070
|
}
|
|
1898
2071
|
if (runtime) runtime.traceCount = next.trace.length;
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
if (
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2072
|
+
const output = lifecycleOutput(effective.output);
|
|
2073
|
+
const handoff = output ? { summary: output } : undefined;
|
|
2074
|
+
thread = threads.patch(threadId, { usage: threadUsage(effective.usage), result: output, handoff });
|
|
2075
|
+
const restartPending = runtime?.restarting === true && ["cancelled", "complete"].includes(next.status);
|
|
2076
|
+
if (restartPending) return effective;
|
|
2077
|
+
if (effective.status === "complete") {
|
|
2078
|
+
threads.complete(threadId, { usage: threadUsage(effective.usage), result: output, handoff });
|
|
2079
|
+
} else if (effective.status === "failed") {
|
|
2080
|
+
threads.fail(threadId, {
|
|
2081
|
+
usage: threadUsage(effective.usage),
|
|
2082
|
+
result: output,
|
|
2083
|
+
handoff,
|
|
2084
|
+
message: effective.errorMessage ?? effective.terminationReason ?? "child failed",
|
|
2085
|
+
code: effective.terminationReason,
|
|
2086
|
+
});
|
|
2087
|
+
} else if (effective.status === "cancelled" || effective.status === "limited") {
|
|
2088
|
+
threads.stop(threadId, {
|
|
2089
|
+
usage: threadUsage(effective.usage),
|
|
2090
|
+
result: output,
|
|
2091
|
+
handoff,
|
|
2092
|
+
reason: effective.terminationReason ?? (effective.status === "limited" ? "resource_limit" : "interrupted"),
|
|
2093
|
+
});
|
|
1920
2094
|
}
|
|
1921
2095
|
return effective;
|
|
1922
2096
|
};
|
|
@@ -2097,16 +2271,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2097
2271
|
});
|
|
2098
2272
|
}
|
|
2099
2273
|
|
|
2100
|
-
const toolDefinition: Parameters<ExtensionAPI["registerTool"]>[0] = {
|
|
2101
|
-
name: "subagent",
|
|
2102
|
-
label: "Subagents",
|
|
2103
|
-
description: `Spawn and manage named child threads.
|
|
2104
|
-
promptSnippet: "Delegate bounded
|
|
2105
|
-
promptGuidelines: [
|
|
2106
|
-
"
|
|
2107
|
-
"Parallel tasks with write-capable roles use one shared slot because all children share the parent worktree.",
|
|
2274
|
+
const toolDefinition: Parameters<ExtensionAPI["registerTool"]>[0] = {
|
|
2275
|
+
name: "subagent",
|
|
2276
|
+
label: "Subagents",
|
|
2277
|
+
description: `Spawn and manage named child threads. A task-only spawn creates a generic read-only child; agent may name an optional custom role from an approved agents folder or provide 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.`,
|
|
2278
|
+
promptSnippet: "Delegate bounded work to isolated KillerOS subagents",
|
|
2279
|
+
promptGuidelines: [
|
|
2280
|
+
"Omit agent for a generic read-only child; use an approved custom role name or inline role when the task needs a specific prompt or write access.",
|
|
2281
|
+
"Parallel tasks with write-capable roles use one shared slot because all children share the parent worktree.",
|
|
2108
2282
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
2109
|
-
"
|
|
2283
|
+
"Default model and thinking are inherited. If the user requires one model for the batch, pass model; set thinking only when requested, and use inherit to follow the selected role default or active parent.",
|
|
2110
2284
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
2111
2285
|
],
|
|
2112
2286
|
parameters: createSubagentParams(limits),
|
|
@@ -2230,10 +2404,8 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2230
2404
|
if (thread.state === "queued" || thread.state === "active") {
|
|
2231
2405
|
throw new Error(`Cannot close thread ${thread.id} from ${thread.state}`);
|
|
2232
2406
|
}
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
const exitConfirmed = exits.every(Boolean);
|
|
2236
|
-
if (!exitConfirmed) {
|
|
2407
|
+
const exitConfirmed = await queueTerminalCleanup(thread.id);
|
|
2408
|
+
if (!exitConfirmed) {
|
|
2237
2409
|
const current = savedResults.get(thread.id);
|
|
2238
2410
|
if (current) {
|
|
2239
2411
|
const failed = cloneResult(current);
|
|
@@ -2244,83 +2416,101 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2244
2416
|
saveResult(thread.id, failed);
|
|
2245
2417
|
recordSnapshot(thread, failed);
|
|
2246
2418
|
}
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
|
|
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);
|
|
2419
|
+
}
|
|
2420
|
+
threads.close(thread.id as SubagentThreadId);
|
|
2421
|
+
savedResults.delete(thread.id);
|
|
2260
2422
|
pruneClosedThreads();
|
|
2261
2423
|
return actionResult(exitConfirmed
|
|
2262
2424
|
? `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}). Heavy trace and handoff data were evicted; a tombstone remains inspectable.`
|
|
2263
2425
|
: `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}); process exit was not confirmed, so its session directory was retained.`, thread.id);
|
|
2264
2426
|
}
|
|
2265
2427
|
|
|
2266
|
-
let resumeTarget: ThreadSnapshot | undefined;
|
|
2267
|
-
let resumePrompt: string | undefined;
|
|
2268
|
-
|
|
2428
|
+
let resumeTarget: ThreadSnapshot | undefined;
|
|
2429
|
+
let resumePrompt: string | undefined;
|
|
2430
|
+
let resumeAgent: AgentRole | undefined;
|
|
2431
|
+
const parentTools = activeParentTools(pi);
|
|
2432
|
+
const isResume = request.kind === "resume";
|
|
2269
2433
|
if (isResume) {
|
|
2270
2434
|
const target = resolveOwnedThread(request.input.threadId, parentId);
|
|
2271
2435
|
if (!target) throw new Error(`Unknown child thread ${JSON.stringify(request.input.threadId)}`);
|
|
2272
2436
|
if (!terminalThread(target) || target.state === "closed") {
|
|
2273
2437
|
throw new Error(`Cannot resume thread ${target.id} from ${target.state}`);
|
|
2274
2438
|
}
|
|
2275
|
-
|
|
2439
|
+
resumeAgent = threadMetadata.get(target.id)?.agent;
|
|
2440
|
+
if (!resumeAgent && target.role === "generic") resumeAgent = genericAgentRole(parentTools);
|
|
2441
|
+
if (!resumeAgent && savedResults.get(target.id)?.agentSource === "inline") {
|
|
2276
2442
|
throw new Error(`Cannot resume inline role ${JSON.stringify(target.role)}; inline roles are scoped to one spawn`);
|
|
2277
2443
|
}
|
|
2278
2444
|
resumePrompt = request.input.task;
|
|
2279
2445
|
resumeTarget = target;
|
|
2280
|
-
}
|
|
2281
|
-
const spawnRequest = (isResume
|
|
2282
|
-
? {
|
|
2283
|
-
|
|
2446
|
+
}
|
|
2447
|
+
const spawnRequest = (isResume
|
|
2448
|
+
? {
|
|
2449
|
+
kind: "spawn-single",
|
|
2450
|
+
input: {
|
|
2451
|
+
agent: resumeAgent ?? resumeTarget!.role,
|
|
2452
|
+
task: resumePrompt ?? resumeTarget!.prompt,
|
|
2453
|
+
model: resumeTarget!.model,
|
|
2454
|
+
...(threadMetadata.get(resumeTarget!.id)?.thinking
|
|
2455
|
+
? { thinking: threadMetadata.get(resumeTarget!.id)!.thinking }
|
|
2456
|
+
: {}),
|
|
2457
|
+
},
|
|
2458
|
+
}
|
|
2459
|
+
: request) as Extract<NormalizedSubagentRequest, { kind: "spawn-single" | "spawn-parallel" | "spawn-chain" }>;
|
|
2284
2460
|
const params = spawnRequest.input;
|
|
2285
|
-
const scope: AgentScope = params.agentScope ?? "user";
|
|
2286
|
-
const hasParallel = spawnRequest.kind === "spawn-parallel";
|
|
2287
|
-
const hasChain = spawnRequest.kind === "spawn-chain";
|
|
2288
|
-
const writerConcurrencyOverride = hasParallel ? spawnRequest.input.writerConcurrency : undefined;
|
|
2289
|
-
|
|
2290
|
-
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
2291
|
-
const roles = new Map(discovery.agents.map((agent) => [agent.name, agent]));
|
|
2461
|
+
const scope: AgentScope = params.agentScope ?? (isResume && resumeAgent?.source === "project" ? "project" : "user");
|
|
2462
|
+
const hasParallel = spawnRequest.kind === "spawn-parallel";
|
|
2463
|
+
const hasChain = spawnRequest.kind === "spawn-chain";
|
|
2464
|
+
const writerConcurrencyOverride = hasParallel ? spawnRequest.input.writerConcurrency : undefined;
|
|
2465
|
+
|
|
2466
|
+
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
2467
|
+
const roles = new Map(discovery.agents.map((agent) => [agent.name, agent]));
|
|
2292
2468
|
const rawInputs: TaskInput[] = spawnRequest.kind === "spawn-single"
|
|
2293
2469
|
? [{ agent: spawnRequest.input.agent, task: spawnRequest.input.task, ...(spawnRequest.input.name ? { name: spawnRequest.input.name } : {}) }]
|
|
2294
2470
|
: spawnRequest.kind === "spawn-parallel" ? spawnRequest.input.tasks : spawnRequest.input.chain;
|
|
2295
2471
|
const rolesForInputs = rawInputs.map((input) => {
|
|
2296
|
-
if (
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2472
|
+
if (isResume && resumeAgent && input.agent === resumeAgent) {
|
|
2473
|
+
const role = cloneAgentRole(resumeAgent);
|
|
2474
|
+
validateAgentTools(role, parentTools);
|
|
2475
|
+
return role;
|
|
2476
|
+
}
|
|
2477
|
+
if (input.agent === undefined) return genericAgentRole(parentTools);
|
|
2478
|
+
if (typeof input.agent !== "string") {
|
|
2479
|
+
const role = inlineAgentRole(input.agent);
|
|
2480
|
+
validateAgentTools(role, parentTools);
|
|
2481
|
+
return role;
|
|
2302
2482
|
}
|
|
2303
2483
|
const selected = roles.get(input.agent);
|
|
2304
2484
|
if (selected) return selected;
|
|
2305
2485
|
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
|
-
});
|
|
2486
|
+
throw new Error(`Unknown custom subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2487
|
+
});
|
|
2488
|
+
rolesForInputs.forEach((role) => validateAgentTools(role, parentTools));
|
|
2308
2489
|
const inputs: Array<Omit<TaskInput, "agent"> & { agent: string }> = rawInputs.map((input, index) => ({
|
|
2309
2490
|
...input,
|
|
2310
2491
|
agent: rolesForInputs[index]!.name,
|
|
2311
2492
|
}));
|
|
2312
2493
|
|
|
2313
2494
|
const projectRoles = [...new Set(rolesForInputs.filter((role) => role.source === "project"))];
|
|
2314
|
-
if (projectRoles.length) {
|
|
2495
|
+
if (projectRoles.length) {
|
|
2315
2496
|
if (!ctx.hasUI) throw new Error("Project-local subagents require interactive confirmation");
|
|
2316
2497
|
const approved = await ctx.ui.confirm(
|
|
2317
2498
|
"Run project-local subagents?",
|
|
2318
2499
|
`Roles: ${projectRoles.map((role) => role.name).join(", ")}\nSources:\n${projectRoles.map((role) => role.filePath).join("\n")}\n\nThese trusted repository files control child prompts and tools.`,
|
|
2319
2500
|
);
|
|
2320
|
-
if (!approved) throw new Error("Project-local subagents were not approved");
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2501
|
+
if (!approved) throw new Error("Project-local subagents were not approved");
|
|
2502
|
+
}
|
|
2503
|
+
const legacyRoles = [...new Set(rolesForInputs.filter((role) => role.source === "legacy"))];
|
|
2504
|
+
if (legacyRoles.length) {
|
|
2505
|
+
if (!ctx.hasUI) throw new Error("Legacy persisted subagents require interactive confirmation");
|
|
2506
|
+
const approved = await ctx.ui.confirm(
|
|
2507
|
+
"Resume legacy subagents?",
|
|
2508
|
+
`Roles: ${legacyRoles.map((role) => role.name).join(", ")}\nThese saved roles came from an earlier KillerOS release and their original source is unavailable.`,
|
|
2509
|
+
);
|
|
2510
|
+
if (!approved) throw new Error("Legacy persisted subagents were not approved");
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
const resolvedModels = rolesForInputs.map((role) => resolveAgentModel(role, ctx, params.model, params.thinking));
|
|
2324
2514
|
|
|
2325
2515
|
const mode: SubagentDetails["mode"] = hasParallel ? "parallel" : hasChain ? "chain" : "single";
|
|
2326
2516
|
if (inputs.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
@@ -2350,9 +2540,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2350
2540
|
throw new Error(`At most ${limits.maxTasks} child threads may be active at once`);
|
|
2351
2541
|
}
|
|
2352
2542
|
|
|
2353
|
-
const existingThreads = threads.listAll().filter((thread) => thread.parentId === parentId).map((thread) => threadView(thread, threadMetadata));
|
|
2354
|
-
const allocatedNames = new Set<string>();
|
|
2355
|
-
if (isResume)
|
|
2543
|
+
const existingThreads = threads.listAll().filter((thread) => thread.parentId === parentId).map((thread) => threadView(thread, threadMetadata));
|
|
2544
|
+
const allocatedNames = new Set<string>();
|
|
2545
|
+
if (isResume) {
|
|
2546
|
+
resumeTarget = resumeThread(resumeTarget!, resumePrompt);
|
|
2547
|
+
const metadata = threadMetadata.get(resumeTarget.id);
|
|
2548
|
+
if (metadata) {
|
|
2549
|
+
metadata.agent = cloneAgentRole(rolesForInputs[0]!);
|
|
2550
|
+
metadata.thinking = resolvedModels[0]!.thinking;
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2356
2553
|
const threadRecords = isResume
|
|
2357
2554
|
? [resumeTarget!]
|
|
2358
2555
|
: inputs.map((input, index) => {
|
|
@@ -2375,7 +2572,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2375
2572
|
session: { id: "killeros-pending", directory: path.join(os.tmpdir(), "killeros-subagent-pending") },
|
|
2376
2573
|
} as any);
|
|
2377
2574
|
const session = childSessionPath(ctx, thread.id) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
2378
|
-
threadMetadata.set(thread.id, {
|
|
2575
|
+
threadMetadata.set(thread.id, {
|
|
2576
|
+
displayName,
|
|
2577
|
+
attempt: 1,
|
|
2578
|
+
session,
|
|
2579
|
+
persistentSession: Boolean(session.directory),
|
|
2580
|
+
agent: cloneAgentRole(rolesForInputs[index]!),
|
|
2581
|
+
thinking: resolvedModels[index]!.thinking,
|
|
2582
|
+
});
|
|
2379
2583
|
recordSpawn(thread);
|
|
2380
2584
|
return thread;
|
|
2381
2585
|
});
|
|
@@ -2575,13 +2779,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2575
2779
|
runtime.aggregate = limited;
|
|
2576
2780
|
results[index] = cloneResult(limited);
|
|
2577
2781
|
saveResult(threadId, limited);
|
|
2578
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2782
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2783
|
+
const output = lifecycleOutput(limited.output);
|
|
2784
|
+
threads.stop(threadId, {
|
|
2785
|
+
usage: threadUsage(limited.usage),
|
|
2786
|
+
result: output,
|
|
2787
|
+
handoff: output ? { summary: output } : undefined,
|
|
2788
|
+
reason,
|
|
2789
|
+
});
|
|
2585
2790
|
}
|
|
2586
2791
|
emit();
|
|
2587
2792
|
};
|
|
@@ -2592,9 +2797,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2592
2797
|
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
2593
2798
|
const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
|
|
2594
2799
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
2595
|
-
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
2596
|
-
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
2597
|
-
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
2800
|
+
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
2801
|
+
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
2802
|
+
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
2803
|
+
const usedTurns = aggregate?.usage.turns ?? 0;
|
|
2804
|
+
const maxTurns = limits.maxTurns ?? limits.defaultMaxTurns;
|
|
2805
|
+
const quotaTokens = limits.quotaTokens ?? limits.defaultQuotaTokens;
|
|
2598
2806
|
if (remainingWallTimeMs !== undefined && remainingWallTimeMs <= 0) {
|
|
2599
2807
|
stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
|
|
2600
2808
|
break;
|
|
@@ -2611,10 +2819,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2611
2819
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
2612
2820
|
break;
|
|
2613
2821
|
}
|
|
2614
|
-
if (
|
|
2615
|
-
stopForBudget("
|
|
2616
|
-
break;
|
|
2617
|
-
}
|
|
2822
|
+
if (usedTurns >= maxTurns) {
|
|
2823
|
+
stopForBudget("turn_limit", `Child thread reaches ${maxTurns} turns`);
|
|
2824
|
+
break;
|
|
2825
|
+
}
|
|
2826
|
+
if (usedTokens >= quotaTokens) {
|
|
2827
|
+
stopForBudget("quota_tokens", `Child thread reaches ${quotaTokens} tokens`);
|
|
2828
|
+
break;
|
|
2829
|
+
}
|
|
2618
2830
|
if (limits.quotaUsd !== undefined && usedCost >= limits.quotaUsd) {
|
|
2619
2831
|
stopForBudget("quota_cost", `Child thread exceeds $${limits.quotaUsd}`);
|
|
2620
2832
|
break;
|
|
@@ -2638,10 +2850,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2638
2850
|
limits: {
|
|
2639
2851
|
...limits,
|
|
2640
2852
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
|
|
2641
|
-
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
|
|
2642
|
-
...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
|
|
2643
|
-
|
|
2644
|
-
|
|
2853
|
+
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
|
|
2854
|
+
...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
|
|
2855
|
+
maxTurns: maxTurns - usedTurns,
|
|
2856
|
+
quotaTokens: quotaTokens - usedTokens,
|
|
2857
|
+
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
2645
2858
|
},
|
|
2646
2859
|
timeoutMs: remainingWallTimeMs,
|
|
2647
2860
|
onHandle: (handle) => {
|
|
@@ -2676,14 +2889,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2676
2889
|
runtime.aggregate = cancelled;
|
|
2677
2890
|
results[index] = cloneResult(cancelled);
|
|
2678
2891
|
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2679
|
-
saveResult(threadId, cancelled);
|
|
2680
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2892
|
+
saveResult(threadId, cancelled);
|
|
2893
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2894
|
+
const output = lifecycleOutput(cancelled.output);
|
|
2895
|
+
threads.stop(threadId, {
|
|
2896
|
+
usage: threadUsage(cancelled.usage),
|
|
2897
|
+
result: output,
|
|
2898
|
+
handoff: output ? { summary: output } : undefined,
|
|
2899
|
+
reason: cancelled.terminationReason,
|
|
2900
|
+
});
|
|
2687
2901
|
}
|
|
2688
2902
|
emit();
|
|
2689
2903
|
}
|
|
@@ -2697,13 +2911,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2697
2911
|
runtime.aggregate = unconfirmed;
|
|
2698
2912
|
results[index] = cloneResult(unconfirmed);
|
|
2699
2913
|
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, unconfirmed);
|
|
2700
|
-
if (runtime.sessionGeneration === sessionGeneration && !threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2914
|
+
if (runtime.sessionGeneration === sessionGeneration && !threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2915
|
+
const output = lifecycleOutput(unconfirmed.output);
|
|
2916
|
+
threads.fail(threadId, {
|
|
2917
|
+
usage: threadUsage(unconfirmed.usage),
|
|
2918
|
+
result: output,
|
|
2919
|
+
handoff: output ? { summary: output } : undefined,
|
|
2920
|
+
message,
|
|
2921
|
+
code: "process_exit_unconfirmed",
|
|
2707
2922
|
});
|
|
2708
2923
|
}
|
|
2709
2924
|
emit();
|
|
@@ -2711,14 +2926,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2711
2926
|
}
|
|
2712
2927
|
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) break;
|
|
2713
2928
|
const steering = runtime.steering.splice(0);
|
|
2714
|
-
runtime.restarting = false;
|
|
2715
|
-
runtime.requestedReason = undefined;
|
|
2716
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2929
|
+
runtime.restarting = false;
|
|
2930
|
+
runtime.requestedReason = undefined;
|
|
2931
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2932
|
+
const output = lifecycleOutput(runtime.aggregate.output);
|
|
2933
|
+
threads.patch(threadId, {
|
|
2934
|
+
usage: threadUsage(runtime.aggregate.usage),
|
|
2935
|
+
result: output,
|
|
2936
|
+
handoff: output ? { summary: output } : undefined,
|
|
2937
|
+
});
|
|
2722
2938
|
}
|
|
2723
2939
|
if (steeredTaskWouldExceedLimit(task, steering, limits.taskCharacters)) {
|
|
2724
2940
|
const failed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
@@ -2727,15 +2943,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2727
2943
|
failed.errorMessage = `Expanded task plus steering exceeds ${limits.taskCharacters} characters`;
|
|
2728
2944
|
runtime.aggregate = failed;
|
|
2729
2945
|
results[index] = cloneResult(failed);
|
|
2730
|
-
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2731
|
-
saveResult(threadId, failed);
|
|
2732
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2946
|
+
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2947
|
+
saveResult(threadId, failed);
|
|
2948
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2949
|
+
const output = lifecycleOutput(failed.output);
|
|
2950
|
+
threads.fail(threadId, {
|
|
2951
|
+
usage: threadUsage(failed.usage),
|
|
2952
|
+
result: output,
|
|
2953
|
+
handoff: output ? { summary: output } : undefined,
|
|
2954
|
+
message: failed.errorMessage,
|
|
2955
|
+
code: failed.terminationReason,
|
|
2739
2956
|
});
|
|
2740
2957
|
}
|
|
2741
2958
|
emit();
|
|
@@ -2765,14 +2982,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2765
2982
|
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2766
2983
|
saveResult(threadId, unconfirmed);
|
|
2767
2984
|
const current = threads.inspect(threadId);
|
|
2768
|
-
if (current?.state === "active") {
|
|
2769
|
-
if (unconfirmed.status === "failed") {
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2985
|
+
if (current?.state === "active") {
|
|
2986
|
+
if (unconfirmed.status === "failed") {
|
|
2987
|
+
const output = lifecycleOutput(unconfirmed.output);
|
|
2988
|
+
threads.fail(threadId, {
|
|
2989
|
+
usage: threadUsage(unconfirmed.usage),
|
|
2990
|
+
result: output,
|
|
2991
|
+
handoff: output ? { summary: output } : undefined,
|
|
2992
|
+
message: unconfirmed.errorMessage ?? "Child process exit was not confirmed before cleanup",
|
|
2993
|
+
code: unconfirmed.terminationReason,
|
|
2776
2994
|
});
|
|
2777
2995
|
} else {
|
|
2778
2996
|
threads.stop(threadId, { reason: unconfirmed.terminationReason ?? "process_exit_unconfirmed" });
|
|
@@ -2780,14 +2998,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2780
2998
|
}
|
|
2781
2999
|
emit();
|
|
2782
3000
|
}
|
|
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
|
-
|
|
3001
|
+
} else if (!persistentSession) {
|
|
3002
|
+
try {
|
|
3003
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
3004
|
+
} catch {
|
|
3005
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (allExited && runtime.sessionGeneration === sessionGeneration) threadResources.delete(threadId);
|
|
3009
|
+
}
|
|
2791
3010
|
emit();
|
|
2792
3011
|
};
|
|
2793
3012
|
|
|
@@ -2843,11 +3062,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2843
3062
|
} finally {
|
|
2844
3063
|
settleQueued("parallel_stopped");
|
|
2845
3064
|
}
|
|
2846
|
-
} else {
|
|
2847
|
-
await runAt(0, inputs[0]!.task);
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
|
|
3065
|
+
} else {
|
|
3066
|
+
await runAt(0, inputs[0]!.task);
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
await waitForTerminalCleanup();
|
|
3070
|
+
const board = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2851
3071
|
const currentResults = results.map(cloneResult);
|
|
2852
3072
|
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
2853
3073
|
const toolContent = buildToolContent(mode, details.results, limits.toolOutputBytes);
|
|
@@ -2876,9 +3096,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2876
3096
|
|
|
2877
3097
|
const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2878
3098
|
const queuedResults = results.map(cloneResult);
|
|
2879
|
-
const queuedDetails: SubagentDetails = {
|
|
2880
|
-
...queuedBoard,
|
|
2881
|
-
|
|
3099
|
+
const queuedDetails: SubagentDetails = {
|
|
3100
|
+
...queuedBoard,
|
|
3101
|
+
backgroundStarted: true,
|
|
3102
|
+
executionNote,
|
|
2882
3103
|
results: queuedResults,
|
|
2883
3104
|
aggregateUsage: aggregateUsage(queuedResults),
|
|
2884
3105
|
};
|
|
@@ -2954,17 +3175,28 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2954
3175
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${spawnRequest.input.tasks.length} · ${schedule}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2955
3176
|
}
|
|
2956
3177
|
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);
|
|
2957
|
-
const agentName =
|
|
3178
|
+
const agentName = spawnRequest.input.agent === undefined
|
|
3179
|
+
? "generic"
|
|
3180
|
+
: typeof spawnRequest.input.agent === "string" ? spawnRequest.input.agent : spawnRequest.input.agent.name;
|
|
2958
3181
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", agentName)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2959
3182
|
},
|
|
2960
3183
|
|
|
2961
3184
|
renderResult(result, { expanded }, theme) {
|
|
2962
3185
|
const details = result.details as SubagentDetails | undefined;
|
|
2963
|
-
if (!details?.results.length) {
|
|
2964
|
-
const first = result.content[0];
|
|
2965
|
-
return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
|
|
2966
|
-
}
|
|
2967
|
-
|
|
3186
|
+
if (!details?.results.length) {
|
|
3187
|
+
const first = result.content[0];
|
|
3188
|
+
return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
|
|
3189
|
+
}
|
|
3190
|
+
if (details.backgroundStarted) {
|
|
3191
|
+
const lines = [
|
|
3192
|
+
theme.fg("toolTitle", theme.bold(`Started in background (${details.results.length})`)),
|
|
3193
|
+
...details.results.map((task) => `${theme.fg("toolTitle", theme.bold(task.name ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1}`)}`),
|
|
3194
|
+
];
|
|
3195
|
+
if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
|
|
3196
|
+
lines.push(theme.fg("dim", "Live status appears below while child threads run."));
|
|
3197
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
3198
|
+
}
|
|
3199
|
+
const board = formatThreadBoard({
|
|
2968
3200
|
title: `Subagents · ${details.mode}`,
|
|
2969
3201
|
threads: details.results.map(threadBoardRecord),
|
|
2970
3202
|
selectedThreadId: details.selectedThreadId,
|