killeros 1.4.7 → 1.4.9
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 +11 -2
- package/Killeros.ts +335 -70
- package/README.md +8 -8
- package/package.json +6 -6
- package/subagent-lifecycle.ts +27 -1
- package/subagent-process.ts +26 -2
- package/subagents.ts +241 -49
package/subagents.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
17
17
|
import { Type } from "typebox";
|
|
18
18
|
import { SubagentThreadRegistry, type SubagentThread, type SubagentThreadId, type SubagentThreadState } from "./subagent-lifecycle.ts";
|
|
19
|
-
import { runSubagentProcess, type SubagentProcessHandle, type SubagentProcessResult } from "./subagent-process.ts";
|
|
19
|
+
import { MAX_NODE_TIMER_MS, runSubagentProcess, type SubagentProcessHandle, type SubagentProcessResult } from "./subagent-process.ts";
|
|
20
20
|
import { formatThreadBoard, formatThreadInspection, type ThreadRecord as ThreadBoardRecord } from "./subagent-ui.ts";
|
|
21
21
|
|
|
22
22
|
export const SUBAGENT_LIMITS = {
|
|
@@ -26,6 +26,8 @@ export const SUBAGENT_LIMITS = {
|
|
|
26
26
|
traceRetentionBytes: 8 * 1024 * 1024,
|
|
27
27
|
stderrRetentionBytes: 1 * 1024 * 1024,
|
|
28
28
|
taskOutputRetentionBytes: 1 * 1024 * 1024,
|
|
29
|
+
threadRetentionRecords: 64,
|
|
30
|
+
threadRetentionBytes: 128 * 1024 * 1024,
|
|
29
31
|
roleFileBytes: 64 * 1024,
|
|
30
32
|
taskCharacters: 20_000,
|
|
31
33
|
killGraceMs: 5_000,
|
|
@@ -302,7 +304,7 @@ function parseAgentFile(filePath: string, source: AgentSource, limits: SubagentL
|
|
|
302
304
|
tools,
|
|
303
305
|
model: typeof modelValue === "string" ? modelValue.trim() : undefined,
|
|
304
306
|
thinking: typeof thinkingValue === "string" ? thinkingValue.trim() : undefined,
|
|
305
|
-
timeoutMs: optionalPositiveInteger(frontmatter, filePath, "timeoutMs"),
|
|
307
|
+
timeoutMs: optionalPositiveInteger(frontmatter, filePath, "timeoutMs", undefined, MAX_NODE_TIMER_MS),
|
|
306
308
|
prompt,
|
|
307
309
|
source,
|
|
308
310
|
filePath,
|
|
@@ -713,6 +715,22 @@ async function mapReadTasks<T>(items: T[], concurrency: number, run: (item: T, i
|
|
|
713
715
|
await Promise.all(workers);
|
|
714
716
|
}
|
|
715
717
|
|
|
718
|
+
function waitForConfirmedProcessExit(handle: SubagentProcessHandle, timeoutMs = 1_000): Promise<boolean> {
|
|
719
|
+
if (handle.hasExited) return Promise.resolve(true);
|
|
720
|
+
return new Promise((resolve) => {
|
|
721
|
+
let settled = false;
|
|
722
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
723
|
+
const finish = (exited: boolean): void => {
|
|
724
|
+
if (settled) return;
|
|
725
|
+
settled = true;
|
|
726
|
+
if (timeout) clearTimeout(timeout);
|
|
727
|
+
resolve(exited);
|
|
728
|
+
};
|
|
729
|
+
timeout = setTimeout(() => finish(handle.hasExited), timeoutMs);
|
|
730
|
+
void handle.exited.then(() => finish(true));
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
716
734
|
function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxReadConcurrency" | "taskCharacters">) {
|
|
717
735
|
const taskSchema = Type.Object({
|
|
718
736
|
agent: Type.String({ minLength: 1, maxLength: 64, description: "Agent role name" }),
|
|
@@ -732,7 +750,8 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
732
750
|
all: Type.Optional(Type.Boolean({ description: "Interrupt every active child thread" })),
|
|
733
751
|
agent: Type.Optional(Type.String({ minLength: 1, maxLength: 64, description: "Agent role for single mode" })),
|
|
734
752
|
task: Type.Optional(Type.String({ minLength: 1, maxLength: limits.taskCharacters, description: "Task for single mode" })),
|
|
735
|
-
tasks: Type.Optional(Type.Array(taskSchema, { minItems: 1, maxItems: limits.maxTasks, description: `Parallel role tasks: read-only
|
|
753
|
+
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` })),
|
|
754
|
+
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` })),
|
|
736
755
|
chain: Type.Optional(Type.Array(chainTaskSchema, { minItems: 1, maxItems: limits.maxTasks, description: "Sequential role tasks; {previous} inserts the prior result" })),
|
|
737
756
|
model: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Model for every task as provider/model; inherit uses each role setting or the active parent" })),
|
|
738
757
|
thinking: Type.Optional(Type.String({ minLength: 1, maxLength: 16, description: "Thinking effort for every task: off, minimal, low, medium, high, xhigh, max, or inherit" })),
|
|
@@ -758,6 +777,41 @@ function clipCharacters(text: string, maxCharacters: number, fromEnd = false): s
|
|
|
758
777
|
return (fromEnd ? characters.slice(-maxCharacters) : characters.slice(0, maxCharacters)).join("");
|
|
759
778
|
}
|
|
760
779
|
|
|
780
|
+
function codePointLength(text: string): number {
|
|
781
|
+
let length = 0;
|
|
782
|
+
for (const _character of text) length += 1;
|
|
783
|
+
return length;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function expandChainTask(template: string, previous: string, maxCharacters: number): string | undefined {
|
|
787
|
+
const placeholder = "{previous}";
|
|
788
|
+
let occurrences = 0;
|
|
789
|
+
let searchFrom = 0;
|
|
790
|
+
while (true) {
|
|
791
|
+
const index = template.indexOf(placeholder, searchFrom);
|
|
792
|
+
if (index < 0) break;
|
|
793
|
+
occurrences += 1;
|
|
794
|
+
searchFrom = index + placeholder.length;
|
|
795
|
+
}
|
|
796
|
+
if (occurrences === 0) return codePointLength(template) <= maxCharacters ? template : undefined;
|
|
797
|
+
|
|
798
|
+
const expandedCharacters = codePointLength(template) + occurrences * (codePointLength(previous) - codePointLength(placeholder));
|
|
799
|
+
if (expandedCharacters > maxCharacters) return undefined;
|
|
800
|
+
|
|
801
|
+
const pieces: string[] = [];
|
|
802
|
+
let start = 0;
|
|
803
|
+
while (true) {
|
|
804
|
+
const index = template.indexOf(placeholder, start);
|
|
805
|
+
if (index < 0) {
|
|
806
|
+
pieces.push(template.slice(start));
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
pieces.push(template.slice(start, index), previous);
|
|
810
|
+
start = index + placeholder.length;
|
|
811
|
+
}
|
|
812
|
+
return pieces.join("");
|
|
813
|
+
}
|
|
814
|
+
|
|
761
815
|
function buildSteeredTask(task: string, steering: readonly string[], maxCharacters: number): string {
|
|
762
816
|
const steeringLabel = "\n\nParent steering:\n";
|
|
763
817
|
const steeringText = clipCharacters(steering.join("\n"), Math.max(0, maxCharacters - [...steeringLabel].length), true);
|
|
@@ -805,6 +859,7 @@ function statusIcon(status: SubagentStatus): string {
|
|
|
805
859
|
interface ActiveThreadRuntime {
|
|
806
860
|
controller: AbortController;
|
|
807
861
|
handle?: SubagentProcessHandle;
|
|
862
|
+
handles: Set<SubagentProcessHandle>;
|
|
808
863
|
steering: string[];
|
|
809
864
|
restarting: boolean;
|
|
810
865
|
traceCount: number;
|
|
@@ -918,6 +973,49 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
918
973
|
const threads = new SubagentThreadRegistry();
|
|
919
974
|
const activeRuntimes = new Map<string, ActiveThreadRuntime>();
|
|
920
975
|
const savedResults = new Map<string, SubagentTaskResult>();
|
|
976
|
+
const evictedThreadParents = new Map<string, string | undefined>();
|
|
977
|
+
const maxClosedThreads = Number.isSafeInteger(limits.threadRetentionRecords) && limits.threadRetentionRecords > 0
|
|
978
|
+
? limits.threadRetentionRecords
|
|
979
|
+
: SUBAGENT_LIMITS.threadRetentionRecords;
|
|
980
|
+
|
|
981
|
+
const rememberEvictedThreads = (threadsToRemember: readonly SubagentThread[]): void => {
|
|
982
|
+
for (const thread of threadsToRemember) evictedThreadParents.set(thread.id, thread.parentId);
|
|
983
|
+
while (evictedThreadParents.size > maxClosedThreads) {
|
|
984
|
+
const oldest = evictedThreadParents.keys().next().value;
|
|
985
|
+
if (oldest === undefined) break;
|
|
986
|
+
evictedThreadParents.delete(oldest);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
const pruneClosedThreads = (): void => {
|
|
990
|
+
if (threads.isDisposed) return;
|
|
991
|
+
rememberEvictedThreads(threads.pruneClosed(maxClosedThreads));
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const resultBytes = (result: SubagentTaskResult): number => Buffer.byteLength([
|
|
995
|
+
result.task,
|
|
996
|
+
...result.trace,
|
|
997
|
+
result.stderr,
|
|
998
|
+
result.output,
|
|
999
|
+
result.errorMessage ?? "",
|
|
1000
|
+
].join("\n"), "utf8");
|
|
1001
|
+
const trimSavedResults = (): void => {
|
|
1002
|
+
const candidates = threads.listAll()
|
|
1003
|
+
.filter((thread) => ["done", "failed", "stopped"].includes(thread.state))
|
|
1004
|
+
.sort((left, right) => left.timestamps.createdAt - right.timestamps.createdAt);
|
|
1005
|
+
const retainedBytes = (): number => [...savedResults.values()].reduce((total, result) => total + resultBytes(result), 0);
|
|
1006
|
+
while ((savedResults.size > limits.threadRetentionRecords || retainedBytes() > limits.threadRetentionBytes) && candidates.length) {
|
|
1007
|
+
const candidate = candidates.shift()!;
|
|
1008
|
+
savedResults.delete(candidate.id);
|
|
1009
|
+
const current = threads.inspect(candidate.id);
|
|
1010
|
+
if (current && ["done", "failed", "stopped"].includes(current.state)) threads.close(candidate.id);
|
|
1011
|
+
}
|
|
1012
|
+
pruneClosedThreads();
|
|
1013
|
+
};
|
|
1014
|
+
const saveResult = (threadId: string, result: SubagentTaskResult): void => {
|
|
1015
|
+
savedResults.delete(threadId);
|
|
1016
|
+
savedResults.set(threadId, cloneResult(result));
|
|
1017
|
+
trimSavedResults();
|
|
1018
|
+
};
|
|
921
1019
|
|
|
922
1020
|
const detailsFor = (
|
|
923
1021
|
parentId: string,
|
|
@@ -928,13 +1026,17 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
928
1026
|
): SubagentDetails => {
|
|
929
1027
|
const all = threads.listAll().filter((thread) => thread.parentId === parentId);
|
|
930
1028
|
const visible = all.filter((thread) => thread.state !== "closed");
|
|
1029
|
+
const selectedClosed = selectedThreadId
|
|
1030
|
+
? all.find((thread) => thread.id === selectedThreadId && thread.state === "closed")
|
|
1031
|
+
: undefined;
|
|
1032
|
+
const listed = selectedClosed ? [...visible, selectedClosed] : visible;
|
|
931
1033
|
const results = visible.map((thread) => threadResult(thread, savedResults.get(thread.id)));
|
|
932
1034
|
return {
|
|
933
1035
|
...cloneDetails(mode, scope, projectAgentsDir, results),
|
|
934
1036
|
parentId,
|
|
935
|
-
threads:
|
|
936
|
-
activeThreads:
|
|
937
|
-
doneThreads:
|
|
1037
|
+
threads: listed,
|
|
1038
|
+
activeThreads: visible.filter((thread) => thread.state === "active"),
|
|
1039
|
+
doneThreads: visible.filter((thread) => ["done", "failed", "stopped"].includes(thread.state)),
|
|
938
1040
|
selectedThreadId,
|
|
939
1041
|
};
|
|
940
1042
|
};
|
|
@@ -962,6 +1064,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
962
1064
|
lines.push(`Trace: ${selected.trace.length} entries`);
|
|
963
1065
|
if (selected.result) lines.push(`Handoff: ${selected.result}`);
|
|
964
1066
|
if (selected.stopReason) lines.push(`Reason: ${selected.stopReason}`);
|
|
1067
|
+
if (selected.evicted) lines.push("Retention: heavy thread data was evicted after close");
|
|
965
1068
|
}
|
|
966
1069
|
}
|
|
967
1070
|
return boundedText(lines.join("\n"), limits.toolOutputBytes, "\n\n[Thread board truncated; inspect a child thread for its bounded detail.]");
|
|
@@ -970,7 +1073,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
970
1073
|
const syncThread = (threadId: SubagentThreadId, next: SubagentTaskResult, runtime?: ActiveThreadRuntime): SubagentTaskResult => {
|
|
971
1074
|
const effective = mergeTaskResults(runtime?.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
972
1075
|
if (runtime?.requestedReason && next.status === "cancelled") effective.terminationReason = runtime.requestedReason;
|
|
973
|
-
|
|
1076
|
+
saveResult(threadId, effective);
|
|
974
1077
|
let thread = threads.inspect(threadId);
|
|
975
1078
|
if (!thread || threads.isDisposed) return effective;
|
|
976
1079
|
if (thread.state === "queued" && next.status === "running") {
|
|
@@ -1024,17 +1127,19 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1024
1127
|
runtime.controller.abort();
|
|
1025
1128
|
}
|
|
1026
1129
|
threads.dispose();
|
|
1130
|
+
savedResults.clear();
|
|
1131
|
+
evictedThreadParents.clear();
|
|
1027
1132
|
});
|
|
1028
1133
|
}
|
|
1029
1134
|
|
|
1030
1135
|
pi.registerTool({
|
|
1031
1136
|
name: "subagent",
|
|
1032
1137
|
label: "Subagents",
|
|
1033
|
-
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks
|
|
1138
|
+
description: `Spawn and manage named child threads. Children finish naturally. Parallel tasks with write-capable roles use one shared slot by default; read-only batches run concurrently up to ${limits.maxReadConcurrency}. Set writerConcurrency above 1 only after proving path ownership in the shared worktree. The message parameter is only valid with action steer. Use action list, inspect, steer, interrupt, collect, and close to manage active and completed handoffs.`,
|
|
1034
1139
|
promptSnippet: "Delegate bounded specialist work to isolated KillerOS subagents",
|
|
1035
1140
|
promptGuidelines: [
|
|
1036
1141
|
"Use subagent for clearly separable specialist work; prefer read-only scout, planner, reviewer, or security roles before a writer.",
|
|
1037
|
-
|
|
1142
|
+
"Parallel tasks with write-capable roles use one shared slot by default because all children share the parent worktree. Set writerConcurrency above 1 only when callers have proved path ownership; callers remain responsible for file conflicts.",
|
|
1038
1143
|
"Every child can load relevant skills with read and can use web_search, source_check, fetch_content, and get_search_content for external research.",
|
|
1039
1144
|
"When the user names a model or thinking effort, pass model and thinking separately; use inherit when the active parent or role setting should decide.",
|
|
1040
1145
|
"Keep completed and stopped threads inspectable until the parent explicitly closes them.",
|
|
@@ -1062,7 +1167,13 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1062
1167
|
if (action === "inspect") {
|
|
1063
1168
|
if (!params.threadId) throw new Error("inspect requires threadId");
|
|
1064
1169
|
const thread = threads.inspect(params.threadId as SubagentThreadId);
|
|
1065
|
-
if (!thread
|
|
1170
|
+
if (!thread) {
|
|
1171
|
+
if (evictedThreadParents.get(params.threadId) === parentId) {
|
|
1172
|
+
return actionResult(`Thread ${params.threadId} was evicted from bounded retention; its heavy data is no longer available.`, params.threadId);
|
|
1173
|
+
}
|
|
1174
|
+
throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1066
1177
|
return actionResult(threadBoardText(parentId, params.threadId), params.threadId);
|
|
1067
1178
|
}
|
|
1068
1179
|
if (action === "steer") {
|
|
@@ -1120,7 +1231,9 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1120
1231
|
const thread = threads.inspect(params.threadId as SubagentThreadId);
|
|
1121
1232
|
if (!thread || thread.parentId !== parentId) throw new Error(`Unknown child thread ${JSON.stringify(params.threadId)}`);
|
|
1122
1233
|
threads.close(params.threadId as SubagentThreadId);
|
|
1123
|
-
|
|
1234
|
+
savedResults.delete(params.threadId);
|
|
1235
|
+
pruneClosedThreads();
|
|
1236
|
+
return actionResult(`Closed ${params.threadId}. Heavy trace and handoff data were evicted; a tombstone remains inspectable.`, params.threadId);
|
|
1124
1237
|
}
|
|
1125
1238
|
|
|
1126
1239
|
const scope: AgentScope = params.agentScope ?? "user";
|
|
@@ -1134,6 +1247,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1134
1247
|
|| hasChain && params.chain!.length === 0) {
|
|
1135
1248
|
throw new Error("Provide exactly one subagent mode: agent + task, tasks, or chain");
|
|
1136
1249
|
}
|
|
1250
|
+
if (params.writerConcurrency !== undefined) {
|
|
1251
|
+
if (!hasParallel) throw new Error("writerConcurrency is only valid with parallel tasks");
|
|
1252
|
+
if (!Number.isSafeInteger(params.writerConcurrency) || params.writerConcurrency < 1 || params.writerConcurrency > limits.maxTasks) {
|
|
1253
|
+
throw new Error(`writerConcurrency must be a positive integer no greater than ${limits.maxTasks}`);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1137
1256
|
|
|
1138
1257
|
const discovery = discoverAgentRoles(ctx.cwd, scope, ctx.isProjectTrusted(), options);
|
|
1139
1258
|
const roles = new Map(discovery.agents.map((agent) => [agent.name, agent]));
|
|
@@ -1171,9 +1290,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1171
1290
|
const writerIndexes = hasParallel
|
|
1172
1291
|
? inputs.map((input, index) => ({ input, index })).filter(({ input }) => roles.get(input.agent)!.access === "write").map(({ index }) => index)
|
|
1173
1292
|
: [];
|
|
1293
|
+
if (params.writerConcurrency !== undefined && hasParallel && writerIndexes.length === 0) {
|
|
1294
|
+
throw new Error("writerConcurrency requires at least one write-capable role");
|
|
1295
|
+
}
|
|
1296
|
+
const writerConcurrency = params.writerConcurrency ?? (writerIndexes.length > 0 ? 1 : limits.maxReadConcurrency);
|
|
1297
|
+
const useSharedParallelPool = hasParallel && writerIndexes.length > 0;
|
|
1174
1298
|
const executionNote = hasParallel
|
|
1175
1299
|
? writerIndexes.length
|
|
1176
|
-
? `Parallel schedule:
|
|
1300
|
+
? `Parallel schedule: all tasks run through a shared pool of up to ${writerConcurrency}${params.writerConcurrency === undefined ? " (safe default)" : " (explicit)"}; concurrent write-capable tasks share the parent worktree, so callers must prove path ownership.`
|
|
1177
1301
|
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
1178
1302
|
: undefined;
|
|
1179
1303
|
|
|
@@ -1201,48 +1325,65 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1201
1325
|
details: { ...board, executionNote, results: currentResults, aggregateUsage: aggregateUsage(currentResults) },
|
|
1202
1326
|
});
|
|
1203
1327
|
};
|
|
1328
|
+
const failQueuedTask = (index: number, reason: string, message: string): void => {
|
|
1329
|
+
const threadId = threadRecords[index]!.id;
|
|
1330
|
+
const thread = threads.inspect(threadId);
|
|
1331
|
+
if (thread?.state === "queued") threads.begin(threadId);
|
|
1332
|
+
results[index] = {
|
|
1333
|
+
...results[index]!,
|
|
1334
|
+
status: "failed",
|
|
1335
|
+
terminationReason: reason,
|
|
1336
|
+
errorMessage: message,
|
|
1337
|
+
};
|
|
1338
|
+
if (threads.inspect(threadId)?.state === "active") threads.fail(threadId, { message, code: reason });
|
|
1339
|
+
saveResult(threadId, results[index]!);
|
|
1340
|
+
emit();
|
|
1341
|
+
};
|
|
1204
1342
|
const runAt = async (index: number, task: string): Promise<void> => {
|
|
1205
1343
|
const threadId = threadRecords[index]!.id;
|
|
1206
1344
|
const initialThread = threads.inspect(threadId);
|
|
1207
1345
|
if (signal?.aborted) {
|
|
1208
1346
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: "abort" };
|
|
1209
1347
|
if (initialThread?.state === "queued" || initialThread?.state === "active") threads.stop(threadId, { reason: "abort" });
|
|
1210
|
-
|
|
1348
|
+
saveResult(threadId, results[index]!);
|
|
1211
1349
|
emit();
|
|
1212
1350
|
return;
|
|
1213
1351
|
}
|
|
1214
1352
|
if (!initialThread) return;
|
|
1215
1353
|
if (initialThread.state === "closed") {
|
|
1216
1354
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: initialThread.stopReason ?? "disposed" };
|
|
1217
|
-
|
|
1355
|
+
saveResult(threadId, results[index]!);
|
|
1218
1356
|
emit();
|
|
1219
1357
|
return;
|
|
1220
1358
|
}
|
|
1221
1359
|
if (initialThread.state === "stopped") {
|
|
1222
1360
|
results[index] = { ...results[index]!, status: "cancelled", terminationReason: initialThread.stopReason ?? "interrupted" };
|
|
1223
|
-
|
|
1361
|
+
saveResult(threadId, results[index]!);
|
|
1224
1362
|
emit();
|
|
1225
1363
|
return;
|
|
1226
1364
|
}
|
|
1227
1365
|
if (initialThread.state !== "queued") return;
|
|
1228
1366
|
const input = inputs[index]!;
|
|
1229
1367
|
threads.begin(threadId);
|
|
1230
|
-
if (
|
|
1231
|
-
|
|
1232
|
-
...results[index]!,
|
|
1233
|
-
status: "failed",
|
|
1234
|
-
terminationReason: "task_limit",
|
|
1235
|
-
errorMessage: `Expanded task exceeds ${limits.taskCharacters} characters`,
|
|
1236
|
-
};
|
|
1237
|
-
threads.fail(threadId, { message: results[index]!.errorMessage ?? "Expanded task exceeds the task limit", code: "task_limit" });
|
|
1238
|
-
savedResults.set(threadId, cloneResult(results[index]!));
|
|
1239
|
-
emit();
|
|
1368
|
+
if (codePointLength(task) > limits.taskCharacters) {
|
|
1369
|
+
failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
|
|
1240
1370
|
return;
|
|
1241
1371
|
}
|
|
1372
|
+
const controller = new AbortController();
|
|
1373
|
+
const runtime: ActiveThreadRuntime = {
|
|
1374
|
+
controller,
|
|
1375
|
+
handles: new Set(),
|
|
1376
|
+
steering: [],
|
|
1377
|
+
restarting: false,
|
|
1378
|
+
traceCount: 0,
|
|
1379
|
+
startedAt: Date.now(),
|
|
1380
|
+
};
|
|
1381
|
+
activeRuntimes.set(threadId, runtime);
|
|
1242
1382
|
let sessionDirectory: string;
|
|
1243
1383
|
try {
|
|
1244
1384
|
sessionDirectory = await mkdtemp(path.join(os.tmpdir(), "killeros-subagent-session-"));
|
|
1245
1385
|
} catch (error) {
|
|
1386
|
+
activeRuntimes.delete(threadId);
|
|
1246
1387
|
const message = error instanceof Error ? error.message : String(error);
|
|
1247
1388
|
results[index] = {
|
|
1248
1389
|
...results[index]!,
|
|
@@ -1251,19 +1392,26 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1251
1392
|
errorMessage: message,
|
|
1252
1393
|
};
|
|
1253
1394
|
threads.fail(threadId, { message, code: "session_error" });
|
|
1254
|
-
|
|
1395
|
+
saveResult(threadId, results[index]!);
|
|
1255
1396
|
emit();
|
|
1256
1397
|
return;
|
|
1257
1398
|
}
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1399
|
+
const currentThread = threads.inspect(threadId);
|
|
1400
|
+
if (controller.signal.aborted || threads.isDisposed || currentThread?.state !== "active") {
|
|
1401
|
+
activeRuntimes.delete(threadId);
|
|
1402
|
+
try {
|
|
1403
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1404
|
+
} catch {
|
|
1405
|
+
// Temporary child session cleanup is best effort before process startup.
|
|
1406
|
+
}
|
|
1407
|
+
const reason = runtime.requestedReason ?? currentThread?.stopReason ?? (threads.isDisposed ? "session_shutdown" : "interrupted");
|
|
1408
|
+
results[index] = { ...results[index]!, status: "cancelled", terminationReason: reason };
|
|
1409
|
+
if (!threads.isDisposed && currentThread?.state === "active") threads.stop(threadId, { reason });
|
|
1410
|
+
saveResult(threadId, results[index]!);
|
|
1411
|
+
emit();
|
|
1412
|
+
return;
|
|
1264
1413
|
}
|
|
1265
|
-
const
|
|
1266
|
-
activeRuntimes.set(threadId, runtime);
|
|
1414
|
+
const sessionId = `killeros-${threadId.replace(/[^A-Za-z0-9_.-]/gu, "_")}`;
|
|
1267
1415
|
const agent = roles.get(input.agent)!;
|
|
1268
1416
|
const queuedSteering = initialThread.steering.map((entry) => entry.message);
|
|
1269
1417
|
let currentTask = queuedSteering.length ? buildSteeredTask(task, queuedSteering, limits.taskCharacters) : task;
|
|
@@ -1274,7 +1422,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1274
1422
|
limited.errorMessage = message;
|
|
1275
1423
|
runtime.aggregate = limited;
|
|
1276
1424
|
results[index] = cloneResult(limited);
|
|
1277
|
-
|
|
1425
|
+
saveResult(threadId, limited);
|
|
1278
1426
|
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
1279
1427
|
threads.stop(threadId, {
|
|
1280
1428
|
usage: threadUsage(limited.usage),
|
|
@@ -1342,7 +1490,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1342
1490
|
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
1343
1491
|
},
|
|
1344
1492
|
timeoutMs: remainingWallTimeMs,
|
|
1345
|
-
onHandle: (handle) => {
|
|
1493
|
+
onHandle: (handle) => {
|
|
1494
|
+
runtime.handle = handle;
|
|
1495
|
+
runtime.handles.add(handle);
|
|
1496
|
+
},
|
|
1346
1497
|
onChange: (changed) => {
|
|
1347
1498
|
results[index] = syncThread(threadId, changed, runtime);
|
|
1348
1499
|
emit();
|
|
@@ -1352,9 +1503,32 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1352
1503
|
runtime.aggregate = mergeTaskResults(runtime.aggregate, next, limits.traceRetentionBytes, limits.stderrRetentionBytes);
|
|
1353
1504
|
runtime.aggregate.task = task;
|
|
1354
1505
|
results[index] = cloneResult(runtime.aggregate);
|
|
1355
|
-
|
|
1506
|
+
saveResult(threadId, runtime.aggregate);
|
|
1356
1507
|
const shouldRestart = runtime.steering.length > 0 && !controller.signal.aborted && (runtime.restarting || next.status === "complete" || next.status === "cancelled");
|
|
1357
1508
|
if (!shouldRestart) break;
|
|
1509
|
+
const previousHandle = runtime.handle;
|
|
1510
|
+
if (previousHandle && !(await waitForConfirmedProcessExit(previousHandle))) {
|
|
1511
|
+
const message = "Child process exit was not confirmed before the steering restart";
|
|
1512
|
+
const unconfirmed = cloneResult(runtime.aggregate ?? results[index]!);
|
|
1513
|
+
unconfirmed.status = "failed";
|
|
1514
|
+
unconfirmed.terminationReason = "process_exit_unconfirmed";
|
|
1515
|
+
unconfirmed.errorMessage = message;
|
|
1516
|
+
runtime.aggregate = unconfirmed;
|
|
1517
|
+
results[index] = cloneResult(unconfirmed);
|
|
1518
|
+
saveResult(threadId, unconfirmed);
|
|
1519
|
+
if (!threads.isDisposed && threads.inspect(threadId)?.state === "active") {
|
|
1520
|
+
threads.fail(threadId, {
|
|
1521
|
+
usage: threadUsage(unconfirmed.usage),
|
|
1522
|
+
result: unconfirmed.output || undefined,
|
|
1523
|
+
handoff: unconfirmed.output ? { summary: unconfirmed.output } : undefined,
|
|
1524
|
+
message,
|
|
1525
|
+
code: "process_exit_unconfirmed",
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
emit();
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
if (controller.signal.aborted || threads.isDisposed) break;
|
|
1358
1532
|
const steering = runtime.steering.splice(0);
|
|
1359
1533
|
runtime.restarting = false;
|
|
1360
1534
|
runtime.requestedReason = undefined;
|
|
@@ -1369,12 +1543,18 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1369
1543
|
}
|
|
1370
1544
|
} finally {
|
|
1371
1545
|
activeRuntimes.delete(threadId);
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1546
|
+
const removeSessionDirectory = async (): Promise<void> => {
|
|
1547
|
+
try {
|
|
1548
|
+
await rm(sessionDirectory, { recursive: true, force: true });
|
|
1549
|
+
} catch {
|
|
1550
|
+
// Temporary child session cleanup is best effort after process termination.
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
const pendingExits = [...runtime.handles]
|
|
1554
|
+
.filter((handle) => !handle.hasExited)
|
|
1555
|
+
.map((handle) => handle.exited);
|
|
1556
|
+
if (!pendingExits.length) await removeSessionDirectory();
|
|
1557
|
+
else void Promise.all(pendingExits).then(removeSessionDirectory);
|
|
1378
1558
|
}
|
|
1379
1559
|
emit();
|
|
1380
1560
|
};
|
|
@@ -1385,14 +1565,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1385
1565
|
if (result.status !== "queued") continue;
|
|
1386
1566
|
const thread = threads.inspect(threadRecords[index]!.id);
|
|
1387
1567
|
const alreadyStopped = thread?.state === "stopped";
|
|
1388
|
-
result.status = signal?.aborted || alreadyStopped ? "cancelled" : "failed";
|
|
1568
|
+
result.status = signal?.aborted || alreadyStopped || reason === "chain_stopped" ? "cancelled" : "failed";
|
|
1389
1569
|
result.terminationReason = alreadyStopped
|
|
1390
1570
|
? thread.stopReason ?? "interrupted"
|
|
1391
1571
|
: signal?.aborted ? "abort" : reason;
|
|
1392
1572
|
if (thread?.state === "queued" || thread?.state === "active") {
|
|
1393
1573
|
threads.stop(threadRecords[index]!.id, { reason: result.terminationReason });
|
|
1394
1574
|
}
|
|
1395
|
-
|
|
1575
|
+
saveResult(threadRecords[index]!.id, result);
|
|
1396
1576
|
}
|
|
1397
1577
|
};
|
|
1398
1578
|
|
|
@@ -1400,7 +1580,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1400
1580
|
if (hasChain) {
|
|
1401
1581
|
let previous = "";
|
|
1402
1582
|
for (let index = 0; index < inputs.length; index += 1) {
|
|
1403
|
-
const task = inputs[index]!.task
|
|
1583
|
+
const task = expandChainTask(inputs[index]!.task, previous, limits.taskCharacters);
|
|
1584
|
+
if (task === undefined) {
|
|
1585
|
+
failQueuedTask(index, "task_limit", `Expanded task exceeds ${limits.taskCharacters} characters`);
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1404
1588
|
await runAt(index, task);
|
|
1405
1589
|
if (results[index]!.status !== "complete") break;
|
|
1406
1590
|
previous = results[index]!.output;
|
|
@@ -1408,8 +1592,13 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1408
1592
|
settleQueued("chain_stopped");
|
|
1409
1593
|
} else if (hasParallel) {
|
|
1410
1594
|
try {
|
|
1411
|
-
|
|
1412
|
-
|
|
1595
|
+
if (useSharedParallelPool) {
|
|
1596
|
+
const indexes = inputs.map((_, index) => index);
|
|
1597
|
+
await mapReadTasks(indexes, writerConcurrency, async (index) => runAt(index, inputs[index]!.task));
|
|
1598
|
+
} else {
|
|
1599
|
+
await mapReadTasks(readIndexes, limits.maxReadConcurrency, async ({ index }) => runAt(index, inputs[index]!.task));
|
|
1600
|
+
for (const index of writerIndexes) await runAt(index, inputs[index]!.task);
|
|
1601
|
+
}
|
|
1413
1602
|
} finally {
|
|
1414
1603
|
settleQueued("parallel_stopped");
|
|
1415
1604
|
}
|
|
@@ -1430,7 +1619,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
1430
1619
|
renderCall(args, theme) {
|
|
1431
1620
|
const scope = args.agentScope ?? "user";
|
|
1432
1621
|
if (args.action && args.action !== "spawn") return new Text(`${theme.fg("toolTitle", theme.bold("threads "))}${theme.fg("accent", args.action)}${theme.fg("dim", args.threadId ? ` · ${args.threadId}` : "")}`, 0, 0);
|
|
1433
|
-
if (args.tasks?.length)
|
|
1622
|
+
if (args.tasks?.length) {
|
|
1623
|
+
const schedule = args.writerConcurrency === undefined ? "parallel default" : `shared pool ${args.writerConcurrency}`;
|
|
1624
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `parallel ${args.tasks.length} · ${schedule}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1625
|
+
}
|
|
1434
1626
|
if (args.chain?.length) return new Text(`${theme.fg("toolTitle", theme.bold("subagents "))}${theme.fg("accent", `chain ${args.chain.length}`)}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1435
1627
|
return new Text(`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent ?? "…")}${theme.fg("dim", ` · ${scope}`)}`, 0, 0);
|
|
1436
1628
|
},
|