killeros 1.5.7 → 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 +18 -0
- package/README.md +13 -24
- package/killeros/goals.ts +9 -8
- package/killeros/question.ts +19 -4
- package/killeros/subagents.ts +488 -279
- 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,
|
|
@@ -48,7 +47,7 @@ const ROLE_FIELDS = new Set(["name", "description", "access", "tools", "model",
|
|
|
48
47
|
|
|
49
48
|
type ThinkingLevel = ModelThinkingLevel;
|
|
50
49
|
export type AgentAccess = "read" | "write";
|
|
51
|
-
export type AgentSource = "bundled" | "personal" | "project" | "inline";
|
|
50
|
+
export type AgentSource = "bundled" | "personal" | "project" | "inline" | "legacy";
|
|
52
51
|
export type AgentScope = "user" | "project" | "both";
|
|
53
52
|
export type SubagentStatus = "queued" | "running" | "complete" | "failed" | "cancelled" | "limited" | "orphaned";
|
|
54
53
|
|
|
@@ -206,9 +205,10 @@ type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
|
206
205
|
quotaUsd?: number;
|
|
207
206
|
};
|
|
208
207
|
|
|
209
|
-
export interface SubagentRuntimeOptions {
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
export interface SubagentRuntimeOptions {
|
|
209
|
+
/** Explicit test or embedding role directory; KillerOS ships no bundled roles. */
|
|
210
|
+
bundledAgentsDir?: string;
|
|
211
|
+
userAgentsDir?: string;
|
|
212
212
|
webExtension?: string;
|
|
213
213
|
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SpawnedProcess;
|
|
214
214
|
limits?: Partial<SubagentLimits>;
|
|
@@ -400,20 +400,21 @@ function findProjectAgentsDir(cwd: string): string | null {
|
|
|
400
400
|
}
|
|
401
401
|
}
|
|
402
402
|
|
|
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");
|
|
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");
|
|
412
412
|
const wantsProject = scope === "project" || scope === "both";
|
|
413
413
|
if (wantsProject && !projectTrusted) throw new Error("Project agents require a trusted project");
|
|
414
414
|
const projectAgentsDir = wantsProject ? findProjectAgentsDir(cwd) : null;
|
|
415
415
|
|
|
416
|
-
const layers: Array<{ dir: string; source: AgentSource }> = [
|
|
416
|
+
const layers: Array<{ dir: string; source: AgentSource }> = [];
|
|
417
|
+
if (bundledDir) layers.push({ dir: bundledDir, source: "bundled" });
|
|
417
418
|
if (scope === "user" || scope === "both") layers.push({ dir: userDir, source: "personal" });
|
|
418
419
|
if (wantsProject && projectAgentsDir) layers.push({ dir: projectAgentsDir, source: "project" });
|
|
419
420
|
|
|
@@ -816,11 +817,11 @@ type SpawnOptions = {
|
|
|
816
817
|
agentScope?: AgentScope;
|
|
817
818
|
};
|
|
818
819
|
|
|
819
|
-
type SpawnSingleRequest = SpawnOptions & {
|
|
820
|
-
action?: "spawn";
|
|
821
|
-
agent
|
|
822
|
-
task: string;
|
|
823
|
-
name?: string;
|
|
820
|
+
type SpawnSingleRequest = SpawnOptions & {
|
|
821
|
+
action?: "spawn";
|
|
822
|
+
agent?: AgentSpec;
|
|
823
|
+
task: string;
|
|
824
|
+
name?: string;
|
|
824
825
|
};
|
|
825
826
|
|
|
826
827
|
type SpawnParallelRequest = SpawnOptions & {
|
|
@@ -905,14 +906,15 @@ function requireOnlyFields(record: Record<string, unknown>, allowed: readonly st
|
|
|
905
906
|
if (invalid) throw new Error(`Invalid subagent request: field ${JSON.stringify(invalid)} is not valid with action ${JSON.stringify(action)}.`);
|
|
906
907
|
}
|
|
907
908
|
|
|
908
|
-
function parseAgentSpec(value: unknown): AgentSpec {
|
|
909
|
-
if (
|
|
910
|
-
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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.");
|
|
916
918
|
}
|
|
917
919
|
const role = requireRecord(value, "inline role");
|
|
918
920
|
for (const field of Object.keys(role)) {
|
|
@@ -948,28 +950,119 @@ function parseAgentSpec(value: unknown): AgentSpec {
|
|
|
948
950
|
return { name, description, access, tools };
|
|
949
951
|
}
|
|
950
952
|
|
|
951
|
-
function inlineAgentRole(role: InlineAgentRole): AgentRole {
|
|
952
|
-
return {
|
|
953
|
-
...role,
|
|
954
|
-
prompt: role.description,
|
|
955
|
-
source: "inline",
|
|
956
|
-
filePath: `inline:${role.name}`,
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
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[] {
|
|
961
1053
|
if (!Array.isArray(value) || value.length === 0) {
|
|
962
1054
|
throw new Error(`Invalid subagent request: ${field} must be a non-empty array.`);
|
|
963
1055
|
}
|
|
964
1056
|
if (value.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
965
1057
|
return value.map((entry, index) => {
|
|
966
|
-
const task = requireRecord(entry, `${field}[${index}]`);
|
|
967
|
-
requireOnlyFields(task, ["agent", "task", "name"], `spawn ${field}`);
|
|
968
|
-
const name = optionalThreadName(task);
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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 }),
|
|
973
1066
|
};
|
|
974
1067
|
});
|
|
975
1068
|
}
|
|
@@ -1146,25 +1239,25 @@ function prepareSubagentRequest(
|
|
|
1146
1239
|
}
|
|
1147
1240
|
|
|
1148
1241
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
1149
|
-
const inlineAgentSchema = Type.Object({
|
|
1150
|
-
name: Type.String({ minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$" }),
|
|
1151
|
-
description: Type.String({ minLength: 1, maxLength: 500 }),
|
|
1152
|
-
access: StringEnum(["read", "write"] as const),
|
|
1153
|
-
tools: Type.Array(Type.String({ minLength: 1 }), { minItems: 1, uniqueItems: true }),
|
|
1154
|
-
}, { additionalProperties: false, description: "
|
|
1155
|
-
const agentSchema = Type.Union([
|
|
1156
|
-
Type.String({ minLength: 1, maxLength: 64, description: "
|
|
1157
|
-
inlineAgentSchema,
|
|
1158
|
-
]);
|
|
1159
|
-
const taskSchema = Type.Object({
|
|
1160
|
-
agent: agentSchema,
|
|
1161
|
-
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Bounded task
|
|
1162
|
-
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1163
|
-
}, { additionalProperties: false });
|
|
1164
|
-
const chainTaskSchema = Type.Object({
|
|
1165
|
-
agent: agentSchema,
|
|
1166
|
-
task: Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task with optional {previous}
|
|
1167
|
-
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" })),
|
|
1168
1261
|
}, { additionalProperties: false });
|
|
1169
1262
|
const threadId = Type.String({ minLength: 1, maxLength: 128, description: "Existing child thread ID; omit when spawning because KillerOS creates it" });
|
|
1170
1263
|
return Type.Object({
|
|
@@ -1179,12 +1272,12 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
1179
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` })),
|
|
1180
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` })),
|
|
1181
1274
|
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
1182
|
-
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "
|
|
1183
|
-
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "
|
|
1184
|
-
agentScope: Type.Optional(StringEnum(["user", "project", "both"] as const, {
|
|
1185
|
-
default: "user",
|
|
1186
|
-
description: "
|
|
1187
|
-
})),
|
|
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
|
+
})),
|
|
1188
1281
|
}, { additionalProperties: false });
|
|
1189
1282
|
}
|
|
1190
1283
|
|
|
@@ -1239,7 +1332,7 @@ function buildSteeredTask(task: string, steering: readonly string[], maxCharacte
|
|
|
1239
1332
|
return `${taskText}${steeringLabel}${steeringText}`;
|
|
1240
1333
|
}
|
|
1241
1334
|
|
|
1242
|
-
export type TaskInput = { agent
|
|
1335
|
+
export type TaskInput = { agent?: AgentSpec; task: string; name?: string };
|
|
1243
1336
|
|
|
1244
1337
|
function steeredTaskWouldExceedLimit(task: string, steering: readonly string[], maxCharacters: number): boolean {
|
|
1245
1338
|
if (!steering.length) return false;
|
|
@@ -1303,12 +1396,14 @@ interface ChildSession {
|
|
|
1303
1396
|
directory: string;
|
|
1304
1397
|
}
|
|
1305
1398
|
|
|
1306
|
-
interface ThreadMetadata {
|
|
1307
|
-
displayName: string;
|
|
1308
|
-
attempt: number;
|
|
1309
|
-
session: ChildSession;
|
|
1310
|
-
persistentSession: boolean;
|
|
1311
|
-
|
|
1399
|
+
interface ThreadMetadata {
|
|
1400
|
+
displayName: string;
|
|
1401
|
+
attempt: number;
|
|
1402
|
+
session: ChildSession;
|
|
1403
|
+
persistentSession: boolean;
|
|
1404
|
+
agent?: AgentRole;
|
|
1405
|
+
thinking?: ThinkingLevel;
|
|
1406
|
+
}
|
|
1312
1407
|
|
|
1313
1408
|
type ThreadSnapshot = SubagentThread & {
|
|
1314
1409
|
displayName?: string;
|
|
@@ -1450,7 +1545,7 @@ function restorePersistedResult(value: unknown, thread: ThreadSnapshot): Subagen
|
|
|
1450
1545
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1451
1546
|
const source = value as Record<string, any>;
|
|
1452
1547
|
const statuses = new Set<SubagentStatus>(["queued", "running", "complete", "failed", "cancelled", "limited"]);
|
|
1453
|
-
const agentSources = new Set<SubagentTaskResult["agentSource"]>(["bundled", "personal", "project", "inline", "unknown"]);
|
|
1548
|
+
const agentSources = new Set<SubagentTaskResult["agentSource"]>(["bundled", "personal", "project", "inline", "legacy", "unknown"]);
|
|
1454
1549
|
if (typeof source.id !== "string" || source.id !== thread.id) return undefined;
|
|
1455
1550
|
const status = source.status ?? (thread.state === "done" ? "complete" : thread.state === "failed" ? "failed" : "cancelled");
|
|
1456
1551
|
if (typeof status !== "string" || !statuses.has(status as SubagentStatus)) return undefined;
|
|
@@ -1538,23 +1633,26 @@ function threadBoardRecord(result: SubagentTaskResult): ThreadBoardRecord {
|
|
|
1538
1633
|
}
|
|
1539
1634
|
|
|
1540
1635
|
export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeOptions = {}): SubagentControlApi {
|
|
1541
|
-
const limits = { ...SUBAGENT_LIMITS, ...options.limits };
|
|
1542
|
-
const spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1543
|
-
let threads = new SubagentThreadRegistry();
|
|
1544
|
-
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
1545
|
-
const threadMetadata = new Map<string, ThreadMetadata>();
|
|
1546
|
-
|
|
1547
|
-
const
|
|
1548
|
-
const
|
|
1549
|
-
const
|
|
1550
|
-
|
|
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;
|
|
1551
1648
|
let persistenceWarning: string | undefined;
|
|
1552
1649
|
|
|
1553
|
-
const persistText = (value: string | undefined, maxBytes: number): string | undefined => {
|
|
1554
|
-
if (value === undefined) return undefined;
|
|
1555
|
-
return truncateUtf8(value, maxBytes).text;
|
|
1556
|
-
};
|
|
1557
|
-
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 => {
|
|
1558
1656
|
const appendEntry = (pi as unknown as { appendEntry?: (type: string, data: unknown) => void }).appendEntry;
|
|
1559
1657
|
if (!appendEntry) return;
|
|
1560
1658
|
try {
|
|
@@ -1563,9 +1661,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1563
1661
|
persistenceWarning ??= `Subagent persistence is unavailable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1564
1662
|
}
|
|
1565
1663
|
};
|
|
1566
|
-
const persistedThread = (thread: ThreadSnapshot): Record<string, unknown> => {
|
|
1567
|
-
const session = threadSession(thread, threadMetadata) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
1568
|
-
|
|
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 {
|
|
1569
1669
|
id: thread.id,
|
|
1570
1670
|
parentId: thread.parentId,
|
|
1571
1671
|
displayName: threadDisplayName(thread, threadMetadata),
|
|
@@ -1573,8 +1673,23 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1573
1673
|
role: thread.role,
|
|
1574
1674
|
prompt: clipCharacters(thread.prompt, limits.taskCharacters),
|
|
1575
1675
|
model: thread.model,
|
|
1576
|
-
tools: thread.tools.slice(0, 32).map((tool) => clipCharacters(tool, 64)),
|
|
1577
|
-
|
|
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 },
|
|
1578
1693
|
session: { ...session },
|
|
1579
1694
|
state: thread.state,
|
|
1580
1695
|
usage: { ...thread.usage },
|
|
@@ -1643,12 +1758,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1643
1758
|
});
|
|
1644
1759
|
};
|
|
1645
1760
|
|
|
1646
|
-
const restoreRecords = (
|
|
1647
|
-
entries: readonly unknown[],
|
|
1648
|
-
parentId: string,
|
|
1649
|
-
expectedSession?: (threadId: string) => ChildSession | undefined,
|
|
1650
|
-
): Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }> => {
|
|
1651
|
-
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 }>();
|
|
1652
1767
|
for (const entry of entries) {
|
|
1653
1768
|
try {
|
|
1654
1769
|
if (!entry || typeof entry !== "object") continue;
|
|
@@ -1659,10 +1774,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1659
1774
|
const record = data as Record<string, any>;
|
|
1660
1775
|
if (record.version !== 1 || typeof record.parentId !== "string" || record.parentId !== parentId) continue;
|
|
1661
1776
|
if (record.event === "spawn" || record.event === "snapshot") {
|
|
1662
|
-
const rawThread = record.thread;
|
|
1777
|
+
const rawThread = record.thread;
|
|
1663
1778
|
if (!rawThread || typeof rawThread !== "object" || typeof rawThread.id !== "string" || rawThread.parentId !== parentId) continue;
|
|
1664
1779
|
if (typeof rawThread.role !== "string" || typeof rawThread.prompt !== "string" || typeof rawThread.model !== "string") continue;
|
|
1665
|
-
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;
|
|
1666
1785
|
thread.prompt = clipCharacters(thread.prompt, limits.taskCharacters);
|
|
1667
1786
|
thread.result = typeof rawThread.result === "string" ? persistText(rawThread.result, 256 * 1024) : rawThread.result;
|
|
1668
1787
|
thread.tools = Array.isArray(rawThread.tools) ? rawThread.tools.slice(0, 32) : [];
|
|
@@ -1679,13 +1798,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1679
1798
|
id: `killeros-${safeSessionId(thread.id)}`,
|
|
1680
1799
|
directory: "",
|
|
1681
1800
|
};
|
|
1682
|
-
if (thread.state === "queued" || thread.state === "active") {
|
|
1683
|
-
thread.state = "orphaned" as SubagentThreadState;
|
|
1684
|
-
thread.stopReason = "parent_restarted";
|
|
1685
|
-
}
|
|
1686
|
-
const result = record.result === undefined ? undefined : restorePersistedResult(record.result, thread);
|
|
1687
|
-
if (record.result !== undefined && !result) continue;
|
|
1688
|
-
|
|
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 });
|
|
1689
1812
|
} else if (record.event === "close" && typeof record.id === "string") {
|
|
1690
1813
|
const previous = records.get(record.id);
|
|
1691
1814
|
if (!previous) continue;
|
|
@@ -1708,7 +1831,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1708
1831
|
return [...records.values()];
|
|
1709
1832
|
};
|
|
1710
1833
|
|
|
1711
|
-
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult }>): void => {
|
|
1834
|
+
const installRestoredThreads = (restored: Array<{ thread: ThreadSnapshot; result?: SubagentTaskResult; agent?: AgentRole; thinking?: ThinkingLevel }>): void => {
|
|
1712
1835
|
if (!restored.length) return;
|
|
1713
1836
|
const ids = [...restored.map(({ thread }) => thread.id)];
|
|
1714
1837
|
let idIndex = 0;
|
|
@@ -1732,17 +1855,20 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1732
1855
|
attempt: thread.attempt,
|
|
1733
1856
|
session: thread.session,
|
|
1734
1857
|
} as any);
|
|
1735
|
-
threadMetadata.set(created.id, {
|
|
1736
|
-
displayName: thread.displayName ?? thread.role,
|
|
1737
|
-
attempt: thread.attempt ?? 1,
|
|
1738
|
-
session: thread.session ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" },
|
|
1739
|
-
persistentSession: Boolean(thread.session?.directory),
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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 });
|
|
1746
1872
|
} else if (thread.state === "failed") {
|
|
1747
1873
|
threads.begin(created.id);
|
|
1748
1874
|
threads.fail(created.id, { message: thread.failure?.message ?? entry.result?.errorMessage ?? "restored failure" });
|
|
@@ -1777,8 +1903,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1777
1903
|
}
|
|
1778
1904
|
};
|
|
1779
1905
|
|
|
1780
|
-
const rememberEvictedThreads = (threadsToRemember: readonly SubagentThread[]): void => {
|
|
1781
|
-
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
|
+
}
|
|
1782
1911
|
while (evictedThreadParents.size > maxClosedThreads) {
|
|
1783
1912
|
const oldest = evictedThreadParents.keys().next().value;
|
|
1784
1913
|
if (oldest === undefined) break;
|
|
@@ -1790,23 +1919,62 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1790
1919
|
rememberEvictedThreads(threads.pruneClosed(maxClosedThreads));
|
|
1791
1920
|
};
|
|
1792
1921
|
|
|
1793
|
-
const resultBytes = (result: SubagentTaskResult): number => Buffer.byteLength([
|
|
1794
|
-
result.task,
|
|
1795
|
-
...result.trace,
|
|
1922
|
+
const resultBytes = (result: SubagentTaskResult): number => Buffer.byteLength([
|
|
1923
|
+
result.task,
|
|
1924
|
+
...result.trace,
|
|
1796
1925
|
result.stderr,
|
|
1797
1926
|
result.output,
|
|
1798
|
-
result.errorMessage ?? "",
|
|
1799
|
-
].join("\n"), "utf8");
|
|
1800
|
-
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 => {
|
|
1801
1966
|
const candidates = threads.listAll()
|
|
1802
1967
|
.filter((thread) => ["done", "failed", "stopped"].includes(thread.state))
|
|
1803
1968
|
.sort((left, right) => left.timestamps.createdAt - right.timestamps.createdAt);
|
|
1804
1969
|
const retainedBytes = (): number => [...savedResults.values()].reduce((total, result) => total + resultBytes(result), 0);
|
|
1805
1970
|
while ((savedResults.size > limits.threadRetentionRecords || retainedBytes() > limits.threadRetentionBytes) && candidates.length) {
|
|
1806
1971
|
const candidate = candidates.shift()!;
|
|
1807
|
-
savedResults.delete(candidate.id);
|
|
1808
|
-
const current = threads.inspect(candidate.id);
|
|
1809
|
-
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
|
+
}
|
|
1810
1978
|
}
|
|
1811
1979
|
pruneClosedThreads();
|
|
1812
1980
|
};
|
|
@@ -1901,27 +2069,28 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1901
2069
|
}
|
|
1902
2070
|
}
|
|
1903
2071
|
if (runtime) runtime.traceCount = next.trace.length;
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
if (
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
+
});
|
|
1925
2094
|
}
|
|
1926
2095
|
return effective;
|
|
1927
2096
|
};
|
|
@@ -2102,16 +2271,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2102
2271
|
});
|
|
2103
2272
|
}
|
|
2104
2273
|
|
|
2105
|
-
const toolDefinition: Parameters<ExtensionAPI["registerTool"]>[0] = {
|
|
2106
|
-
name: "subagent",
|
|
2107
|
-
label: "Subagents",
|
|
2108
|
-
description: `Spawn and manage named child threads.
|
|
2109
|
-
promptSnippet: "Delegate bounded
|
|
2110
|
-
promptGuidelines: [
|
|
2111
|
-
"
|
|
2112
|
-
"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.",
|
|
2113
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.",
|
|
2114
|
-
"
|
|
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.",
|
|
2115
2284
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
2116
2285
|
],
|
|
2117
2286
|
parameters: createSubagentParams(limits),
|
|
@@ -2235,10 +2404,8 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2235
2404
|
if (thread.state === "queued" || thread.state === "active") {
|
|
2236
2405
|
throw new Error(`Cannot close thread ${thread.id} from ${thread.state}`);
|
|
2237
2406
|
}
|
|
2238
|
-
const
|
|
2239
|
-
|
|
2240
|
-
const exitConfirmed = exits.every(Boolean);
|
|
2241
|
-
if (!exitConfirmed) {
|
|
2407
|
+
const exitConfirmed = await queueTerminalCleanup(thread.id);
|
|
2408
|
+
if (!exitConfirmed) {
|
|
2242
2409
|
const current = savedResults.get(thread.id);
|
|
2243
2410
|
if (current) {
|
|
2244
2411
|
const failed = cloneResult(current);
|
|
@@ -2249,83 +2416,101 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2249
2416
|
saveResult(thread.id, failed);
|
|
2250
2417
|
recordSnapshot(thread, failed);
|
|
2251
2418
|
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
const expectedDirectory = childSessionPath(ctx, thread.id)?.directory;
|
|
2256
|
-
const trustedRestoredDirectory = !resource && directory && expectedDirectory
|
|
2257
|
-
&& path.resolve(directory) === path.resolve(expectedDirectory);
|
|
2258
|
-
if ((resource?.persistent || trustedRestoredDirectory) && directory) {
|
|
2259
|
-
await rm(directory, { recursive: true, force: true });
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
threads.close(thread.id as SubagentThreadId);
|
|
2263
|
-
if (exitConfirmed) threadResources.delete(thread.id);
|
|
2264
|
-
savedResults.delete(thread.id);
|
|
2419
|
+
}
|
|
2420
|
+
threads.close(thread.id as SubagentThreadId);
|
|
2421
|
+
savedResults.delete(thread.id);
|
|
2265
2422
|
pruneClosedThreads();
|
|
2266
2423
|
return actionResult(exitConfirmed
|
|
2267
2424
|
? `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}). Heavy trace and handoff data were evicted; a tombstone remains inspectable.`
|
|
2268
2425
|
: `Closed ${threadDisplayName(thread, threadMetadata)} (${thread.id}); process exit was not confirmed, so its session directory was retained.`, thread.id);
|
|
2269
2426
|
}
|
|
2270
2427
|
|
|
2271
|
-
let resumeTarget: ThreadSnapshot | undefined;
|
|
2272
|
-
let resumePrompt: string | undefined;
|
|
2273
|
-
|
|
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";
|
|
2274
2433
|
if (isResume) {
|
|
2275
2434
|
const target = resolveOwnedThread(request.input.threadId, parentId);
|
|
2276
2435
|
if (!target) throw new Error(`Unknown child thread ${JSON.stringify(request.input.threadId)}`);
|
|
2277
2436
|
if (!terminalThread(target) || target.state === "closed") {
|
|
2278
2437
|
throw new Error(`Cannot resume thread ${target.id} from ${target.state}`);
|
|
2279
2438
|
}
|
|
2280
|
-
|
|
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") {
|
|
2281
2442
|
throw new Error(`Cannot resume inline role ${JSON.stringify(target.role)}; inline roles are scoped to one spawn`);
|
|
2282
2443
|
}
|
|
2283
2444
|
resumePrompt = request.input.task;
|
|
2284
2445
|
resumeTarget = target;
|
|
2285
|
-
}
|
|
2286
|
-
const spawnRequest = (isResume
|
|
2287
|
-
? {
|
|
2288
|
-
|
|
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" }>;
|
|
2289
2460
|
const params = spawnRequest.input;
|
|
2290
|
-
const scope: AgentScope = params.agentScope ?? "user";
|
|
2291
|
-
const hasParallel = spawnRequest.kind === "spawn-parallel";
|
|
2292
|
-
const hasChain = spawnRequest.kind === "spawn-chain";
|
|
2293
|
-
const writerConcurrencyOverride = hasParallel ? spawnRequest.input.writerConcurrency : undefined;
|
|
2294
|
-
|
|
2295
|
-
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
2296
|
-
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]));
|
|
2297
2468
|
const rawInputs: TaskInput[] = spawnRequest.kind === "spawn-single"
|
|
2298
2469
|
? [{ agent: spawnRequest.input.agent, task: spawnRequest.input.task, ...(spawnRequest.input.name ? { name: spawnRequest.input.name } : {}) }]
|
|
2299
2470
|
: spawnRequest.kind === "spawn-parallel" ? spawnRequest.input.tasks : spawnRequest.input.chain;
|
|
2300
2471
|
const rolesForInputs = rawInputs.map((input) => {
|
|
2301
|
-
if (
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
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;
|
|
2307
2482
|
}
|
|
2308
2483
|
const selected = roles.get(input.agent);
|
|
2309
2484
|
if (selected) return selected;
|
|
2310
2485
|
const available = discovery.agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
|
|
2311
|
-
throw new Error(`Unknown subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2312
|
-
});
|
|
2486
|
+
throw new Error(`Unknown custom subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2487
|
+
});
|
|
2488
|
+
rolesForInputs.forEach((role) => validateAgentTools(role, parentTools));
|
|
2313
2489
|
const inputs: Array<Omit<TaskInput, "agent"> & { agent: string }> = rawInputs.map((input, index) => ({
|
|
2314
2490
|
...input,
|
|
2315
2491
|
agent: rolesForInputs[index]!.name,
|
|
2316
2492
|
}));
|
|
2317
2493
|
|
|
2318
2494
|
const projectRoles = [...new Set(rolesForInputs.filter((role) => role.source === "project"))];
|
|
2319
|
-
if (projectRoles.length) {
|
|
2495
|
+
if (projectRoles.length) {
|
|
2320
2496
|
if (!ctx.hasUI) throw new Error("Project-local subagents require interactive confirmation");
|
|
2321
2497
|
const approved = await ctx.ui.confirm(
|
|
2322
2498
|
"Run project-local subagents?",
|
|
2323
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.`,
|
|
2324
2500
|
);
|
|
2325
|
-
if (!approved) throw new Error("Project-local subagents were not approved");
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
|
|
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));
|
|
2329
2514
|
|
|
2330
2515
|
const mode: SubagentDetails["mode"] = hasParallel ? "parallel" : hasChain ? "chain" : "single";
|
|
2331
2516
|
if (inputs.length > limits.maxTasks) throw new Error(`At most ${limits.maxTasks} subagent tasks are allowed`);
|
|
@@ -2355,9 +2540,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2355
2540
|
throw new Error(`At most ${limits.maxTasks} child threads may be active at once`);
|
|
2356
2541
|
}
|
|
2357
2542
|
|
|
2358
|
-
const existingThreads = threads.listAll().filter((thread) => thread.parentId === parentId).map((thread) => threadView(thread, threadMetadata));
|
|
2359
|
-
const allocatedNames = new Set<string>();
|
|
2360
|
-
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
|
+
}
|
|
2361
2553
|
const threadRecords = isResume
|
|
2362
2554
|
? [resumeTarget!]
|
|
2363
2555
|
: inputs.map((input, index) => {
|
|
@@ -2380,7 +2572,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2380
2572
|
session: { id: "killeros-pending", directory: path.join(os.tmpdir(), "killeros-subagent-pending") },
|
|
2381
2573
|
} as any);
|
|
2382
2574
|
const session = childSessionPath(ctx, thread.id) ?? { id: `killeros-${safeSessionId(thread.id)}`, directory: "" };
|
|
2383
|
-
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
|
+
});
|
|
2384
2583
|
recordSpawn(thread);
|
|
2385
2584
|
return thread;
|
|
2386
2585
|
});
|
|
@@ -2580,13 +2779,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2580
2779
|
runtime.aggregate = limited;
|
|
2581
2780
|
results[index] = cloneResult(limited);
|
|
2582
2781
|
saveResult(threadId, limited);
|
|
2583
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
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
|
+
});
|
|
2590
2790
|
}
|
|
2591
2791
|
emit();
|
|
2592
2792
|
};
|
|
@@ -2689,14 +2889,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2689
2889
|
runtime.aggregate = cancelled;
|
|
2690
2890
|
results[index] = cloneResult(cancelled);
|
|
2691
2891
|
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2692
|
-
saveResult(threadId, cancelled);
|
|
2693
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
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
|
+
});
|
|
2700
2901
|
}
|
|
2701
2902
|
emit();
|
|
2702
2903
|
}
|
|
@@ -2710,13 +2911,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2710
2911
|
runtime.aggregate = unconfirmed;
|
|
2711
2912
|
results[index] = cloneResult(unconfirmed);
|
|
2712
2913
|
if (runtime.sessionGeneration === sessionGeneration) saveResult(threadId, unconfirmed);
|
|
2713
|
-
if (runtime.sessionGeneration === sessionGeneration && !threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
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",
|
|
2720
2922
|
});
|
|
2721
2923
|
}
|
|
2722
2924
|
emit();
|
|
@@ -2724,14 +2926,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2724
2926
|
}
|
|
2725
2927
|
if (runtime.sessionGeneration !== sessionGeneration || controller.signal.aborted || threads.isDisposed) break;
|
|
2726
2928
|
const steering = runtime.steering.splice(0);
|
|
2727
|
-
runtime.restarting = false;
|
|
2728
|
-
runtime.requestedReason = undefined;
|
|
2729
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
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
|
+
});
|
|
2735
2938
|
}
|
|
2736
2939
|
if (steeredTaskWouldExceedLimit(task, steering, limits.taskCharacters)) {
|
|
2737
2940
|
const failed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
@@ -2740,15 +2943,16 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2740
2943
|
failed.errorMessage = `Expanded task plus steering exceeds ${limits.taskCharacters} characters`;
|
|
2741
2944
|
runtime.aggregate = failed;
|
|
2742
2945
|
results[index] = cloneResult(failed);
|
|
2743
|
-
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2744
|
-
saveResult(threadId, failed);
|
|
2745
|
-
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
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,
|
|
2752
2956
|
});
|
|
2753
2957
|
}
|
|
2754
2958
|
emit();
|
|
@@ -2778,14 +2982,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2778
2982
|
if (runtime.sessionGeneration === sessionGeneration) {
|
|
2779
2983
|
saveResult(threadId, unconfirmed);
|
|
2780
2984
|
const current = threads.inspect(threadId);
|
|
2781
|
-
if (current?.state === "active") {
|
|
2782
|
-
if (unconfirmed.status === "failed") {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
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,
|
|
2789
2994
|
});
|
|
2790
2995
|
} else {
|
|
2791
2996
|
threads.stop(threadId, { reason: unconfirmed.terminationReason ?? "process_exit_unconfirmed" });
|
|
@@ -2793,14 +2998,15 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2793
2998
|
}
|
|
2794
2999
|
emit();
|
|
2795
3000
|
}
|
|
2796
|
-
} else if (!persistentSession) {
|
|
2797
|
-
try {
|
|
2798
|
-
await rm(sessionDirectory, { recursive: true, force: true });
|
|
2799
|
-
} catch {
|
|
2800
|
-
// Temporary child session cleanup is best effort after process termination.
|
|
2801
|
-
}
|
|
2802
|
-
}
|
|
2803
|
-
|
|
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
|
+
}
|
|
2804
3010
|
emit();
|
|
2805
3011
|
};
|
|
2806
3012
|
|
|
@@ -2856,11 +3062,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2856
3062
|
} finally {
|
|
2857
3063
|
settleQueued("parallel_stopped");
|
|
2858
3064
|
}
|
|
2859
|
-
} else {
|
|
2860
|
-
await runAt(0, inputs[0]!.task);
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
|
|
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);
|
|
2864
3071
|
const currentResults = results.map(cloneResult);
|
|
2865
3072
|
const details: SubagentDetails = { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) };
|
|
2866
3073
|
const toolContent = buildToolContent(mode, details.results, limits.toolOutputBytes);
|
|
@@ -2968,7 +3175,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2968
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);
|
|
2969
3176
|
}
|
|
2970
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);
|
|
2971
|
-
const agentName =
|
|
3178
|
+
const agentName = spawnRequest.input.agent === undefined
|
|
3179
|
+
? "generic"
|
|
3180
|
+
: typeof spawnRequest.input.agent === "string" ? spawnRequest.input.agent : spawnRequest.input.agent.name;
|
|
2972
3181
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", agentName)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
2973
3182
|
},
|
|
2974
3183
|
|