@ryan_nookpi/pi-extension-subagent 0.3.2 → 0.3.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/README.md +96 -139
- package/commands.ts +29 -0
- package/mentions.ts +177 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,55 +1,21 @@
|
|
|
1
1
|
# subagent
|
|
2
2
|
|
|
3
|
-
Asynchronous subagent delegation for [pi](https://github.com/earendil-works/pi). Run specialist agents in dedicated child sessions,
|
|
4
|
-
|
|
5
|
-
The primary interface is **CLI-style**: one `subagent` tool accepts a command string with verbs, options, and a `--` task separator, such as `subagent run worker --isolated -- review this change`. This provides one consistent grammar for single runs, continuation, parallel batches, sequential chains, inspection, and cleanup. These strings are tool input, not shell commands.
|
|
3
|
+
Asynchronous subagent delegation for [pi](https://github.com/earendil-works/pi). Run specialist agents in dedicated child sessions, share main-session context when needed, and receive results as follow-up messages.
|
|
6
4
|
|
|
7
5
|
> [!WARNING]
|
|
8
|
-
> Subagents run headlessly without approval prompts. Claude-runtime agents
|
|
9
|
-
|
|
10
|
-
## Requirements
|
|
11
|
-
|
|
12
|
-
- pi 0.80.6 or later (tested with 0.80.6)
|
|
13
|
-
- For `runtime: claude` with the default `claudeRuntime: "sdk"`: supported Anthropic authentication such as `ANTHROPIC_API_KEY`; see the [official Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview)
|
|
14
|
-
- For `runtime: claude` with `claudeRuntime: "cli"`: the `claude` executable on `PATH` and an authenticated Claude Code installation
|
|
6
|
+
> Subagents run headlessly without approval prompts. Claude-runtime agents bypass permissions, and pi-runtime agents can use every tool declared by their agent definition. This extension is not a sandbox. Use trusted repositories, prompts, and agent definitions only.
|
|
15
7
|
|
|
16
8
|
## Install
|
|
17
9
|
|
|
10
|
+
Requires pi 0.80.6 or later. Compatibility is tested with pi 0.80.7.
|
|
11
|
+
|
|
18
12
|
```bash
|
|
19
13
|
pi install npm:@ryan_nookpi/pi-extension-subagent
|
|
20
14
|
```
|
|
21
15
|
|
|
22
16
|
## Quick start
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Run this from the interactive pi UI:
|
|
27
|
-
|
|
28
|
-
```text
|
|
29
|
-
/subagents
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
If no agent definitions exist in any discovery location, the extension offers an optional starter pack containing:
|
|
33
|
-
|
|
34
|
-
- Nine portable English agents: `browser`, `challenger`, `code-cleaner`, `reviewer`, `searcher`, `security-auditor`, `simplifier`, `verifier`, and `worker`
|
|
35
|
-
- The `stress-interview` and `self-healing` skills, written in English and validated against the [Agent Skills specification](https://agentskills.io/specification)
|
|
36
|
-
- Missing global `subagent` settings: `defaultAgent: "worker"`, `claudeRuntime: "cli"`, and symbol mappings for searcher, challenger, and browser
|
|
37
|
-
|
|
38
|
-
Seeded agents intentionally omit model IDs and inherit the user's Pi model. Existing files and configured setting values are never overwritten. If the offer is declined, nothing is recorded or written, so the extension asks again the next time the list is still empty.
|
|
39
|
-
|
|
40
|
-
Agents and subagent settings are available immediately after installation. Run `/reload` or start a new Pi session to activate the two newly copied skills. Headless sessions never install automatically; they return instructions to run `/subagents` interactively.
|
|
41
|
-
|
|
42
|
-
The same offer is available from either agent-list tool:
|
|
43
|
-
|
|
44
|
-
```json
|
|
45
|
-
{ "command": "subagent agents" }
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The separate `list-agents` tool behaves the same way. The `subagent ...` examples in this README are **tool command strings**, not terminal commands. Do not run them in Bash.
|
|
49
|
-
|
|
50
|
-
### 2. Or create an agent manually
|
|
51
|
-
|
|
52
|
-
Agents are Markdown files with YAML frontmatter. Create `~/.pi/agent/agents/worker.md` for a global agent, or `.pi/agents/worker.md` inside one project:
|
|
18
|
+
Create a global agent at `~/.pi/agent/agents/worker.md`, or a project agent at `.pi/agents/worker.md`:
|
|
53
19
|
|
|
54
20
|
```markdown
|
|
55
21
|
---
|
|
@@ -63,18 +29,7 @@ runtime: pi
|
|
|
63
29
|
Implement the requested changes and verify them.
|
|
64
30
|
```
|
|
65
31
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
- `runtime`: `pi` (default) or `claude`
|
|
69
|
-
- `model`: runtime-compatible model ID
|
|
70
|
-
- `thinking`: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`
|
|
71
|
-
- `tools`: comma-separated tool names
|
|
72
|
-
|
|
73
|
-
Omitted model, thinking, and tools values use that runtime's defaults.
|
|
74
|
-
|
|
75
|
-
### 3. Launch a run
|
|
76
|
-
|
|
77
|
-
Interactive user command:
|
|
32
|
+
Then launch it from pi:
|
|
78
33
|
|
|
79
34
|
```text
|
|
80
35
|
/sub:isolate worker implement the requested change and run tests
|
|
@@ -86,36 +41,54 @@ Equivalent AI tool call:
|
|
|
86
41
|
{ "command": "subagent run worker --isolated -- implement the requested change and run tests" }
|
|
87
42
|
```
|
|
88
43
|
|
|
89
|
-
|
|
44
|
+
Tool launches are asynchronous. Wait for the automatic completion or failure follow-up instead of polling immediately.
|
|
45
|
+
|
|
46
|
+
## Agent definitions
|
|
47
|
+
|
|
48
|
+
Agent files use YAML frontmatter followed by the system prompt. `name` and `description` are required.
|
|
90
49
|
|
|
91
|
-
|
|
50
|
+
| Field | Description |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `runtime` | `pi` (default) or `claude` |
|
|
53
|
+
| `model` | Runtime-compatible model ID |
|
|
54
|
+
| `thinking` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
|
|
55
|
+
| `tools` | Comma-separated tool names |
|
|
56
|
+
|
|
57
|
+
Omitted optional fields use runtime defaults.
|
|
92
58
|
|
|
93
|
-
|
|
59
|
+
Agents are discovered in this order; later definitions override earlier agents with the same name:
|
|
94
60
|
|
|
95
61
|
1. `$PI_CODING_AGENT_DIR/agents/*.md` (normally `~/.pi/agent/agents/*.md`)
|
|
96
62
|
2. Nearest `.claude/agents/**/*.md`
|
|
97
63
|
3. Nearest `.pi/agents/*.md`
|
|
98
64
|
|
|
99
|
-
|
|
65
|
+
`.claude/agents` is searched recursively. `.pi/agents` is not.
|
|
66
|
+
|
|
67
|
+
Run `/subagents`, `subagent agents`, or the `list-agents` tool to inspect discovered agents.
|
|
68
|
+
|
|
69
|
+
### Optional starter pack
|
|
100
70
|
|
|
101
|
-
|
|
71
|
+
If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies nine agent templates, two example workflow skills, and missing global `subagent` settings. Existing files and configured values are not overwritten.
|
|
102
72
|
|
|
103
|
-
|
|
104
|
-
- `--main` adds selected main-session context to the child task.
|
|
105
|
-
- `/sub:isolate` selects isolated context; `/sub:main` selects main context.
|
|
106
|
-
- `>>` and `>` use main-session context.
|
|
107
|
-
- Continuing a run preserves its original context mode and child session. Supplying `--main` or `--isolated` to `continue` does not retroactively change it.
|
|
73
|
+
The starter pack is not required. Decline it if you prefer to define agents manually. It fills a missing `claudeRuntime` setting with `cli`; without that setting, the extension default is `sdk`.
|
|
108
74
|
|
|
109
|
-
|
|
75
|
+
## Context modes
|
|
110
76
|
|
|
111
|
-
|
|
77
|
+
- `--isolated` starts without copying the main conversation. It is the default for tool launches.
|
|
78
|
+
- `--main` passes selected main-session context to the child.
|
|
79
|
+
- `/sub:isolate` and `/sub:main` provide the same choice for interactive commands.
|
|
80
|
+
- Continuing a run preserves its original child session and context mode.
|
|
112
81
|
|
|
113
|
-
|
|
82
|
+
Active child processes stop when pi replaces or reloads the parent extension runtime. This follows pi's [official extension lifecycle guidance](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#long-lived-resources-and-shutdown).
|
|
114
83
|
|
|
115
|
-
|
|
84
|
+
## Tool interface
|
|
116
85
|
|
|
117
|
-
|
|
118
|
-
|
|
86
|
+
The extension registers two tools:
|
|
87
|
+
|
|
88
|
+
- `list-agents` — list discovered agents and runtime settings
|
|
89
|
+
- `subagent` — execute a CLI-style command string
|
|
90
|
+
|
|
91
|
+
These strings are tool input, not shell commands. Do not run them in Bash.
|
|
119
92
|
|
|
120
93
|
```text
|
|
121
94
|
subagent help
|
|
@@ -123,15 +96,15 @@ subagent agents
|
|
|
123
96
|
subagent runs
|
|
124
97
|
subagent run <agent> [--main|--isolated] -- <task>
|
|
125
98
|
subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>
|
|
126
|
-
subagent batch [--main|--isolated] --agent <agent> --task <task>
|
|
127
|
-
subagent chain [--main|--isolated] --agent <agent> --task <task>
|
|
99
|
+
subagent batch [--main|--isolated] --agent <agent> --task <task> ...
|
|
100
|
+
subagent chain [--main|--isolated] --agent <agent> --task <task> ...
|
|
128
101
|
subagent status <runId>
|
|
129
102
|
subagent detail <runId>
|
|
130
103
|
subagent abort <runId|runId,runId|all>
|
|
131
104
|
subagent remove <runId|runId,runId|all>
|
|
132
105
|
```
|
|
133
106
|
|
|
134
|
-
`batch` runs independent tasks in parallel
|
|
107
|
+
`batch` runs independent tasks in parallel:
|
|
135
108
|
|
|
136
109
|
```json
|
|
137
110
|
{
|
|
@@ -147,46 +120,51 @@ subagent remove <runId|runId,runId|all>
|
|
|
147
120
|
}
|
|
148
121
|
```
|
|
149
122
|
|
|
150
|
-
Use `status` and `detail`
|
|
123
|
+
Use `status` and `detail` for one-off inspection, not polling loops.
|
|
151
124
|
|
|
152
|
-
##
|
|
125
|
+
## Interactive commands
|
|
153
126
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
127
|
+
| Command | Description |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `/subagents` | List agents and offer the starter pack when none exist |
|
|
130
|
+
| `/sub:main [agent\|runId] <task>` | Launch or continue with main-session context |
|
|
131
|
+
| `/sub:isolate [agent\|runId] <task>` | Launch or continue with isolated context |
|
|
132
|
+
| `/sub:peek [runId]` | Show the latest result |
|
|
133
|
+
| `/sub:open [runId]` | Open the child session replay |
|
|
134
|
+
| `/sub:history` | Show run history, including removed runs |
|
|
135
|
+
| `/sub:abort [runId\|all]` | Abort running work |
|
|
136
|
+
| `/sub:rm [runId]` | Remove a run, aborting it first if necessary |
|
|
137
|
+
| `/sub:clear [all]` | Clear finished runs, or all runs |
|
|
163
138
|
|
|
164
139
|
When an agent is omitted, launch commands use `defaultAgent`.
|
|
165
140
|
|
|
166
|
-
|
|
141
|
+
### Shortcuts
|
|
167
142
|
|
|
168
|
-
| Shortcut |
|
|
143
|
+
| Shortcut | Description |
|
|
169
144
|
| --- | --- |
|
|
170
|
-
| `>> [agent\|runId] <task>` | Visible run
|
|
171
|
-
| `> [agent\|runId] <task>` | Hidden run
|
|
145
|
+
| `>> [agent\|runId] <task>` | Visible run with main-session context |
|
|
146
|
+
| `> [agent\|runId] <task>` | Hidden run with main-session context |
|
|
172
147
|
| `#<runId> <task>` | Continue a run |
|
|
173
|
-
| `>><symbol> <task>`
|
|
174
|
-
|
|
|
175
|
-
|
|
|
176
|
-
|
|
|
177
|
-
| `<<< [all]` | Clear finished runs; use `all` to clear every run |
|
|
148
|
+
| `>><symbol> <task>` / `><symbol> <task>` | Run an agent from `symbolMap` |
|
|
149
|
+
| `<>runId` | Peek at a result |
|
|
150
|
+
| `<< [runId\|runId,runId]` | Abort running or clear finished runs |
|
|
151
|
+
| `<<< [all]` | Clear finished runs, or all runs |
|
|
178
152
|
|
|
179
|
-
Hidden runs do not add start or completion messages to the main transcript.
|
|
153
|
+
Hidden runs are available only in the interactive UI and do not add start or completion messages to the main transcript.
|
|
180
154
|
|
|
181
|
-
|
|
155
|
+
### Prompt mentions
|
|
182
156
|
|
|
183
|
-
|
|
157
|
+
Use `>agent-name` inside a prompt to reference a discovered agent. Exact names are highlighted and rewritten to `subagent:agent-name` before the main LLM receives the prompt; they do not launch a run directly. Unknown names remain unchanged.
|
|
184
158
|
|
|
185
|
-
|
|
159
|
+
```text
|
|
160
|
+
Delegate implementation to >worker and review to >reviewer.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A mention has no space after `>`. Launch shortcuts remain separate, such as `> worker implement this`.
|
|
186
164
|
|
|
187
165
|
## Configuration
|
|
188
166
|
|
|
189
|
-
Global
|
|
167
|
+
Global settings belong under `subagent` in `$PI_CODING_AGENT_DIR/settings.json`:
|
|
190
168
|
|
|
191
169
|
```json
|
|
192
170
|
{
|
|
@@ -212,65 +190,44 @@ A nearest project `.pi/subagent.json` overrides global values:
|
|
|
212
190
|
}
|
|
213
191
|
```
|
|
214
192
|
|
|
215
|
-
- `claudeRuntime`: `sdk` (default) or `cli`; applies only to
|
|
216
|
-
- `defaultAgent`:
|
|
217
|
-
- `symbolMap`: one-character shortcuts mapped to
|
|
193
|
+
- `claudeRuntime`: `sdk` (default) or `cli`; applies only to `runtime: claude`
|
|
194
|
+
- `defaultAgent`: used when an interactive launch omits the agent; defaults to `worker`
|
|
195
|
+
- `symbolMap`: one-character shortcuts mapped to agent names; a valid project map replaces the global map
|
|
218
196
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
`PI_SUBAGENT_CONTEXT_GUARD_TOKENS` overrides the proactive context limit for pi-runtime children. Set it to a positive integer to apply that ceiling to every pi model. Set it to `0` or an empty value to disable the proactive guard and rely on native compaction or provider overflow handling.
|
|
222
|
-
|
|
223
|
-
## Troubleshooting
|
|
197
|
+
Set `PI_SUBAGENT_CONTEXT_GUARD_TOKENS` to a positive integer to override the proactive context limit for pi-runtime children. Set it to `0` or an empty value to disable the guard.
|
|
224
198
|
|
|
225
|
-
|
|
199
|
+
## Claude runtime
|
|
226
200
|
|
|
227
|
-
|
|
201
|
+
The default Claude runtime uses the Claude Agent SDK and requires supported Anthropic authentication such as `ANTHROPIC_API_KEY`; see the [official Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview).
|
|
228
202
|
|
|
229
|
-
|
|
203
|
+
For `claudeRuntime: "cli"`, install the `claude` executable, ensure it is on `PATH`, and authenticate Claude Code.
|
|
230
204
|
|
|
231
|
-
|
|
205
|
+
## Escalation
|
|
232
206
|
|
|
233
|
-
|
|
207
|
+
Pi-runtime children receive an `ask_master` tool. Calling it reports a decision or blocker to the parent and immediately terminates the child run. Use it only when the child cannot proceed safely.
|
|
234
208
|
|
|
235
|
-
|
|
209
|
+
Claude-runtime children do not receive `ask_master`; they report blockers in their final response.
|
|
236
210
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
That is intentional. Hidden `>` runs are human-only UI jobs. Inspect them with `/sub:peek`, `<>runId`, or `/sub:open`.
|
|
240
|
-
|
|
241
|
-
### Debug runner termination
|
|
242
|
-
|
|
243
|
-
The extension records process lifecycle diagnostics as `subagent-runner-diagnostic` custom entries in the parent session JSONL. They include run/batch IDs, parent and child PID/PGID, abort reason, internal kill cause, `exit`/`close` code and signal, settle reason, shutdown reason, and process-error stacks. Custom entries are not sent to the LLM and remain hidden unless an entry renderer is registered, as documented by pi's [`appendEntry` API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#piappendentrycustomtype-data).
|
|
244
|
-
|
|
245
|
-
Run `/session` to find the current session file, set `SESSION_FILE` to that path, then extract only the diagnostic entries:
|
|
246
|
-
|
|
247
|
-
```bash
|
|
248
|
-
jq -c 'select(.type == "custom" and .customType == "subagent-runner-diagnostic") | {timestamp, data}' "$SESSION_FILE"
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
For batch failures, compare entries by `batchId` and `runId`, then follow each run from `spawn` through `kill_intent`, `exit`, `close`, and `settled`. Share only the selected diagnostic lines—not the full session file, which may contain prompts or tool output.
|
|
252
|
-
|
|
253
|
-
To report a reproducible extension bug, create `subagent-issue.md` with the pi and extension versions, OS and Node version, launch mode/command, reproduction steps, expected and actual behavior, and the sanitized diagnostic lines. Then submit it with:
|
|
211
|
+
## Troubleshooting
|
|
254
212
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
```
|
|
213
|
+
- **`Configured defaultAgent "worker" was not found`** — create a matching agent, choose another agent explicitly, or update `defaultAgent`.
|
|
214
|
+
- **Claude SDK authentication failure** — confirm the environment that starts pi contains valid Anthropic authentication.
|
|
215
|
+
- **`spawn claude ENOENT`** — install Claude Code or switch `claudeRuntime` to `sdk`.
|
|
216
|
+
- **Hidden run shows no transcript message** — inspect it with `/sub:peek`, `<>runId`, or `/sub:open`.
|
|
260
217
|
|
|
261
|
-
|
|
218
|
+
When reporting a bug, include the pi and extension versions, OS and Node version, launch command, reproduction steps, and sanitized error output. Do not attach full session files because they may contain prompts, tool output, or secrets.
|
|
262
219
|
|
|
263
|
-
## Security
|
|
220
|
+
## Security
|
|
264
221
|
|
|
265
|
-
- Claude SDK
|
|
266
|
-
- Pi-runtime children
|
|
267
|
-
- Project agent definitions are repository-controlled instructions. Review `.pi/agents` and `.claude/agents`
|
|
268
|
-
- Restrict each agent's `tools` list to
|
|
269
|
-
- `--isolated` separates conversation context
|
|
222
|
+
- Claude SDK uses permission bypass; Claude CLI uses `--dangerously-skip-permissions`.
|
|
223
|
+
- Pi-runtime children can use every tool declared by their agent.
|
|
224
|
+
- Project agent definitions are repository-controlled instructions. Review `.pi/agents` and `.claude/agents` in unfamiliar repositories.
|
|
225
|
+
- Restrict each agent's `tools` list to the minimum required.
|
|
226
|
+
- `--isolated` separates conversation context, not filesystem, process, credential, or network access.
|
|
270
227
|
|
|
271
228
|
## Stability
|
|
272
229
|
|
|
273
|
-
This is
|
|
230
|
+
This package is pre-1.0. Documented commands, configuration, and agent frontmatter are the supported surface; internal TypeScript modules may change between releases.
|
|
274
231
|
|
|
275
232
|
## License
|
|
276
233
|
|
package/commands.ts
CHANGED
|
@@ -42,6 +42,11 @@ import {
|
|
|
42
42
|
upsertPendingGroupCompletion,
|
|
43
43
|
} from "./group-pending.js";
|
|
44
44
|
import { enqueueSubagentInvocation } from "./invocation-queue.js";
|
|
45
|
+
import {
|
|
46
|
+
createAgentMentionAutocompleteProvider,
|
|
47
|
+
registerAgentMentionHighlighting,
|
|
48
|
+
replaceAgentMentions,
|
|
49
|
+
} from "./mentions.js";
|
|
45
50
|
import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
|
|
46
51
|
import { SUBAGENT_COMMANDS, type SubagentCommandName } from "./registration-manifest.js";
|
|
47
52
|
import { readSessionReplayItems, SubagentSessionReplayOverlay } from "./replay.js";
|
|
@@ -904,6 +909,17 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
|
|
|
904
909
|
commandDefinitions.set(name, definition);
|
|
905
910
|
};
|
|
906
911
|
|
|
912
|
+
// Prompt mentions are references for the main LLM, not direct launch shortcuts.
|
|
913
|
+
// Register this transform before the legacy `>` handlers so `>worker` cannot
|
|
914
|
+
// be mistaken for a configured one-character symbol shortcut.
|
|
915
|
+
pi.on("input", (event, ctx) => {
|
|
916
|
+
if (event.source === "extension") return { action: "continue" as const };
|
|
917
|
+
|
|
918
|
+
const transformedText = replaceAgentMentions(event.text ?? "", discoverAgents(ctx.cwd).agents);
|
|
919
|
+
if (transformedText === event.text) return { action: "continue" as const };
|
|
920
|
+
return { action: "transform" as const, text: transformedText, images: event.images };
|
|
921
|
+
});
|
|
922
|
+
|
|
907
923
|
pi.registerTool({
|
|
908
924
|
name: "list-agents",
|
|
909
925
|
label: "List Agents",
|
|
@@ -2300,6 +2316,19 @@ export async function handleBeforeAgentStart(
|
|
|
2300
2316
|
export function handleSessionStart(pi: ExtensionAPI, store: SubagentStore, ctx: ExtensionContext): void {
|
|
2301
2317
|
restoreRunsFromSession(store, ctx, pi);
|
|
2302
2318
|
registerTerminalInputRedirect(ctx);
|
|
2319
|
+
|
|
2320
|
+
let cachedAgents = discoverAgents(ctx.cwd).agents;
|
|
2321
|
+
let discoveredAt = Date.now();
|
|
2322
|
+
const getAgents = () => {
|
|
2323
|
+
if (Date.now() - discoveredAt >= 1_000) {
|
|
2324
|
+
cachedAgents = discoverAgents(ctx.cwd).agents;
|
|
2325
|
+
discoveredAt = Date.now();
|
|
2326
|
+
}
|
|
2327
|
+
return cachedAgents;
|
|
2328
|
+
};
|
|
2329
|
+
|
|
2330
|
+
ctx.ui.addAutocompleteProvider((current) => createAgentMentionAutocompleteProvider(current, getAgents));
|
|
2331
|
+
registerAgentMentionHighlighting(ctx, getAgents);
|
|
2303
2332
|
}
|
|
2304
2333
|
|
|
2305
2334
|
/** session_shutdown handler; index.ts invokes this after shutting down runs. */
|
package/mentions.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { CustomEditor, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type {
|
|
3
|
+
AutocompleteProvider,
|
|
4
|
+
AutocompleteSuggestions,
|
|
5
|
+
EditorComponent,
|
|
6
|
+
EditorTheme,
|
|
7
|
+
TUI,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
import type { AgentConfig } from "./agents.js";
|
|
10
|
+
import { COMMAND_COMPLETION_LIMIT } from "./constants.js";
|
|
11
|
+
|
|
12
|
+
const MENTION_PREFIX_PATTERN = /(?:^|[\s([{])>(?!>)([^\s>]*)$/;
|
|
13
|
+
const MENTION_LEADING_BOUNDARY = "[\\s([{]";
|
|
14
|
+
const MENTION_TRAILING_BOUNDARY = "(?=$|[\\s.,!?;:)}\\]])";
|
|
15
|
+
const MENTION_HIGHLIGHT_WRAPPED = Symbol.for("pi-extension-subagent.mentionHighlightWrapped");
|
|
16
|
+
const ANSI_CYAN = "\x1b[36m";
|
|
17
|
+
const ANSI_RESET_FOREGROUND = "\x1b[39m";
|
|
18
|
+
|
|
19
|
+
type HighlightableEditor = EditorComponent & Record<PropertyKey, unknown>;
|
|
20
|
+
type RichEditorTheme = EditorTheme & {
|
|
21
|
+
fg?: (color: string, text: string) => string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function escapeRegExp(value: string): string {
|
|
25
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildAgentMentionPattern(agents: AgentConfig[]): {
|
|
29
|
+
pattern: RegExp;
|
|
30
|
+
canonicalNames: Map<string, string>;
|
|
31
|
+
} | null {
|
|
32
|
+
const canonicalNames = new Map(agents.map((agent) => [agent.name.toLowerCase(), agent.name]));
|
|
33
|
+
const names = Array.from(canonicalNames.values())
|
|
34
|
+
.filter((name) => name.length > 0 && !/\s/.test(name))
|
|
35
|
+
.sort((left, right) => right.length - left.length)
|
|
36
|
+
.map(escapeRegExp);
|
|
37
|
+
if (names.length === 0) return null;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
pattern: new RegExp(`(^|${MENTION_LEADING_BOUNDARY})>(${names.join("|")})${MENTION_TRAILING_BOUNDARY}`, "gim"),
|
|
41
|
+
canonicalNames,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function styleAgentMention(text: string, theme: EditorTheme): string {
|
|
46
|
+
const richTheme = theme as RichEditorTheme;
|
|
47
|
+
for (const color of ["subagentMention", "syntaxType", "toolTitle"]) {
|
|
48
|
+
try {
|
|
49
|
+
const styled = richTheme.fg?.(color, text);
|
|
50
|
+
if (styled && styled !== text) return styled;
|
|
51
|
+
} catch {
|
|
52
|
+
// Custom theme keys are optional.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return theme.selectList?.selectedText?.(text) ?? `${ANSI_CYAN}${text}${ANSI_RESET_FOREGROUND}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return the incomplete agent name after a mention marker at the cursor. */
|
|
59
|
+
export function extractAgentMentionQuery(textBeforeCursor: string): string | undefined {
|
|
60
|
+
return MENTION_PREFIX_PATTERN.exec(textBeforeCursor)?.[1];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Filter mention candidates by case-insensitive containment, preferring prefix matches. */
|
|
64
|
+
export function filterAgentMentionCandidates(agents: AgentConfig[], query: string): AgentConfig[] {
|
|
65
|
+
const normalizedQuery = query.toLowerCase();
|
|
66
|
+
return agents
|
|
67
|
+
.filter((agent) => agent.name.toLowerCase().includes(normalizedQuery))
|
|
68
|
+
.sort((left, right) => {
|
|
69
|
+
const leftName = left.name.toLowerCase();
|
|
70
|
+
const rightName = right.name.toLowerCase();
|
|
71
|
+
const leftStartsWith = leftName.startsWith(normalizedQuery);
|
|
72
|
+
const rightStartsWith = rightName.startsWith(normalizedQuery);
|
|
73
|
+
if (leftStartsWith !== rightStartsWith) return leftStartsWith ? -1 : 1;
|
|
74
|
+
return leftName.localeCompare(rightName);
|
|
75
|
+
})
|
|
76
|
+
.slice(0, COMMAND_COMPLETION_LIMIT);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Replace exact, discovered `>agent` mentions with the main-LLM-friendly `subagent:agent` form. */
|
|
80
|
+
export function replaceAgentMentions(text: string, agents: AgentConfig[]): string {
|
|
81
|
+
const mentionPattern = buildAgentMentionPattern(agents);
|
|
82
|
+
if (!mentionPattern) return text;
|
|
83
|
+
|
|
84
|
+
return text.replace(mentionPattern.pattern, (_match, boundary: string, name: string) => {
|
|
85
|
+
const canonicalName = mentionPattern.canonicalNames.get(name.toLowerCase()) ?? name;
|
|
86
|
+
return `${boundary}subagent:${canonicalName}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Highlight only exact mentions for agents that are currently discoverable. */
|
|
91
|
+
export function highlightAgentMentions(
|
|
92
|
+
text: string,
|
|
93
|
+
agents: AgentConfig[],
|
|
94
|
+
style: (mention: string) => string,
|
|
95
|
+
): string {
|
|
96
|
+
const mentionPattern = buildAgentMentionPattern(agents);
|
|
97
|
+
if (!mentionPattern) return text;
|
|
98
|
+
|
|
99
|
+
return text.replace(mentionPattern.pattern, (_match, boundary: string, name: string) => {
|
|
100
|
+
return `${boundary}${style(`>${name}`)}`;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function createAgentMentionHighlightEditor(
|
|
105
|
+
baseEditor: EditorComponent,
|
|
106
|
+
getAgents: () => AgentConfig[],
|
|
107
|
+
theme: EditorTheme,
|
|
108
|
+
): EditorComponent {
|
|
109
|
+
const highlightableEditor = baseEditor as HighlightableEditor;
|
|
110
|
+
if (highlightableEditor[MENTION_HIGHLIGHT_WRAPPED]) return baseEditor;
|
|
111
|
+
|
|
112
|
+
return new Proxy(highlightableEditor, {
|
|
113
|
+
get(target, property) {
|
|
114
|
+
if (property === MENTION_HIGHLIGHT_WRAPPED) return true;
|
|
115
|
+
if (property === "render") {
|
|
116
|
+
return (width: number) => {
|
|
117
|
+
const agents = getAgents();
|
|
118
|
+
return target
|
|
119
|
+
.render(width)
|
|
120
|
+
.map((line) => highlightAgentMentions(line, agents, (mention) => styleAgentMention(mention, theme)));
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const value = Reflect.get(target, property, target);
|
|
125
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
126
|
+
},
|
|
127
|
+
set(target, property, value) {
|
|
128
|
+
return Reflect.set(target, property, value, target);
|
|
129
|
+
},
|
|
130
|
+
}) as EditorComponent;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function registerAgentMentionHighlighting(ctx: ExtensionContext, getAgents: () => AgentConfig[]): void {
|
|
134
|
+
if (ctx.mode !== "tui") return;
|
|
135
|
+
|
|
136
|
+
const previousEditorFactory = ctx.ui.getEditorComponent();
|
|
137
|
+
ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
138
|
+
const baseEditor = previousEditorFactory?.(tui, theme, keybindings) ?? new CustomEditor(tui, theme, keybindings);
|
|
139
|
+
return createAgentMentionHighlightEditor(baseEditor, getAgents, theme);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createAgentMentionAutocompleteProvider(
|
|
144
|
+
current: AutocompleteProvider,
|
|
145
|
+
getAgents: () => AgentConfig[],
|
|
146
|
+
): AutocompleteProvider {
|
|
147
|
+
return {
|
|
148
|
+
triggerCharacters: Array.from(new Set([...(current.triggerCharacters ?? []), ">"])),
|
|
149
|
+
async getSuggestions(lines, cursorLine, cursorCol, options): Promise<AutocompleteSuggestions | null> {
|
|
150
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
151
|
+
const query = extractAgentMentionQuery(currentLine.slice(0, cursorCol));
|
|
152
|
+
if (query === undefined) {
|
|
153
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const candidates = filterAgentMentionCandidates(getAgents(), query);
|
|
157
|
+
if (options.signal.aborted || candidates.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
prefix: `>${query}`,
|
|
161
|
+
items: candidates.map((agent) => ({
|
|
162
|
+
value: `>${agent.name}`,
|
|
163
|
+
label: `>${agent.name}`,
|
|
164
|
+
description: agent.description,
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
170
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
174
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryan_nookpi/pi-extension-subagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"lifecycle.ts",
|
|
41
41
|
"invocation-queue.ts",
|
|
42
42
|
"live-preview.ts",
|
|
43
|
+
"mentions.ts",
|
|
43
44
|
"persisted-session.ts",
|
|
44
45
|
"replay.ts",
|
|
45
46
|
"registration-manifest.ts",
|