killeros 1.5.3 → 1.5.4
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 +15 -0
- package/Killeros.ts +30 -4
- package/README.md +37 -15
- package/killeros/commands.ts +238 -1
- package/killeros/context-compaction.ts +566 -0
- package/killeros/goals.ts +25 -2
- package/killeros/runtime.ts +25 -0
- package/killeros/subagent-lifecycle.ts +237 -8
- package/killeros/subagent-persistence.ts +572 -0
- package/killeros/subagent-process.ts +583 -565
- package/killeros/subagent-ui.ts +17 -1
- package/killeros/subagents.ts +2570 -1665
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to KillerOS are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.5.4] - 2026-08-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added named child sessions with `wait` and `resume`, persisted lifecycle records, real `/subagents` controls, empty-response failure, a 30-minute default wall time, bounded process-exit cleanup, and serialized shared-worktree writers.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Added guarded automatic context compaction at 30% remaining, with structured summaries and goal continuation after the compaction is saved.
|
|
16
|
+
- Kept the parent-facing cancellation reason when a steer was already in flight: aborting the parent turn after a steer now reports `abort` on the settled thread and result and no longer triggers a replacement follow-up turn for the cancelled batch.
|
|
17
|
+
- Isolated host update callbacks in child-process and tool telemetry paths so a throwing callback cannot crash the host, strand the result promise, or fail a settled batch.
|
|
18
|
+
- Rejected steering explicitly once 20 messages are pending for a thread instead of silently dropping the oldest steers; task-size overflow is also rejected before mutation, and bounded steering history keeps the earliest messages.
|
|
19
|
+
- `interrupt all` now also stops queued children of the batch, matching `interrupt` on one thread and parent-turn abort, so queued writers cannot run after a stop command.
|
|
20
|
+
- Recreated the subagent thread registry on `session_start`, stopped old children, and fenced old callbacks so embedding hosts that keep the extension instance between sessions can still spawn children without stale follow-ups.
|
|
21
|
+
|
|
7
22
|
## [1.5.3] - 2026-08-03
|
|
8
23
|
|
|
9
24
|
### Fixed
|
package/Killeros.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { registerSubagentTool } from "./killeros/subagents.ts";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createSubagentControlApi,
|
|
5
|
+
registerAliases,
|
|
6
|
+
registerSlashAutocomplete,
|
|
7
|
+
registerSubagentCommand,
|
|
8
|
+
type SubagentControlApi,
|
|
9
|
+
type SubagentToolLike,
|
|
10
|
+
} from "./killeros/commands.ts";
|
|
4
11
|
import { registerConcisePrompt } from "./killeros/concise.ts";
|
|
12
|
+
import { registerContextCompaction } from "./killeros/context-compaction.ts";
|
|
5
13
|
import { registerFooter } from "./killeros/footer.ts";
|
|
6
14
|
import { registerGoal, registerGoalSettlement } from "./killeros/goals.ts";
|
|
7
15
|
import { registerLifecycleHooks } from "./killeros/hooks.ts";
|
|
8
16
|
import { registerInitCommand, registerInitSettlement } from "./killeros/init.ts";
|
|
9
17
|
import { registerPersonalInstructions } from "./killeros/personal-instructions.ts";
|
|
10
18
|
import { registerQuestionTool } from "./killeros/question.ts";
|
|
11
|
-
import { createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
19
|
+
import { createCompactionRuntime, createGoalRuntime, createInitRuntime } from "./killeros/runtime.ts";
|
|
12
20
|
import { registerShellUi } from "./killeros/shell-ui.ts";
|
|
13
21
|
import { registerVariants } from "./killeros/variants.ts";
|
|
14
22
|
|
|
15
23
|
export { CONCISE_SYSTEM_PROMPT, isConcisedEnabled } from "./killeros/concise.ts";
|
|
24
|
+
export { contextPercentRemaining } from "./killeros/context-compaction.ts";
|
|
16
25
|
export { formatCost, formatContextProgress } from "./killeros/footer.ts";
|
|
17
26
|
export { executeHook } from "./killeros/hooks.ts";
|
|
18
27
|
export { INIT_WORKFLOW_PROMPT, writeInitAgentsFile } from "./killeros/init.ts";
|
|
@@ -20,18 +29,35 @@ export { INIT_WORKFLOW_PROMPT, writeInitAgentsFile } from "./killeros/init.ts";
|
|
|
20
29
|
export default function Killeros(pi: ExtensionAPI): void {
|
|
21
30
|
const initRuntime = createInitRuntime();
|
|
22
31
|
const goalRuntime = createGoalRuntime();
|
|
32
|
+
const compactionRuntime = createCompactionRuntime();
|
|
23
33
|
registerShellUi(pi);
|
|
24
34
|
registerConcisePrompt(pi);
|
|
25
35
|
registerGoal(pi, goalRuntime, initRuntime);
|
|
26
36
|
registerPersonalInstructions(pi, initRuntime);
|
|
27
37
|
registerQuestionTool(pi);
|
|
28
|
-
|
|
38
|
+
let subagentTool: SubagentToolLike | undefined;
|
|
39
|
+
const registrationPi = new Proxy(pi, {
|
|
40
|
+
get(target, property, receiver) {
|
|
41
|
+
if (property === "registerTool") {
|
|
42
|
+
return (tool: Parameters<ExtensionAPI["registerTool"]>[0]) => {
|
|
43
|
+
if (tool.name === "subagent") subagentTool = tool as unknown as SubagentToolLike;
|
|
44
|
+
return target.registerTool(tool);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return Reflect.get(target, property, receiver);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const subagents = registerSubagentTool(registrationPi);
|
|
51
|
+
const subagentControl = (subagents as unknown as SubagentControlApi | undefined)
|
|
52
|
+
?? (subagentTool ? createSubagentControlApi(subagentTool) : undefined);
|
|
53
|
+
registerSubagentCommand(pi, subagentControl);
|
|
29
54
|
registerAliases(pi);
|
|
30
55
|
registerSlashAutocomplete(pi);
|
|
31
56
|
registerFooter(pi, goalRuntime);
|
|
32
57
|
registerVariants(pi);
|
|
33
58
|
registerInitCommand(pi, initRuntime, goalRuntime);
|
|
34
59
|
registerLifecycleHooks(pi);
|
|
35
|
-
|
|
60
|
+
registerContextCompaction(pi, compactionRuntime, goalRuntime);
|
|
61
|
+
registerGoalSettlement(pi, goalRuntime, initRuntime, compactionRuntime);
|
|
36
62
|
registerInitSettlement(pi, initRuntime);
|
|
37
63
|
}
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pi install git:github.com/KyrosHendrix/pi-KillerOS
|
|
|
33
33
|
Pin an install to a release:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.5.
|
|
36
|
+
pi install git:github.com/KyrosHendrix/pi-KillerOS@v1.5.4
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Add `-l` to either command for a project-only install. Restart Pi after installing.
|
|
@@ -45,6 +45,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
45
45
|
- Coral Spark activity indicator with Claude-adjacent verbs that advance between agent runs and a quiet hidden-thinking label
|
|
46
46
|
- Framed multiline editor with Shift+Enter support
|
|
47
47
|
- Responsive footer with polished model/provider identity, plain-language context, and active goal state remaining; reasoning, Git branch, elapsed time, cost, and path cut down by available width
|
|
48
|
+
- Automatic non-destructive context compaction at 30% remaining with a structured handoff; active goals continue after the saved summary
|
|
48
49
|
- `/variants` selector and direct reasoning-level arguments
|
|
49
50
|
- Codex-style `/goal` for durable long-running objectives with pause, resume, edit, clear, automatic continuation, and explicit completion
|
|
50
51
|
- Pi-native `subagent` tool with named, inspectable child threads, Markdown roles, explicit read/write boundaries, parent controls, natural completion, and cancellation propagation
|
|
@@ -66,6 +67,7 @@ Add `-l` to either command for a project-only install. Restart Pi after installi
|
|
|
66
67
|
/goal clear Remove the current goal
|
|
67
68
|
/variants Open the reasoning-level selector
|
|
68
69
|
/variants high Set a reasoning level directly
|
|
70
|
+
/subagents Open child-thread selectors in TUI mode
|
|
69
71
|
/clear Start a new session after confirmation
|
|
70
72
|
/exit Quit Pi gracefully
|
|
71
73
|
```
|
|
@@ -97,48 +99,66 @@ KillerOS ships `planner`, `reviewer`, `scout`, and `security` as read-only roles
|
|
|
97
99
|
|
|
98
100
|
The default `agentScope: "user"` uses bundled and personal roles. Use `"project"` or `"both"` to opt into trusted project roles; a selected project override requires interactive confirmation. Role frontmatter requires `name`, `description`, `access`, and an explicit `tools` list. Optional fields are `model`, `thinking`, and `timeoutMs`. Every bundled role shows `model: inherit` and `thinking: inherit` as editable placeholders. Replace them with an available `provider/model` and a separate thinking level when you want to pin a role; `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, and `max` are checked against that model’s supported capabilities.
|
|
99
101
|
|
|
100
|
-
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. Read-only-only batches run concurrently, up to four at a time. Batches with write-capable roles
|
|
102
|
+
The tool supports a single `agent` + `task`, parallel `tasks`, or a sequential `chain` whose task text may include `{previous}`. 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. A call can also set `model` and `thinking` for every task, overriding role settings; use `inherit` to fall back to each role and then the active parent model.
|
|
101
103
|
|
|
102
104
|
| Action | Required fields | Allowed optional fields |
|
|
103
105
|
|---|---|---|
|
|
104
|
-
| omitted / `spawn` single | `agent`, `task` | `model`, `thinking`, `agentScope` |
|
|
105
|
-
| omitted / `spawn` parallel | `tasks` | `writerConcurrency`, `model`, `thinking`, `agentScope` |
|
|
106
|
-
| omitted / `spawn` chain | `chain` | `model`, `thinking`, `agentScope` |
|
|
106
|
+
| omitted / `spawn` single | `agent`, `task` | `name`, `model`, `thinking`, `agentScope` |
|
|
107
|
+
| omitted / `spawn` parallel | `tasks` | per-task `name`, `writerConcurrency`, `model`, `thinking`, `agentScope` |
|
|
108
|
+
| omitted / `spawn` chain | `chain` | per-task `name`, `model`, `thinking`, `agentScope` |
|
|
107
109
|
| `list` | none | none |
|
|
108
110
|
| `inspect` | `threadId` | none |
|
|
111
|
+
| `wait` | none | `threadId`, `all: true`, `timeoutMs` |
|
|
109
112
|
| `steer` | `threadId`, `message` | none |
|
|
110
113
|
| `interrupt` one | `threadId` | none |
|
|
111
114
|
| `interrupt` all | `all: true` | none |
|
|
112
115
|
| `collect` | `threadId` | none |
|
|
116
|
+
| `resume` | `threadId` | `task` |
|
|
113
117
|
| `close` | `threadId` | none |
|
|
114
118
|
|
|
115
|
-
The three spawn shapes cannot be mixed. The `message` field is only valid with `action: "steer"`, and lifecycle actions reject spawn fields. 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:
|
|
119
|
+
The three spawn shapes cannot be mixed. The `message` field is only valid with `action: "steer"`, and lifecycle actions reject spawn fields. 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`. 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:
|
|
116
120
|
|
|
117
121
|
```json
|
|
118
|
-
{"agent":"reviewer","task":"Review the change","model":"provider/model","thinking":"high"}
|
|
122
|
+
{"agent":"reviewer","task":"Review the change","name":"auth-audit","model":"provider/model","thinking":"high"}
|
|
119
123
|
```
|
|
120
124
|
|
|
121
|
-
Spawn returns the generated thread IDs immediately while the children continue in the background. This lets the parent use `list`, `inspect`, `steer`, `interrupt`, `collect`, and `close` in later tool calls. 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. 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.
|
|
122
126
|
|
|
123
|
-
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.
|
|
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. The default child wall time is 30 minutes; token and 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. An embedding caller may opt into named child 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
|
+
|
|
129
|
+
The command grammar is:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
/subagents
|
|
133
|
+
/subagents list
|
|
134
|
+
/subagents inspect <id-or-name>
|
|
135
|
+
/subagents wait [<id-or-name>] [timeout-ms]
|
|
136
|
+
/subagents steer <id-or-name> <message>
|
|
137
|
+
/subagents interrupt <id-or-name|all>
|
|
138
|
+
/subagents collect <id-or-name>
|
|
139
|
+
/subagents resume <id-or-name> [task]
|
|
140
|
+
/subagents close <id-or-name>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Bare `/subagents` opens TUI selectors. RPC, JSON, and print modes require an explicit verb and never open a UI prompt.
|
|
124
144
|
|
|
125
145
|
### Thread lifecycle
|
|
126
146
|
|
|
127
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. Roles define the child’s access and tools; they do not own lifecycle controls or grant new filesystem powers. The parent owns scope, waits, inspection, steering, collection, and closure.
|
|
128
148
|
|
|
129
|
-
Threads move through `queued`, `active`, `done`, `failed`, `stopped`, 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.
|
|
149
|
+
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.
|
|
130
150
|
|
|
131
|
-
The parent can inspect a thread’s prompt, role, model, tools, trace, usage, and handoff; steer an active thread with
|
|
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.
|
|
132
152
|
|
|
133
|
-
A child completes
|
|
153
|
+
A child completes only when it returns usable final assistant text. The default wall time is 30 minutes; token and dollar quotas remain opt-in, while every JSONL record has an 8 MiB parser ceiling. Explicit embedding options can add output, trace, stderr, JSONL, token, or cost guards; those guards report their cause and return 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.
|
|
134
154
|
|
|
135
155
|
The replacement lifecycle has nine phases:
|
|
136
156
|
|
|
137
157
|
1. **Dispatch:** create a named thread and store its contract before launch.
|
|
138
158
|
2. **Track:** maintain lifecycle states and Active/Done visibility.
|
|
139
159
|
3. **Inspect:** keep the trace in the child thread, not the parent context.
|
|
140
|
-
4. **Steer:** append a bounded parent follow-up to an active thread.
|
|
141
|
-
5. **Interrupt:** stop one or all active children while preserving partial work.
|
|
160
|
+
4. **Steer:** append a bounded parent follow-up to an active or queued thread.
|
|
161
|
+
5. **Interrupt:** stop one or all active or queued children while preserving partial work.
|
|
142
162
|
6. **Collect:** return a concise handoff while retaining the expanded trace.
|
|
143
163
|
7. **Guard:** honor only explicitly configured child resource guards; do not impose a routine turn stop.
|
|
144
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.
|
|
@@ -150,6 +170,8 @@ KillerOS activates its packaged `killeros` theme when a TUI session starts. Tool
|
|
|
150
170
|
|
|
151
171
|
KillerOS displays session costs in USD. The footer uses Pi's human-readable model name when available, keeps the provider visually secondary, and renders context as `percent left (tokens)` without a progress bar. When a goal exists, the footer adds its active time or terminal state; at narrow widths, context pressure and goal state take priority.
|
|
152
172
|
|
|
173
|
+
KillerOS checks context after each agent turn. At 30% remaining, it starts Pi compaction after the current run settles, so the active turn is not aborted. Manual `/compact` remains available, and the compaction summary keeps the goal, progress, decisions, next steps, and changed files.
|
|
174
|
+
|
|
153
175
|
For trusted projects, KillerOS loads `AGENTS.local.md` after Pi's shared repository context. A one-line `@path` or `@~/path` file imports personal guidance from another location.
|
|
154
176
|
|
|
155
177
|
Lifecycle hooks are loaded from `.pi/killeros-hooks.json` at session start. Supported event keys are `tool_call`, `tool_result`, and `agent_settled`; matchers are JavaScript regular expressions over Pi tool names. Hook commands run from the repository root with `KILLEROS_EVENT`, `KILLEROS_TOOL`, and `KILLEROS_PAYLOAD` environment variables. Failed `tool_call` hooks block the tool, while later-event failures notify the user.
|
|
@@ -180,7 +202,7 @@ The package manifest lists Pi’s built-in modules as peer dependencies, so npm
|
|
|
180
202
|
|
|
181
203
|
The [`pi-package`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md) keyword makes a published npm release visible in Pi’s package catalog.
|
|
182
204
|
|
|
183
|
-
For release
|
|
205
|
+
For a release, publish after the validation checks pass:
|
|
184
206
|
|
|
185
207
|
```bash
|
|
186
208
|
npm login
|
package/killeros/commands.ts
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { formatThreadControls, type ThreadStatus } from "./subagent-ui.ts";
|
|
4
|
+
|
|
5
|
+
export type SubagentControlAction = "list" | "inspect" | "wait" | "steer" | "interrupt" | "collect" | "resume" | "close";
|
|
6
|
+
|
|
7
|
+
export interface SubagentControlRequest {
|
|
8
|
+
action: SubagentControlAction;
|
|
9
|
+
threadId?: string;
|
|
10
|
+
all?: true;
|
|
11
|
+
message?: string;
|
|
12
|
+
task?: string;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SubagentControlThread {
|
|
17
|
+
id: string;
|
|
18
|
+
displayName?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
agent?: string;
|
|
21
|
+
role?: string;
|
|
22
|
+
task?: string;
|
|
23
|
+
prompt?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
state?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SubagentControlDetails {
|
|
29
|
+
results?: readonly SubagentControlThread[];
|
|
30
|
+
threads?: readonly SubagentControlThread[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SubagentControlResult {
|
|
34
|
+
text: string;
|
|
35
|
+
details?: SubagentControlDetails;
|
|
36
|
+
usage?: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SubagentControlApi {
|
|
40
|
+
execute(request: SubagentControlRequest, ctx: ExtensionContext): Promise<SubagentControlResult>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SubagentToolLike {
|
|
44
|
+
name: string;
|
|
45
|
+
execute(
|
|
46
|
+
toolCallId: string,
|
|
47
|
+
params: unknown,
|
|
48
|
+
signal: AbortSignal | undefined,
|
|
49
|
+
onUpdate: undefined,
|
|
50
|
+
ctx: ExtensionContext,
|
|
51
|
+
): Promise<{
|
|
52
|
+
content?: readonly { type: string; text?: string }[];
|
|
53
|
+
details?: unknown;
|
|
54
|
+
usage?: unknown;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createSubagentControlApi(tool: SubagentToolLike): SubagentControlApi {
|
|
59
|
+
return {
|
|
60
|
+
async execute(request, ctx) {
|
|
61
|
+
const toolRequest = request.action === "interrupt" && request.threadId === "all"
|
|
62
|
+
? { action: "interrupt", all: true }
|
|
63
|
+
: request;
|
|
64
|
+
const result = await tool.execute("subagents-command", toolRequest, ctx.signal, undefined, ctx);
|
|
65
|
+
const text = result.content?.find((item) => item.type === "text")?.text ?? "";
|
|
66
|
+
return {
|
|
67
|
+
text,
|
|
68
|
+
details: result.details as SubagentControlDetails | undefined,
|
|
69
|
+
usage: result.usage,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
3
74
|
|
|
4
75
|
async function confirmNewSession(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
5
76
|
if (!ctx.hasUI) return true;
|
|
@@ -57,6 +128,7 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
|
|
|
57
128
|
const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
|
|
58
129
|
goal: "/goal [objective|clear|edit|pause|resume]",
|
|
59
130
|
variants: "/variants [level]",
|
|
131
|
+
subagents: "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]",
|
|
60
132
|
model: "/model [provider/model]",
|
|
61
133
|
"scoped-models": "/scoped-models",
|
|
62
134
|
login: "/login [provider]",
|
|
@@ -79,6 +151,162 @@ function scoreCommandMatch(name: string, prefix: string): number {
|
|
|
79
151
|
return 0;
|
|
80
152
|
}
|
|
81
153
|
|
|
154
|
+
const SUBAGENT_COMMAND_USAGE = "/subagents [list|inspect|wait|steer|interrupt|collect|resume|close] [thread]";
|
|
155
|
+
|
|
156
|
+
function subagentCommandError(message: string): Error {
|
|
157
|
+
return new Error(`${message} Usage: ${SUBAGENT_COMMAND_USAGE}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseThreadReference(action: SubagentControlAction, tail: string): string {
|
|
161
|
+
const reference = tail.match(/^(\S+)(?:\s+([\s\S]*))?$/u)?.[1];
|
|
162
|
+
if (!reference) throw subagentCommandError(`/subagents ${action} requires a thread reference.`);
|
|
163
|
+
return reference;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseExplicitSubagentCommand(args: string): SubagentControlRequest {
|
|
167
|
+
const trimmed = args.trim();
|
|
168
|
+
const match = trimmed.match(/^(\S+)(?:\s+([\s\S]*))?$/u);
|
|
169
|
+
if (!match) throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
170
|
+
const action = match[1]!.toLocaleLowerCase() as SubagentControlAction;
|
|
171
|
+
const tail = match[2]?.trim() ?? "";
|
|
172
|
+
|
|
173
|
+
if (action === "list") {
|
|
174
|
+
if (tail) throw subagentCommandError("/subagents list does not accept arguments.");
|
|
175
|
+
return { action };
|
|
176
|
+
}
|
|
177
|
+
if (action === "wait") {
|
|
178
|
+
if (!tail) return { action };
|
|
179
|
+
const parts = tail.split(/\s+/u);
|
|
180
|
+
if (parts.length > 2) throw subagentCommandError("/subagents wait accepts one thread reference and one timeout-ms value.");
|
|
181
|
+
if (parts.length === 1 && /^\d+$/u.test(parts[0]!)) {
|
|
182
|
+
return { action, timeoutMs: parseTimeout(parts[0]!) };
|
|
183
|
+
}
|
|
184
|
+
const request: SubagentControlRequest = { action, threadId: parts[0] };
|
|
185
|
+
if (parts[1] !== undefined) request.timeoutMs = parseTimeout(parts[1]);
|
|
186
|
+
return request;
|
|
187
|
+
}
|
|
188
|
+
if (action === "steer") {
|
|
189
|
+
const referenceAndMessage = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
190
|
+
if (!referenceAndMessage?.[1]) throw subagentCommandError("/subagents steer requires a thread reference.");
|
|
191
|
+
if (!referenceAndMessage[2]?.trim()) throw subagentCommandError("/subagents steer requires a message.");
|
|
192
|
+
return { action, threadId: referenceAndMessage[1], message: referenceAndMessage[2] };
|
|
193
|
+
}
|
|
194
|
+
if (action === "resume") {
|
|
195
|
+
const referenceAndTask = tail.match(/^(\S+)(?:\s+([\s\S]+))?$/u);
|
|
196
|
+
if (!referenceAndTask?.[1]) throw subagentCommandError("/subagents resume requires a thread reference.");
|
|
197
|
+
return {
|
|
198
|
+
action,
|
|
199
|
+
threadId: referenceAndTask[1],
|
|
200
|
+
...(referenceAndTask[2] ? { task: referenceAndTask[2] } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (action === "inspect" || action === "interrupt" || action === "collect" || action === "close") {
|
|
204
|
+
const threadId = parseThreadReference(action, tail);
|
|
205
|
+
if (tail.slice(threadId.length).trim()) throw subagentCommandError(`/subagents ${action} accepts one thread reference.`);
|
|
206
|
+
return { action, threadId };
|
|
207
|
+
}
|
|
208
|
+
throw subagentCommandError(`Unknown /subagents action ${JSON.stringify(match[1])}.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseTimeout(value: string): number {
|
|
212
|
+
const timeoutMs = Number(value);
|
|
213
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) {
|
|
214
|
+
throw subagentCommandError("/subagents wait timeout-ms must be a non-negative integer.");
|
|
215
|
+
}
|
|
216
|
+
return timeoutMs;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function controlThreads(result: SubagentControlResult): SubagentControlThread[] {
|
|
220
|
+
const details = result.details;
|
|
221
|
+
if (!details) return [];
|
|
222
|
+
const results = [...(details.results ?? [])];
|
|
223
|
+
const threads = [...(details.threads ?? [])];
|
|
224
|
+
const candidates = results.length ? results : threads;
|
|
225
|
+
return candidates.filter((thread) => thread && typeof thread.id === "string" && thread.state !== "closed" && thread.status !== "closed");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function threadStatus(thread: SubagentControlThread): ThreadStatus {
|
|
229
|
+
const status = (thread.status ?? thread.state ?? "queued").toLocaleLowerCase();
|
|
230
|
+
if (status === "active" || status === "running") return "running";
|
|
231
|
+
if (status === "done" || status === "complete" || status === "closed") return "complete";
|
|
232
|
+
if (status === "stopped" || status === "cancelled") return "cancelled";
|
|
233
|
+
if (status === "limited") return "limited";
|
|
234
|
+
if (status === "orphaned") return "orphaned";
|
|
235
|
+
if (status === "failed") return "failed";
|
|
236
|
+
return "queued";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function threadLabel(thread: SubagentControlThread): string {
|
|
240
|
+
const name = thread.displayName ?? thread.name ?? thread.agent ?? thread.role ?? thread.id;
|
|
241
|
+
return `${name} · ${thread.id} · ${threadStatus(thread)}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function selectedThread(threads: readonly SubagentControlThread[], labels: readonly string[], choice: string): SubagentControlThread | undefined {
|
|
245
|
+
const index = labels.indexOf(choice);
|
|
246
|
+
if (index >= 0) return threads[index];
|
|
247
|
+
return threads.find((thread) => thread.id === choice || thread.displayName === choice || thread.name === choice);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function executeSubagentControl(
|
|
251
|
+
control: SubagentControlApi | undefined,
|
|
252
|
+
request: SubagentControlRequest,
|
|
253
|
+
ctx: ExtensionCommandContext,
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
if (!control) throw new Error("Subagent control API is not available.");
|
|
256
|
+
const result = await control.execute(request, ctx);
|
|
257
|
+
if (result?.text) ctx.ui.notify(result.text, "info");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function runTuiSubagentCommand(control: SubagentControlApi | undefined, ctx: ExtensionCommandContext): Promise<void> {
|
|
261
|
+
if (!control) throw new Error("Subagent control API is not available.");
|
|
262
|
+
const listed = await control.execute({ action: "list" }, ctx);
|
|
263
|
+
const threads = controlThreads(listed);
|
|
264
|
+
if (!threads.length) {
|
|
265
|
+
ctx.ui.notify("No child threads.", "info");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const labels = threads.map(threadLabel);
|
|
270
|
+
const selected = await ctx.ui.select("Select a thread", labels);
|
|
271
|
+
if (selected === undefined) return;
|
|
272
|
+
const thread = selectedThread(threads, labels, selected);
|
|
273
|
+
if (!thread) return;
|
|
274
|
+
|
|
275
|
+
const controls = formatThreadControls(threadStatus(thread)).filter((item) => item.enabled);
|
|
276
|
+
const controlLabels = controls.map((item) => item.label);
|
|
277
|
+
const selectedControl = await ctx.ui.select("Select a control", controlLabels);
|
|
278
|
+
if (selectedControl === undefined) return;
|
|
279
|
+
const chosen = controls.find((item) => item.label === selectedControl || item.id === selectedControl);
|
|
280
|
+
if (!chosen) return;
|
|
281
|
+
|
|
282
|
+
const request: SubagentControlRequest = { action: chosen.id, threadId: thread.id };
|
|
283
|
+
if (chosen.id === "steer") {
|
|
284
|
+
const message = await ctx.ui.input("Steer child thread", "Message");
|
|
285
|
+
if (message === undefined || !message.trim()) return;
|
|
286
|
+
request.message = message;
|
|
287
|
+
} else if (chosen.id === "resume") {
|
|
288
|
+
const task = await ctx.ui.input("Resume child thread", "Optional task");
|
|
289
|
+
if (task === undefined) return;
|
|
290
|
+
if (task) request.task = task;
|
|
291
|
+
}
|
|
292
|
+
await executeSubagentControl(control, request, ctx);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function registerSubagentCommand(pi: ExtensionAPI, control?: SubagentControlApi | void): void {
|
|
296
|
+
const api = control && typeof control.execute === "function" ? control : undefined;
|
|
297
|
+
pi.registerCommand("subagents", {
|
|
298
|
+
description: "Inspect and control child threads",
|
|
299
|
+
handler: async (args, ctx) => {
|
|
300
|
+
if (!args.trim()) {
|
|
301
|
+
if (ctx.mode !== "tui") throw subagentCommandError("/subagents requires an explicit verb outside TUI.");
|
|
302
|
+
await runTuiSubagentCommand(api, ctx);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
await executeSubagentControl(api, parseExplicitSubagentCommand(args), ctx);
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
82
310
|
export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
83
311
|
const usage = new Map<string, number>();
|
|
84
312
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -121,6 +349,15 @@ export function registerSlashAutocomplete(pi: ExtensionAPI): void {
|
|
|
121
349
|
}
|
|
122
350
|
}
|
|
123
351
|
|
|
352
|
+
if (!commands.has("subagents")) {
|
|
353
|
+
commands.set("subagents", {
|
|
354
|
+
name: "subagents",
|
|
355
|
+
description: "Inspect and control child threads",
|
|
356
|
+
category: "Extension",
|
|
357
|
+
syntaxHint: COMMAND_SYNTAX_HINTS.subagents,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
124
361
|
const ranked = [...commands.values()]
|
|
125
362
|
.map((command) => ({
|
|
126
363
|
command,
|