killeros 1.5.5 → 1.5.7
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 +16 -0
- package/README.md +5 -5
- package/killeros/subagent-process.ts +20 -13
- package/killeros/subagent-ui.ts +7 -5
- package/killeros/subagents.ts +59 -44
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.5.7] - 2026-08-06
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Stopped runaway subagents at 64 turns or 2,000,000 reported tokens by default while preserving Pi's built-in model retries and the 30-minute wall limit.
|
|
12
|
+
- Kept in-flight model retries labeled `Running`, exposed requested stops as terminal at once, and kept failed or limited children out of the **Active** list.
|
|
13
|
+
- Replaced the frozen queued spawn result with a static launch receipt so only the separate live widget claims current state and usage.
|
|
14
|
+
|
|
15
|
+
## [1.5.6] - 2026-08-05
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Rejected unknown named subagent roles before child launch and returned the available roles instead of falling back to the write-capable `worker`.
|
|
20
|
+
- Kept the real child-boundary test isolated from parent Pi session variables and added direct coverage for inline parallel and chain spawns.
|
|
21
|
+
- Clarified and tested the shared `message` field limits: 20,000 characters for a spawn task alias and 4,000 characters for steering.
|
|
22
|
+
|
|
7
23
|
## [1.5.5] - 2026-08-05
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.md
CHANGED
|
@@ -116,15 +116,15 @@ The tool supports a single `agent` plus `task` or its `message` alias, parallel
|
|
|
116
116
|
| `resume` | `threadId` | `task` |
|
|
117
117
|
| `close` | `threadId` | none |
|
|
118
118
|
|
|
119
|
-
The three spawn shapes cannot be mixed. On a single spawn, `message` aliases `task
|
|
119
|
+
The three spawn shapes cannot be mixed. On a single spawn, `message` aliases `task` with the same 20,000-character limit; supplying both is invalid. With `action: "steer"`, `message` remains required and has a 4,000-character limit, while other lifecycle actions reject it. An unknown named role fails before child launch and reports the available roles. The `wait` action defaults to all queued or active children, waits up to 30 seconds by default, and never stops a child when it times out. The `resume` action keeps the same thread ID, name, session ID, and session directory and increments `attempt`; it requires the original named role and rejects inline roles. KillerOS rejects malformed requests before role discovery, project confirmation, thread creation, or child launch. The TUI shows a parallel or shared-pool schedule only after shape validation; malformed calls show `invalid request` instead of queued work. For example:
|
|
120
120
|
|
|
121
121
|
```json
|
|
122
122
|
{"agent":"reviewer","task":"Review the change","name":"auth-audit","model":"provider/model","thinking":"high"}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
Spawn returns the generated thread IDs immediately while the children continue in the background. This lets the parent use `list`, `inspect`, `wait`, `steer`, `interrupt`, `collect`, `resume`, and `close` in later tool calls. Compact thread records persist through Pi custom session entries. On parent restart, an active record restores as `orphaned`; `close` removes the child session only after confirmed process exit. When the batch settles, KillerOS delivers its bounded handoff as a Pi follow-up and triggers the parent turn. A batch cancelled by parent Escape remains inspectable but does not trigger a replacement turn.
|
|
125
|
+
Spawn returns the generated thread IDs immediately while the children continue in the background. The completed spawn card is a static launch receipt; the separate live widget shows current state and usage until the batch settles. This lets the parent use `list`, `inspect`, `wait`, `steer`, `interrupt`, `collect`, `resume`, and `close` in later tool calls. Compact thread records persist through Pi custom session entries. On parent restart, an active record restores as `orphaned`; `close` removes the child session only after confirmed process exit. When the batch settles, KillerOS delivers its bounded handoff as a Pi follow-up and triggers the parent turn. A batch cancelled by parent Escape remains inspectable but does not trigger a replacement turn.
|
|
126
126
|
|
|
127
|
-
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. An empty final assistant response is a failure.
|
|
127
|
+
Use the separate `model` and `thinking` fields for new configuration. The older `provider/model:thinking` model form remains accepted. Children run as isolated `pi --mode json -p` processes with a private `--session-dir` and `--session-id`, plus explicit local tools and `web_search`, `source_check`, `fetch_content`, and `get_search_content`. Steering restarts the same child session, so the child keeps its prior conversation. Each child explicitly loads `npm:pi-web-access`, discovers available skills, and keeps arbitrary extensions and prompt templates disabled; project-local skills load only when the parent project is trusted. Every bundled role is instructed to load the most relevant `SKILL.md` and report useful evidence. An empty final assistant response is a failure. Each child stops at the first default limit reached: 64 turns, 2,000,000 reported tokens, or 30 minutes. Dollar quotas remain opt-in. Each JSONL record still has a bounded 8 MiB parser ceiling. KillerOS bounds retained trace, stderr, and returned text and spills a large JSONL line to temporary storage; retention never stops a child or marks it `limited`. The parent limits each request to ten tasks, read-only-only batches to four concurrent readers, and bounds role files, task input, and combined parent output. Embedding options can adjust the turn, token, wall-time, and other named resource guards. Aborting the originating parent turn stops its queued and active children; explicit `interrupt` actions and session shutdown also terminate active children and use a bounded 10-second process-exit wait.
|
|
128
128
|
|
|
129
129
|
The command grammar is:
|
|
130
130
|
|
|
@@ -150,7 +150,7 @@ Threads move through `queued`, `active`, `done`, `failed`, `stopped`, `orphaned`
|
|
|
150
150
|
|
|
151
151
|
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; wait for one named or ID child or all queued and active children; steer an active or queued thread with a bounded follow-up (at most 20 pending messages; further steering is rejected explicitly until the child restarts or drains the queue); interrupt one child or all active and queued children; collect a concise handoff into parent context; resume a terminal or orphaned child; and close a finished, stopped, or orphaned thread. An interrupt preserves the partial trace, states the reason, and reports the handoff as partial rather than successful. Closing removes a thread from the active workspace; heavy trace and result payloads are evicted as needed under the bounded retention budget, leaving a small inspectable tombstone.
|
|
152
152
|
|
|
153
|
-
A child completes only when it returns usable final assistant text. The default
|
|
153
|
+
A child completes only when it returns usable final assistant text. The default child limits are 64 turns, 2,000,000 reported tokens, and 30 minutes; dollar quotas remain opt-in, and every JSONL record has an 8 MiB parser ceiling. Explicit embedding options can adjust those limits or add output, trace, stderr, JSONL, or cost guards; each guard reports its cause and returns partial work clearly. The parent still bounds task count, reader concurrency, role files, task input, and combined parent output. Aborting the originating parent turn settles queued work as cancelled and terminates active children; explicit `interrupt` actions and real child-process failures remain visible. Session shutdown also terminates active children and waits up to 10 seconds for confirmed process exit.
|
|
154
154
|
|
|
155
155
|
The replacement lifecycle has nine phases:
|
|
156
156
|
|
|
@@ -160,7 +160,7 @@ The replacement lifecycle has nine phases:
|
|
|
160
160
|
4. **Steer:** append a bounded parent follow-up to an active or queued thread.
|
|
161
161
|
5. **Interrupt:** stop one or all active or queued children while preserving partial work.
|
|
162
162
|
6. **Collect:** return a concise handoff while retaining the expanded trace.
|
|
163
|
-
7. **Guard:**
|
|
163
|
+
7. **Guard:** stop at the default turn, token, or wall-time limit and honor explicit resource-limit overrides.
|
|
164
164
|
8. **Close:** remove a finished or stopped thread from the workspace while retaining a small inspectable tombstone; heavy payloads may be evicted under the retention budget.
|
|
165
165
|
9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, bounded retention, and closure.
|
|
166
166
|
|
|
@@ -81,9 +81,10 @@ export interface SubagentProcessOptions {
|
|
|
81
81
|
spawnProcess?: (args: string[], cwd: string, environment?: NodeJS.ProcessEnv) => SubagentProcessChild;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export interface SubagentProcessLimits {
|
|
85
|
-
wallTimeMs?: number;
|
|
86
|
-
|
|
84
|
+
export interface SubagentProcessLimits {
|
|
85
|
+
wallTimeMs?: number;
|
|
86
|
+
maxTurns?: number;
|
|
87
|
+
jsonlLineBytes?: number;
|
|
87
88
|
traceBytes?: number;
|
|
88
89
|
stderrBytes?: number;
|
|
89
90
|
outputBytes?: number;
|
|
@@ -223,7 +224,7 @@ function hasIsolatedSession(args: readonly string[]): boolean {
|
|
|
223
224
|
|
|
224
225
|
function normalizeLimits(overrides: Partial<SubagentProcessLimits> | undefined): SubagentProcessLimits {
|
|
225
226
|
const limits = { ...SUBAGENT_PROCESS_LIMITS, ...overrides };
|
|
226
|
-
for (const name of ["wallTimeMs", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs", "processExitWaitMs"] as const) {
|
|
227
|
+
for (const name of ["wallTimeMs", "maxTurns", "jsonlLineBytes", "traceBytes", "stderrBytes", "outputBytes", "killGraceMs", "processExitWaitMs"] as const) {
|
|
227
228
|
const value = limits[name];
|
|
228
229
|
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0 || value > MAX_NODE_TIMER_MS
|
|
229
230
|
&& (name === "wallTimeMs" || name === "killGraceMs" || name === "processExitWaitMs"))) {
|
|
@@ -450,11 +451,14 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
450
451
|
publish();
|
|
451
452
|
resolveResult(cloneResult(state));
|
|
452
453
|
};
|
|
453
|
-
const requestTermination = (status: Exclude<SubagentProcessStatus, "running" | "complete">, reason: string, errorMessage?: string): void => {
|
|
454
|
-
if (requestedStatus || closed) return;
|
|
455
|
-
requestedStatus = status;
|
|
456
|
-
requestedReason = reason;
|
|
457
|
-
if (errorMessage) state.errorMessage = errorMessage;
|
|
454
|
+
const requestTermination = (status: Exclude<SubagentProcessStatus, "running" | "complete">, reason: string, errorMessage?: string): void => {
|
|
455
|
+
if (requestedStatus || closed) return;
|
|
456
|
+
requestedStatus = status;
|
|
457
|
+
requestedReason = reason;
|
|
458
|
+
if (errorMessage) state.errorMessage = errorMessage;
|
|
459
|
+
state.status = status;
|
|
460
|
+
state.terminationReason = reason;
|
|
461
|
+
publish();
|
|
458
462
|
if (!child) {
|
|
459
463
|
finish(null);
|
|
460
464
|
return;
|
|
@@ -485,8 +489,8 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
485
489
|
state.usage.turns += 1;
|
|
486
490
|
addUsage(state.usage, { ...message.usage, turns: 0 });
|
|
487
491
|
state.toolCallCount += toolCallCount(message);
|
|
488
|
-
if (limits.quotaTokens !== undefined && state.usage.totalTokens
|
|
489
|
-
requestTermination("limited", "quota_tokens", `Child token usage
|
|
492
|
+
if (limits.quotaTokens !== undefined && state.usage.totalTokens >= limits.quotaTokens) {
|
|
493
|
+
requestTermination("limited", "quota_tokens", `Child token usage reaches ${limits.quotaTokens}`);
|
|
490
494
|
} else if (limits.quotaUsd !== undefined && state.usage.cost.total > limits.quotaUsd) {
|
|
491
495
|
requestTermination("limited", "quota_cost", `Child cost exceeds $${limits.quotaUsd}`);
|
|
492
496
|
}
|
|
@@ -515,8 +519,11 @@ export function runSubagentProcess(options: SubagentProcessOptions): SubagentPro
|
|
|
515
519
|
hasUsableAssistantResponse = true;
|
|
516
520
|
}
|
|
517
521
|
}
|
|
518
|
-
if (typeof message.errorMessage === "string") state.errorMessage = message.errorMessage;
|
|
519
|
-
|
|
522
|
+
if (typeof message.errorMessage === "string") state.errorMessage = message.errorMessage;
|
|
523
|
+
if (message.stopReason === "toolUse" && limits.maxTurns !== undefined && state.usage.turns >= limits.maxTurns) {
|
|
524
|
+
requestTermination("limited", "turn_limit", `Child turn count reached ${limits.maxTurns}`);
|
|
525
|
+
}
|
|
526
|
+
publish();
|
|
520
527
|
} else if (event?.type === "tool_result_end" && event.message) {
|
|
521
528
|
const name = typeof event.message.toolName === "string" ? event.message.toolName : "tool";
|
|
522
529
|
const traceTruncatedBefore = state.traceTruncatedBytes;
|
package/killeros/subagent-ui.ts
CHANGED
|
@@ -131,7 +131,8 @@ function toRecord(item: ThreadListItem): ThreadRecord {
|
|
|
131
131
|
function threadStatus(thread: Pick<ThreadRecord, "status" | "terminationReason">): ThreadStatus {
|
|
132
132
|
const reason = thread.terminationReason;
|
|
133
133
|
if (thread.status === "orphaned") return "orphaned";
|
|
134
|
-
if (thread.status === "
|
|
134
|
+
if (thread.status === "queued" || thread.status === "running") return thread.status;
|
|
135
|
+
if (thread.status === "failed" || reason === "error" || reason === "model_error" || reason === "spawn_error" || reason === "process_closed" || reason === "missing_assistant_message" || reason === "malformed_jsonl" || reason === "invalid_usage" || reason?.startsWith("exit_")) return "failed";
|
|
135
136
|
if (thread.status === "cancelled" || reason === "abort" || reason === "interrupt") return "cancelled";
|
|
136
137
|
if (thread.status === "complete" || reason === "completed") return "complete";
|
|
137
138
|
return thread.status;
|
|
@@ -194,6 +195,7 @@ export function formatThreadControls(status: ThreadStatus): readonly ThreadBoard
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
export function formatThreadInspection(thread: ThreadRecord): ThreadInspectionView {
|
|
198
|
+
const state = formatThreadState(thread);
|
|
197
199
|
return {
|
|
198
200
|
id: thread.id,
|
|
199
201
|
displayName: thread.displayName,
|
|
@@ -201,11 +203,11 @@ export function formatThreadInspection(thread: ThreadRecord): ThreadInspectionVi
|
|
|
201
203
|
agent: thread.agent,
|
|
202
204
|
task: thread.task,
|
|
203
205
|
step: thread.step,
|
|
204
|
-
state
|
|
206
|
+
state,
|
|
205
207
|
usage: formatThreadUsage(thread.usage),
|
|
206
208
|
trace: formatThreadTrace(thread),
|
|
207
209
|
handoff: formatThreadHandoff(thread),
|
|
208
|
-
controls: formatThreadControls(
|
|
210
|
+
controls: formatThreadControls(state.status),
|
|
209
211
|
};
|
|
210
212
|
}
|
|
211
213
|
|
|
@@ -231,8 +233,8 @@ export function formatThreadBoard(input: ThreadBoardInput): ParentThreadBoard {
|
|
|
231
233
|
.filter((thread) => !closed.has(thread.id) && !current.has(thread.id))
|
|
232
234
|
.map(toRecord) ?? [];
|
|
233
235
|
const threads = [...current.values(), ...retained];
|
|
234
|
-
const active = threads.filter((thread) => !isDone(thread
|
|
235
|
-
const done = threads.filter((thread) => isDone(thread
|
|
236
|
+
const active = threads.filter((thread) => !isDone(threadStatus(thread))).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
|
|
237
|
+
const done = threads.filter((thread) => isDone(threadStatus(thread))).map((thread) => formatThreadListItem(thread, input.selectedThreadId));
|
|
236
238
|
const selectedThread = input.selectedThreadId ? threads.find((thread) => thread.id === input.selectedThreadId) : undefined;
|
|
237
239
|
return {
|
|
238
240
|
title: input.title ?? "Threads",
|
package/killeros/subagents.ts
CHANGED
|
@@ -29,9 +29,11 @@ export const SUBAGENT_LIMITS = {
|
|
|
29
29
|
threadRetentionRecords: 64,
|
|
30
30
|
threadRetentionBytes: 128 * 1024 * 1024,
|
|
31
31
|
roleFileBytes: 64 * 1024,
|
|
32
|
-
taskCharacters: 20_000,
|
|
33
|
-
killGraceMs: 5_000,
|
|
34
|
-
|
|
32
|
+
taskCharacters: 20_000,
|
|
33
|
+
killGraceMs: 5_000,
|
|
34
|
+
defaultMaxTurns: 64,
|
|
35
|
+
defaultQuotaTokens: 2_000_000,
|
|
36
|
+
defaultWallTimeMs: 1_800_000,
|
|
35
37
|
processExitWaitMs: 10_000,
|
|
36
38
|
} as const;
|
|
37
39
|
|
|
@@ -136,8 +138,9 @@ export interface SubagentDetails {
|
|
|
136
138
|
threads?: SubagentThread[];
|
|
137
139
|
activeThreads?: SubagentThread[];
|
|
138
140
|
doneThreads?: SubagentThread[];
|
|
139
|
-
selectedThreadId?: string;
|
|
140
|
-
wait?: SubagentWaitSummary;
|
|
141
|
+
selectedThreadId?: string;
|
|
142
|
+
wait?: SubagentWaitSummary;
|
|
143
|
+
backgroundStarted?: boolean;
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
export interface SubagentWaitSummary {
|
|
@@ -192,9 +195,10 @@ interface SpawnedProcess {
|
|
|
192
195
|
once(event: "close", listener: (code: number | null) => void): this;
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
196
|
-
wallTimeMs?: number;
|
|
197
|
-
|
|
198
|
+
type SubagentLimits = { [Key in keyof typeof SUBAGENT_LIMITS]: number } & {
|
|
199
|
+
wallTimeMs?: number;
|
|
200
|
+
maxTurns?: number;
|
|
201
|
+
jsonlLineBytes?: number;
|
|
198
202
|
traceBytes?: number;
|
|
199
203
|
stderrBytes?: number;
|
|
200
204
|
taskOutputBytes?: number;
|
|
@@ -723,9 +727,10 @@ async function runTask(options: RunTaskOptions): Promise<SubagentTaskResult> {
|
|
|
723
727
|
cwd: options.cwd,
|
|
724
728
|
signal: options.signal,
|
|
725
729
|
spawnProcess: options.spawnProcess,
|
|
726
|
-
limits: {
|
|
727
|
-
...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
|
|
728
|
-
...(limits.
|
|
730
|
+
limits: {
|
|
731
|
+
...(options.timeoutMs === undefined ? {} : { wallTimeMs: options.timeoutMs }),
|
|
732
|
+
...(limits.maxTurns === undefined ? {} : { maxTurns: limits.maxTurns }),
|
|
733
|
+
...(limits.jsonlLineBytes === undefined ? {} : { jsonlLineBytes: limits.jsonlLineBytes }),
|
|
729
734
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes }),
|
|
730
735
|
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes }),
|
|
731
736
|
...(limits.taskOutputBytes === undefined ? {} : { outputBytes: limits.taskOutputBytes }),
|
|
@@ -1166,7 +1171,7 @@ function createSubagentParams(limits: Pick<SubagentLimits, "maxTasks" | "maxRead
|
|
|
1166
1171
|
action: Type.Optional(StringEnum(SUBAGENT_ACTIONS, { default: SUBAGENT_ACTION.spawn, description: "Spawn, list, inspect, steer, interrupt, collect, wait, resume, or close. Omit threadId when spawning" })),
|
|
1167
1172
|
threadId: Type.Optional(threadId),
|
|
1168
1173
|
name: Type.Optional(Type.String({ minLength: 1, maxLength: 48, pattern: THREAD_NAME_PATTERN, description: "Parent-scoped child display name" })),
|
|
1169
|
-
message: Type.Optional(Type.String({ minLength: 1, maxLength: Math.max(4_000, limits.taskCharacters), description:
|
|
1174
|
+
message: Type.Optional(Type.String({ minLength: 1, maxLength: Math.max(4_000, limits.taskCharacters), description: `Spawn task alias up to ${limits.taskCharacters.toLocaleString("en-US")} characters; steer message up to 4,000 characters` })),
|
|
1170
1175
|
all: Type.Optional(Type.Literal(true, { description: "Target every active or queued child thread for interrupt or wait" })),
|
|
1171
1176
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_WAIT_TIMEOUT_MS, description: "Wait timeout in milliseconds; defaults to 30 seconds" })),
|
|
1172
1177
|
agent: Type.Optional(agentSchema),
|
|
@@ -2292,26 +2297,18 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2292
2297
|
const rawInputs: TaskInput[] = spawnRequest.kind === "spawn-single"
|
|
2293
2298
|
? [{ agent: spawnRequest.input.agent, task: spawnRequest.input.task, ...(spawnRequest.input.name ? { name: spawnRequest.input.name } : {}) }]
|
|
2294
2299
|
: spawnRequest.kind === "spawn-parallel" ? spawnRequest.input.tasks : spawnRequest.input.chain;
|
|
2295
|
-
const
|
|
2296
|
-
const rolesForInputs = rawInputs.map((input) => {
|
|
2300
|
+
const rolesForInputs = rawInputs.map((input) => {
|
|
2297
2301
|
if (typeof input.agent !== "string") {
|
|
2298
2302
|
const parentTools = new Set(pi.getActiveTools());
|
|
2299
2303
|
for (const tool of input.agent.tools) {
|
|
2300
2304
|
if (!parentTools.has(tool)) throw new Error(`Inline role ${JSON.stringify(input.agent.name)} tool ${JSON.stringify(tool)} is not active for the parent`);
|
|
2301
2305
|
}
|
|
2302
2306
|
return inlineAgentRole(input.agent);
|
|
2303
|
-
}
|
|
2307
|
+
}
|
|
2304
2308
|
const selected = roles.get(input.agent);
|
|
2305
2309
|
if (selected) return selected;
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
throw new Error(`Unknown subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2309
|
-
}
|
|
2310
|
-
const worker = roles.get("worker");
|
|
2311
|
-
if (!worker) throw new Error(`Unknown subagent ${JSON.stringify(input.agent)} and fallback role "worker" is unavailable`);
|
|
2312
|
-
const notice = `Unknown role ${JSON.stringify(input.agent)} - used worker`;
|
|
2313
|
-
if (!fallbackNotices.includes(notice)) fallbackNotices.push(notice);
|
|
2314
|
-
return worker;
|
|
2310
|
+
const available = discovery.agents.map((agent) => `${agent.name} (${agent.source})`).join(", ") || "none";
|
|
2311
|
+
throw new Error(`Unknown subagent ${JSON.stringify(input.agent)}. Available: ${available}`);
|
|
2315
2312
|
});
|
|
2316
2313
|
const inputs: Array<Omit<TaskInput, "agent"> & { agent: string }> = rawInputs.map((input, index) => ({
|
|
2317
2314
|
...input,
|
|
@@ -2351,7 +2348,7 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2351
2348
|
? `Parallel schedule: all tasks run through a shared pool of up to ${writerConcurrency}${writerConcurrencyOverride === undefined ? " (safe default)" : " (explicit)"}. Write-capable tasks are serialized in the shared parent worktree.`
|
|
2352
2349
|
: `Parallel schedule: read-only tasks run concurrently up to ${limits.maxReadConcurrency}.`
|
|
2353
2350
|
: undefined;
|
|
2354
|
-
const executionNote =
|
|
2351
|
+
const executionNote = scheduleNote;
|
|
2355
2352
|
|
|
2356
2353
|
const inFlight = threads.listAll().filter((thread) => ["queued", "active"].includes(thread.state)).length;
|
|
2357
2354
|
if (inFlight + inputs.length > limits.maxTasks) {
|
|
@@ -2600,9 +2597,12 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2600
2597
|
const remainingWallTimeMs = wallTimeMs === undefined ? undefined : wallTimeMs - (Date.now() - runtime.startedAt);
|
|
2601
2598
|
const usedTraceBytes = (aggregate?.traceBytes ?? 0) + (aggregate?.traceTruncatedBytes ?? 0);
|
|
2602
2599
|
const usedStderrBytes = aggregate?.stderrBytes ?? 0;
|
|
2603
|
-
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
2604
|
-
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
2605
|
-
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
2600
|
+
const usedOutputBytes = aggregate?.outputBytes ?? 0;
|
|
2601
|
+
const usedTokens = aggregate?.usage.totalTokens ?? 0;
|
|
2602
|
+
const usedCost = aggregate?.usage.cost.total ?? 0;
|
|
2603
|
+
const usedTurns = aggregate?.usage.turns ?? 0;
|
|
2604
|
+
const maxTurns = limits.maxTurns ?? limits.defaultMaxTurns;
|
|
2605
|
+
const quotaTokens = limits.quotaTokens ?? limits.defaultQuotaTokens;
|
|
2606
2606
|
if (remainingWallTimeMs !== undefined && remainingWallTimeMs <= 0) {
|
|
2607
2607
|
stopForBudget("wall_time_limit", `Child thread exceeds ${wallTimeMs} ms`);
|
|
2608
2608
|
break;
|
|
@@ -2619,10 +2619,14 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2619
2619
|
stopForBudget("output_limit", `Child thread emits more than ${limits.taskOutputBytes} output bytes`);
|
|
2620
2620
|
break;
|
|
2621
2621
|
}
|
|
2622
|
-
if (
|
|
2623
|
-
stopForBudget("
|
|
2624
|
-
break;
|
|
2625
|
-
}
|
|
2622
|
+
if (usedTurns >= maxTurns) {
|
|
2623
|
+
stopForBudget("turn_limit", `Child thread reaches ${maxTurns} turns`);
|
|
2624
|
+
break;
|
|
2625
|
+
}
|
|
2626
|
+
if (usedTokens >= quotaTokens) {
|
|
2627
|
+
stopForBudget("quota_tokens", `Child thread reaches ${quotaTokens} tokens`);
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2626
2630
|
if (limits.quotaUsd !== undefined && usedCost >= limits.quotaUsd) {
|
|
2627
2631
|
stopForBudget("quota_cost", `Child thread exceeds $${limits.quotaUsd}`);
|
|
2628
2632
|
break;
|
|
@@ -2646,10 +2650,11 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2646
2650
|
limits: {
|
|
2647
2651
|
...limits,
|
|
2648
2652
|
...(limits.traceBytes === undefined ? {} : { traceBytes: limits.traceBytes - usedTraceBytes }),
|
|
2649
|
-
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
|
|
2650
|
-
...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
+
...(limits.stderrBytes === undefined ? {} : { stderrBytes: limits.stderrBytes - usedStderrBytes }),
|
|
2654
|
+
...(limits.taskOutputBytes === undefined ? {} : { taskOutputBytes: limits.taskOutputBytes - usedOutputBytes }),
|
|
2655
|
+
maxTurns: maxTurns - usedTurns,
|
|
2656
|
+
quotaTokens: quotaTokens - usedTokens,
|
|
2657
|
+
...(limits.quotaUsd === undefined ? {} : { quotaUsd: limits.quotaUsd - usedCost }),
|
|
2653
2658
|
},
|
|
2654
2659
|
timeoutMs: remainingWallTimeMs,
|
|
2655
2660
|
onHandle: (handle) => {
|
|
@@ -2884,9 +2889,10 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2884
2889
|
|
|
2885
2890
|
const queuedBoard = detailsFor(parentId, mode, scope, discovery.projectAgentsDir, isResume ? resumeTarget?.id : undefined);
|
|
2886
2891
|
const queuedResults = results.map(cloneResult);
|
|
2887
|
-
const queuedDetails: SubagentDetails = {
|
|
2888
|
-
...queuedBoard,
|
|
2889
|
-
|
|
2892
|
+
const queuedDetails: SubagentDetails = {
|
|
2893
|
+
...queuedBoard,
|
|
2894
|
+
backgroundStarted: true,
|
|
2895
|
+
executionNote,
|
|
2890
2896
|
results: queuedResults,
|
|
2891
2897
|
aggregateUsage: aggregateUsage(queuedResults),
|
|
2892
2898
|
};
|
|
@@ -2968,11 +2974,20 @@ export function registerSubagentTool(pi: ExtensionAPI, options: SubagentRuntimeO
|
|
|
2968
2974
|
|
|
2969
2975
|
renderResult(result, { expanded }, theme) {
|
|
2970
2976
|
const details = result.details as SubagentDetails | undefined;
|
|
2971
|
-
if (!details?.results.length) {
|
|
2972
|
-
const first = result.content[0];
|
|
2973
|
-
return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2977
|
+
if (!details?.results.length) {
|
|
2978
|
+
const first = result.content[0];
|
|
2979
|
+
return new Text(first?.type === "text" ? first.text : "(no output)", 0, 0);
|
|
2980
|
+
}
|
|
2981
|
+
if (details.backgroundStarted) {
|
|
2982
|
+
const lines = [
|
|
2983
|
+
theme.fg("toolTitle", theme.bold(`Started in background (${details.results.length})`)),
|
|
2984
|
+
...details.results.map((task) => `${theme.fg("toolTitle", theme.bold(task.name ?? task.agent))}${theme.fg("dim", ` · role ${task.agent} · ${task.id} · attempt ${task.attempt ?? 1}`)}`),
|
|
2985
|
+
];
|
|
2986
|
+
if (details.executionNote) lines.push(theme.fg("dim", details.executionNote));
|
|
2987
|
+
lines.push(theme.fg("dim", "Live status appears below while child threads run."));
|
|
2988
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
2989
|
+
}
|
|
2990
|
+
const board = formatThreadBoard({
|
|
2976
2991
|
title: `Subagents · ${details.mode}`,
|
|
2977
2992
|
threads: details.results.map(threadBoardRecord),
|
|
2978
2993
|
selectedThreadId: details.selectedThreadId,
|