killeros 1.5.6 → 1.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +16 -27
- package/killeros/goals.ts +9 -8
- package/killeros/question.ts +19 -4
- package/killeros/subagent-process.ts +20 -13
- package/killeros/subagent-ui.ts +7 -5
- package/killeros/subagents.ts +541 -309
- package/package.json +2 -3
- package/agents/debugger.md +0 -50
- package/agents/documenter.md +0 -49
- package/agents/planner.md +0 -53
- package/agents/reviewer.md +0 -58
- package/agents/scout.md +0 -56
- package/agents/security.md +0 -54
- package/agents/tester.md +0 -50
- package/agents/worker.md +0 -54
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.5.8] - 2026-08-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Removed KillerOS's eight bundled subagent roles. The main agent now chooses each child; omitting `agent` creates a generic read-only child, while optional custom roles come from approved personal or trusted project agent folders or an inline definition.
|
|
12
|
+
- Persisted the selected child role contract for resume and rechecked its tools against the main agent's current authority.
|
|
13
|
+
- Made model and thinking selection inherit the selected role or active parent by default; an explicit `model` value can pin a whole batch to one available model while omitted thinking still inherits.
|
|
14
|
+
- Preserved bounded role contracts for child threads written by KillerOS 1.5.7, so saved read and write roles can resume after the bundled role files are removed.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Treated whitespace-only child output as missing lifecycle output, failed the thread cleanly, and freed the task slot while retaining the raw child result.
|
|
19
|
+
- Removed persistent child-session directories when retained terminal threads aged out after confirmed child exit, while keeping unconfirmed sessions recoverable until exit was confirmed.
|
|
20
|
+
- Bounded question filter input to 4,000 characters and 16,000 UTF-8 bytes, with a clear rejection notice for excess input.
|
|
21
|
+
- Reported goal start, resume, and edit success only after continuation dispatch succeeded, and paused the goal when dispatch failed.
|
|
22
|
+
- Rechecked project trust and approval when resuming a persisted project role.
|
|
23
|
+
- Removed terminal role metadata when closed threads leave bounded retention.
|
|
24
|
+
|
|
25
|
+
## [1.5.7] - 2026-08-06
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- 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.
|
|
30
|
+
- 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.
|
|
31
|
+
- Replaced the frozen queued spawn result with a static launch receipt so only the separate live widget claims current state and usage.
|
|
32
|
+
|
|
7
33
|
## [1.5.6] - 2026-08-05
|
|
8
34
|
|
|
9
35
|
### Fixed
|
package/README.md
CHANGED
|
@@ -80,32 +80,20 @@ Supported reasoning levels are `off`, `minimal`, `low`, `medium`, `high`, `xhigh
|
|
|
80
80
|
|
|
81
81
|
## Subagents
|
|
82
82
|
|
|
83
|
-
KillerOS ships
|
|
83
|
+
KillerOS ships no subagent roles. The main agent chooses each child. Omit `agent` for a generic read-only child, use a custom role name from an approved agents folder, or provide an inline `{name, description, access, tools}` role when the child needs a custom contract.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
| `debugger` | write | Reproduce failures, fix root causes, and verify regressions |
|
|
88
|
-
| `documenter` | write | Keep repository documentation accurate and audience-focused |
|
|
89
|
-
| `planner` | read | Turn repository constraints into an executable implementation route |
|
|
90
|
-
| `reviewer` | read | Report proven correctness, security, and regression risks |
|
|
91
|
-
| `scout` | read | Map unfamiliar code and return an evidence trail |
|
|
92
|
-
| `security` | read | Audit trust boundaries and report concrete security findings |
|
|
93
|
-
| `tester` | write | Add focused coverage and run deterministic verification |
|
|
94
|
-
| `worker` | write | Execute the assigned repository change |
|
|
95
|
-
|
|
96
|
-
1. Bundled: `<killeros>/agents/*.md`
|
|
97
|
-
2. Personal: `~/.pi/agent/agents/*.md`
|
|
98
|
-
3. Trusted project: `<repo>/.pi/agents/*.md`
|
|
85
|
+
1. Personal: `~/.pi/agent/agents/*.md`
|
|
86
|
+
2. Trusted project: `<repo>/.pi/agents/*.md`
|
|
99
87
|
|
|
100
|
-
The default `agentScope: "user"` uses
|
|
88
|
+
The default `agentScope: "user"` uses personal roles. Use `"project"` or `"both"` to opt into trusted project roles; a selected project role requires interactive confirmation. Role frontmatter requires `name`, `description`, `access`, and an explicit `tools` list. The Markdown body is the child prompt. Optional fields are `model`, `thinking`, and `timeoutMs`; without a role or dispatch setting, model and thinking inherit the active parent. KillerOS does not load package-local role files.
|
|
101
89
|
|
|
102
|
-
The tool supports a single
|
|
90
|
+
The tool supports a single task, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Omit `agent` for a generic read-only child, pass a custom role name from an approved agents folder, or provide an inline `{name, description, access, tools}` role. Inline and file role tools must be active for the parent. Each task may set a `name`; names are unique within the parent session without regard to case and are passed to child Pi as `--name`. Read-only-only batches run concurrently, up to four at a time. Batches with write-capable roles are serialized in the shared worktree with one shared slot. Reader-only batches reject `writerConcurrency` because it does not apply. Model and thinking default to inherit the selected role setting or active parent. Set `model` to pin every task in the call to one available model, or set `thinking` when the user asks for a specific effort; either field accepts `inherit`. Selected role contracts persist with the child thread and are checked against the current parent tool set when resumed.
|
|
103
91
|
|
|
104
92
|
| Action | Required fields | Allowed optional fields |
|
|
105
93
|
|---|---|---|
|
|
106
|
-
| omitted / `spawn` single |
|
|
107
|
-
| omitted / `spawn` parallel | `tasks` | per-task `name`, `writerConcurrency`, `model`, `thinking`, `agentScope` |
|
|
108
|
-
| omitted / `spawn` chain | `chain` | per-task `name`, `model`, `thinking`, `agentScope` |
|
|
94
|
+
| omitted / `spawn` single | one of `task` or `message` | `agent`, `name`, `model`, `thinking`, `agentScope` |
|
|
95
|
+
| omitted / `spawn` parallel | `tasks` | per-task `agent`, per-task `name`, `writerConcurrency`, `model`, `thinking`, `agentScope` |
|
|
96
|
+
| omitted / `spawn` chain | `chain` | per-task `agent`, per-task `name`, `model`, `thinking`, `agentScope` |
|
|
109
97
|
| `list` | none | none |
|
|
110
98
|
| `inspect` | `threadId` | none |
|
|
111
99
|
| `wait` | none | `threadId`, `all: true`, `timeoutMs` |
|
|
@@ -116,15 +104,16 @@ The tool supports a single `agent` plus `task` or its `message` alias, parallel
|
|
|
116
104
|
| `resume` | `threadId` | `task` |
|
|
117
105
|
| `close` | `threadId` | none |
|
|
118
106
|
|
|
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
|
|
107
|
+
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 custom role fails before child launch. 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 restores the saved role contract and checks its tools against the current parent authority. 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
108
|
|
|
121
109
|
```json
|
|
122
|
-
{"
|
|
110
|
+
{"task":"Inspect the change","name":"repo-scan"}
|
|
111
|
+
{"agent":{"name":"auth-audit","description":"Review the change","access":"read","tools":["read","grep","find"]},"task":"Review the change","model":"provider/model","thinking":"high"}
|
|
123
112
|
```
|
|
124
113
|
|
|
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.
|
|
114
|
+
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
115
|
|
|
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.
|
|
116
|
+
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. A generic child uses read-only tools. A selected custom or inline role uses its saved prompt, access, and tools. 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
117
|
|
|
129
118
|
The command grammar is:
|
|
130
119
|
|
|
@@ -144,13 +133,13 @@ Bare `/subagents` opens TUI selectors. RPC, JSON, and print modes require an exp
|
|
|
144
133
|
|
|
145
134
|
### Thread lifecycle
|
|
146
135
|
|
|
147
|
-
Each delegated task creates a named child thread. Its contract records the parent ID, child ID, role, prompt, model, requested capability boundary, trace, usage, and result state.
|
|
136
|
+
Each delegated task creates a named child thread. Its contract records the parent ID, child ID, selected generic or custom role, prompt, model, requested capability boundary, trace, usage, and result state. The selected role defines the child’s access and tools; it does not own lifecycle controls or grant new filesystem powers. The parent owns scope, waits, inspection, steering, collection, and closure.
|
|
148
137
|
|
|
149
138
|
Threads move through `queued`, `active`, `done`, `failed`, `stopped`, `orphaned`, and `closed`. The parent renders separate **Active** and **Done** lists. Active threads show their name, task, model, usage, and direct controls. Done threads keep their handoff and trace available until the parent closes them.
|
|
150
139
|
|
|
151
140
|
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
141
|
|
|
153
|
-
A child completes only when it returns usable final assistant text. The default
|
|
142
|
+
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
143
|
|
|
155
144
|
The replacement lifecycle has nine phases:
|
|
156
145
|
|
|
@@ -160,7 +149,7 @@ The replacement lifecycle has nine phases:
|
|
|
160
149
|
4. **Steer:** append a bounded parent follow-up to an active or queued thread.
|
|
161
150
|
5. **Interrupt:** stop one or all active or queued children while preserving partial work.
|
|
162
151
|
6. **Collect:** return a concise handoff while retaining the expanded trace.
|
|
163
|
-
7. **Guard:**
|
|
152
|
+
7. **Guard:** stop at the default turn, token, or wall-time limit and honor explicit resource-limit overrides.
|
|
164
153
|
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
154
|
9. **Prove:** test identity, visibility, controls, natural completion, guards, partial handoffs, bounded retention, and closure.
|
|
166
155
|
|
package/killeros/goals.ts
CHANGED
|
@@ -209,7 +209,7 @@ function scheduleGoalContinuation(
|
|
|
209
209
|
runtime: GoalRuntime,
|
|
210
210
|
initState: InitRuntime,
|
|
211
211
|
ctx: ExtensionContext,
|
|
212
|
-
):
|
|
212
|
+
): boolean {
|
|
213
213
|
if (!isGoalModeSupported(ctx)
|
|
214
214
|
|| !isSavedSession(ctx)
|
|
215
215
|
|| runtime.state?.status !== "active"
|
|
@@ -217,7 +217,7 @@ function scheduleGoalContinuation(
|
|
|
217
217
|
|| runtime.continuationHeld
|
|
218
218
|
|| runtime.goalTurnInFlight
|
|
219
219
|
|| initState.active
|
|
220
|
-
|| ctx.hasPendingMessages()) return;
|
|
220
|
+
|| ctx.hasPendingMessages()) return false;
|
|
221
221
|
const current = runtime.state;
|
|
222
222
|
runtime.continuationScheduled = true;
|
|
223
223
|
runtime.goalTurnInFlight = false;
|
|
@@ -230,10 +230,12 @@ function scheduleGoalContinuation(
|
|
|
230
230
|
content: goalContinuationMessage(current, ctx),
|
|
231
231
|
display: false,
|
|
232
232
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
233
|
+
return true;
|
|
233
234
|
} catch (error) {
|
|
234
235
|
runtime.continuationScheduled = false;
|
|
235
236
|
runtime.goalTurnInFlight = false;
|
|
236
237
|
pauseGoalAfterFailure(pi, runtime, ctx, `continuation could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
238
|
+
return false;
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -551,8 +553,7 @@ export function registerGoal(
|
|
|
551
553
|
try {
|
|
552
554
|
transitionGoal(pi, runtime, "resume", "active", undefined, true);
|
|
553
555
|
runtime.continuationScheduled = false;
|
|
554
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
555
|
-
ctx.ui.notify("Goal resumed", "info");
|
|
556
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal resumed", "info");
|
|
556
557
|
} catch (error) {
|
|
557
558
|
reportError(ctx, "Goal could not be resumed", error);
|
|
558
559
|
}
|
|
@@ -612,8 +613,7 @@ export function registerGoal(
|
|
|
612
613
|
try {
|
|
613
614
|
persistGoalState(pi, runtime, "edit", next);
|
|
614
615
|
runtime.continuationScheduled = false;
|
|
615
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
|
616
|
-
ctx.ui.notify("Goal updated and active", "info");
|
|
616
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) ctx.ui.notify("Goal updated and active", "info");
|
|
617
617
|
} catch (error) {
|
|
618
618
|
pauseGoalAfterFailure(
|
|
619
619
|
pi,
|
|
@@ -677,8 +677,9 @@ export function registerGoal(
|
|
|
677
677
|
};
|
|
678
678
|
try {
|
|
679
679
|
persistGoalState(pi, runtime, unfinished ? "replace" : "set", state);
|
|
680
|
-
scheduleGoalContinuation(pi, runtime, initState, ctx)
|
|
681
|
-
|
|
680
|
+
if (scheduleGoalContinuation(pi, runtime, initState, ctx)) {
|
|
681
|
+
ctx.ui.notify("Goal active. KillerOS will continue until completion, a repeated blocker, or pause.", "info");
|
|
682
|
+
}
|
|
682
683
|
} catch (error) {
|
|
683
684
|
reportError(ctx, "Goal could not be started", error);
|
|
684
685
|
scheduleGoalContinuation(pi, runtime, initState, ctx);
|
package/killeros/question.ts
CHANGED
|
@@ -56,6 +56,8 @@ type QuestionSelection =
|
|
|
56
56
|
const CUSTOM_INPUT_MAX_CHARACTERS = 4_000;
|
|
57
57
|
const CUSTOM_INPUT_HISTORY_LIMIT = 100;
|
|
58
58
|
const CUSTOM_INPUT_HISTORY_BYTES = 64 * 1024;
|
|
59
|
+
const FILTER_QUERY_MAX_CHARACTERS = 4_000;
|
|
60
|
+
const FILTER_QUERY_MAX_BYTES = 16_000;
|
|
59
61
|
|
|
60
62
|
function isPrintableInput(data: string): boolean {
|
|
61
63
|
return data.length > 0 && !/[\u0000-\u001F\u007F-\u009F]/u.test(data);
|
|
@@ -123,7 +125,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
123
125
|
pi.registerTool<typeof QuestionParams, QuestionDetails>({
|
|
124
126
|
name: "question",
|
|
125
127
|
label: "Question",
|
|
126
|
-
description:
|
|
128
|
+
description: `Ask one interactive multiple-choice question. Provide 1-9 concise options. The user can filter options or type a custom answer. Filter queries are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes.`,
|
|
127
129
|
promptSnippet: "Ask the user one multiple-choice question when a decision is required to proceed",
|
|
128
130
|
promptGuidelines: [
|
|
129
131
|
"Use question only when user input is required to choose between concrete alternatives; do not use question for rhetorical or optional follow-up prompts.",
|
|
@@ -198,6 +200,21 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
198
200
|
tui.requestRender();
|
|
199
201
|
};
|
|
200
202
|
|
|
203
|
+
const appendFilterInput = (value: string): void => {
|
|
204
|
+
const nextCharacters = inputCharacterCount(filterQuery) + inputCharacterCount(value);
|
|
205
|
+
const nextBytes = Buffer.byteLength(filterQuery, "utf8") + Buffer.byteLength(value, "utf8");
|
|
206
|
+
if (nextCharacters > FILTER_QUERY_MAX_CHARACTERS || nextBytes > FILTER_QUERY_MAX_BYTES) {
|
|
207
|
+
ctx.ui.notify(
|
|
208
|
+
`Question filters are limited to ${FILTER_QUERY_MAX_CHARACTERS.toLocaleString()} characters and ${FILTER_QUERY_MAX_BYTES.toLocaleString()} bytes`,
|
|
209
|
+
"error",
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
filterQuery += value;
|
|
214
|
+
optionIndex = 0;
|
|
215
|
+
refresh();
|
|
216
|
+
};
|
|
217
|
+
|
|
201
218
|
editor.onSubmit = (value) => {
|
|
202
219
|
const answer = value.trim();
|
|
203
220
|
if (answer) {
|
|
@@ -288,9 +305,7 @@ export function registerQuestionTool(pi: ExtensionAPI): void {
|
|
|
288
305
|
return;
|
|
289
306
|
}
|
|
290
307
|
if (printableInput) {
|
|
291
|
-
|
|
292
|
-
optionIndex = 0;
|
|
293
|
-
refresh();
|
|
308
|
+
appendFilterInput(printableInput);
|
|
294
309
|
}
|
|
295
310
|
};
|
|
296
311
|
|
|
@@ -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",
|