@tintinweb/pi-subagents 0.2.0
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 +81 -0
- package/LICENSE +21 -0
- package/README.md +223 -0
- package/package.json +46 -0
- package/src/agent-manager.ts +287 -0
- package/src/agent-runner.ts +341 -0
- package/src/agent-types.ts +137 -0
- package/src/context.ts +58 -0
- package/src/custom-agents.ts +94 -0
- package/src/env.ts +33 -0
- package/src/index.ts +855 -0
- package/src/prompts.ts +163 -0
- package/src/types.ts +84 -0
- package/src/ui/agent-widget.ts +326 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Renamed package to `@tintinweb/pi-subagents`
|
|
12
|
+
- Fuzzy model resolver now only matches models with auth configured (prevents selecting unconfigured providers)
|
|
13
|
+
- `getDisplayName()` now delegates to `getConfig()` instead of separate lookups
|
|
14
|
+
- Removed unused `Tool` type export from agent-types
|
|
15
|
+
|
|
16
|
+
### Refactored
|
|
17
|
+
- Extracted `createActivityTracker()` — eliminates duplicated tool activity wiring between foreground and background paths
|
|
18
|
+
- Extracted `safeFormatTokens()` — replaces 4 repeated try-catch blocks
|
|
19
|
+
- Extracted `buildDetails()` — consolidates AgentDetails construction
|
|
20
|
+
- Extracted `getStatusLabel()` / `getStatusNote()` — consolidates 3 duplicated status formatting chains
|
|
21
|
+
- Shared `extractText()` — consolidated duplicate from context.ts and agent-runner.ts
|
|
22
|
+
- Added `ERROR_STATUSES` constant in widget for consistent status checks
|
|
23
|
+
|
|
24
|
+
## [0.4.1] - 2026-03-05
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **Persistent above-editor widget** — tree view of all running/queued/finished agents with animated spinners and live stats
|
|
28
|
+
- **Concurrency queue** — configurable max concurrent background agents (default: 4), auto-drain
|
|
29
|
+
- **Queued agents** collapsed to single summary line in widget
|
|
30
|
+
- **Turn-based widget linger** — completed agents clear after 1 turn, errors/aborted linger for 2 extra turns
|
|
31
|
+
- **Colored status icons** — themed rendering via `setWidget` callback form (`✓` green, `✓` yellow, `✗` red, `■` dim)
|
|
32
|
+
- **Live response streaming** — `onTextDelta` shows truncated agent response text instead of static "thinking..."
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Tool names match Claude Code: `Agent`, `get_subagent_result`, `steer_subagent`
|
|
36
|
+
- Labels use "Agent" / "Agents" (not "Subagent")
|
|
37
|
+
- Widget heading: `●` when active, `○` when only lingering finished agents
|
|
38
|
+
- Extracted all UI code to `src/ui/agent-widget.ts`
|
|
39
|
+
|
|
40
|
+
## [0.2.0] - 2026-03-05
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **Claude Code-style UI rendering** — `renderCall`/`renderResult`/`onUpdate` for live streaming progress
|
|
44
|
+
- Live activity descriptions: "searching, reading 3 files…"
|
|
45
|
+
- Token count display: "33.8k tokens"
|
|
46
|
+
- Per-agent tool use counter
|
|
47
|
+
- Expandable completed results (ctrl+o)
|
|
48
|
+
- Distinct states: running, background, completed, error, aborted
|
|
49
|
+
- **Async environment detection** — replaced `execSync` with `pi.exec()` for non-blocking git/platform detection
|
|
50
|
+
- **Status bar integration** — running background agent count shown in pi's status bar
|
|
51
|
+
- **Fuzzy model selection** — `"haiku"`, `"sonnet"` resolve to best matching available model
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- Tool label changed from "Spawn Agent" to "Agent" (matches Claude Code style)
|
|
55
|
+
- `onToolUse` callback replaced with richer `onToolActivity` (includes tool name + start/end)
|
|
56
|
+
- `onSessionCreated` callback for accessing session stats (token counts)
|
|
57
|
+
- `env.ts` now requires `ExtensionAPI` parameter (async `pi.exec()` instead of `execSync`)
|
|
58
|
+
|
|
59
|
+
## [0.1.0] - 2026-03-05
|
|
60
|
+
|
|
61
|
+
Initial release.
|
|
62
|
+
|
|
63
|
+
### Added
|
|
64
|
+
- **Autonomous sub-agents** — spawn specialized agents via tool call, each running in an isolated pi session
|
|
65
|
+
- **Built-in agent types** — general-purpose, Explore (defaults to haiku), Plan, statusline-setup, claude-code-guide
|
|
66
|
+
- **Custom user-defined agents** — define agents in `.pi/agents/<name>.md` with YAML frontmatter + system prompt body
|
|
67
|
+
- **Frontmatter configuration** — tools, extensions, skills, model, thinking, max_turns, prompt_mode, inherit_context, run_in_background, isolated
|
|
68
|
+
- **Graceful max_turns** — steer message at limit, 5 grace turns, then hard abort
|
|
69
|
+
- **Background execution** — `run_in_background` with completion notifications
|
|
70
|
+
- **`get_subagent_result` tool** — check status, wait for completion, verbose conversation output
|
|
71
|
+
- **`steer_subagent` tool** — inject steering messages into running agents mid-execution
|
|
72
|
+
- **Agent resume** — continue a previous agent's session with a new prompt
|
|
73
|
+
- **Context inheritance** — fork the parent conversation into the sub-agent
|
|
74
|
+
- **Model override** — per-agent model selection
|
|
75
|
+
- **Thinking level** — per-agent extended thinking control
|
|
76
|
+
- **`/agent` and `/agents` commands**
|
|
77
|
+
|
|
78
|
+
[Unreleased]: https://github.com/tintinweb/pi-subagents/compare/v0.4.1...HEAD
|
|
79
|
+
[0.4.1]: https://github.com/tintinweb/pi-subagents/compare/v0.2.0...v0.4.1
|
|
80
|
+
[0.2.0]: https://github.com/tintinweb/pi-subagents/compare/v0.1.0...v0.2.0
|
|
81
|
+
[0.1.0]: https://github.com/tintinweb/pi-subagents/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tintinweb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @tintinweb/pi-subagents
|
|
2
|
+
|
|
3
|
+
A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-agents** to pi. Spawn specialized agents that run in isolated sessions — each with its own tools, system prompt, model, and thinking level. Run them in foreground or background, steer them mid-run, resume completed sessions, and define your own custom agent types.
|
|
4
|
+
|
|
5
|
+
> **Status:** Early release.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Claude Code look & feel** — same tool names, calling conventions, and UI patterns (`Agent`, `get_subagent_result`, `steer_subagent`) — feels native
|
|
10
|
+
- **Parallel background agents** — spawn multiple agents that run concurrently with automatic queuing (configurable concurrency limit, default 4)
|
|
11
|
+
- **Live widget UI** — persistent above-editor widget with animated spinners, live tool activity, token counts, and colored status icons
|
|
12
|
+
- **Custom agent types** — define agents in `.pi/agents/<name>.md` with YAML frontmatter: custom system prompts, model selection, thinking levels, tool restrictions
|
|
13
|
+
- **Mid-run steering** — inject messages into running agents to redirect their work without restarting
|
|
14
|
+
- **Session resume** — pick up where an agent left off, preserving full conversation context
|
|
15
|
+
- **Graceful turn limits** — agents get a "wrap up" warning before hard abort, producing clean partial results instead of cut-off output
|
|
16
|
+
- **Fuzzy model selection** — specify models by name (`"haiku"`, `"sonnet"`) instead of full IDs, with automatic filtering to only available/configured models
|
|
17
|
+
- **Context inheritance** — optionally fork the parent conversation into a sub-agent so it knows what's been discussed
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pi install npm:@tintinweb/pi-subagents
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or load directly for development:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pi -e ./src/index.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
The parent agent spawns sub-agents using the `Agent` tool:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Agent({
|
|
37
|
+
subagent_type: "Explore",
|
|
38
|
+
prompt: "Find all files that handle authentication",
|
|
39
|
+
description: "Find auth files",
|
|
40
|
+
run_in_background: true,
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Foreground agents block until complete and return results inline. Background agents return an ID immediately and notify you on completion.
|
|
45
|
+
|
|
46
|
+
## UI
|
|
47
|
+
|
|
48
|
+
The extension renders a persistent widget above the editor showing all active agents:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
● Agents
|
|
52
|
+
├─ ⠹ Agent Refactor auth module · 5 tool uses · 33.8k tokens · 12.3s
|
|
53
|
+
│ ⎿ editing 2 files…
|
|
54
|
+
├─ ⠹ Explore Find auth files · 3 tool uses · 12.4k tokens · 4.1s
|
|
55
|
+
│ ⎿ searching…
|
|
56
|
+
└─ 2 queued
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Individual agent results render Claude Code-style in the conversation:
|
|
60
|
+
|
|
61
|
+
| State | Example |
|
|
62
|
+
|-------|---------|
|
|
63
|
+
| **Running** | `⠹ 3 tool uses · 12.4k tokens` / `⎿ searching, reading 3 files…` |
|
|
64
|
+
| **Completed** | `✓ 5 tool uses · 33.8k tokens · 12.3s` / `⎿ Done` |
|
|
65
|
+
| **Wrapped up** | `✓ 50 tool uses · 89.1k tokens · 45.2s` / `⎿ Wrapped up (turn limit)` |
|
|
66
|
+
| **Stopped** | `■ 3 tool uses · 12.4k tokens` / `⎿ Stopped` |
|
|
67
|
+
| **Error** | `✗ 3 tool uses · 12.4k tokens` / `⎿ Error: timeout` |
|
|
68
|
+
| **Aborted** | `✗ 55 tool uses · 102.3k tokens` / `⎿ Aborted (max turns exceeded)` |
|
|
69
|
+
|
|
70
|
+
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
71
|
+
|
|
72
|
+
## Built-in Agent Types
|
|
73
|
+
|
|
74
|
+
| Type | Tools | Description |
|
|
75
|
+
|------|-------|-------------|
|
|
76
|
+
| `general-purpose` | all 7 | Full read/write access for complex multi-step tasks |
|
|
77
|
+
| `Explore` | read, bash, grep, find, ls | Fast codebase exploration (read-only, defaults to haiku) |
|
|
78
|
+
| `Plan` | read, bash, grep, find, ls | Software architect for implementation planning (read-only) |
|
|
79
|
+
| `statusline-setup` | read, edit | Configuration editor |
|
|
80
|
+
| `claude-code-guide` | read, grep, find | Documentation and help queries |
|
|
81
|
+
|
|
82
|
+
## Custom Agents
|
|
83
|
+
|
|
84
|
+
Define custom agent types by creating `.pi/agents/<name>.md` files. The filename becomes the agent type name.
|
|
85
|
+
|
|
86
|
+
### Example: `.pi/agents/auditor.md`
|
|
87
|
+
|
|
88
|
+
```markdown
|
|
89
|
+
---
|
|
90
|
+
description: Security Code Reviewer
|
|
91
|
+
tools: read, grep, find, bash
|
|
92
|
+
model: anthropic/claude-opus-4-6
|
|
93
|
+
thinking: high
|
|
94
|
+
max_turns: 30
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
You are a security auditor. Review code for vulnerabilities including:
|
|
98
|
+
- Injection flaws (SQL, command, XSS)
|
|
99
|
+
- Authentication and authorization issues
|
|
100
|
+
- Sensitive data exposure
|
|
101
|
+
- Insecure configurations
|
|
102
|
+
|
|
103
|
+
Report findings with file paths, line numbers, severity, and remediation advice.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Then spawn it like any built-in type:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Agent({ subagent_type: "auditor", prompt: "Review the auth module", description: "Security audit" })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Frontmatter Fields
|
|
113
|
+
|
|
114
|
+
All fields are optional — sensible defaults for everything.
|
|
115
|
+
|
|
116
|
+
| Field | Default | Description |
|
|
117
|
+
|-------|---------|-------------|
|
|
118
|
+
| `description` | filename | Agent description shown in tool listings |
|
|
119
|
+
| `tools` | all 7 | Comma-separated built-in tools: read, bash, edit, write, grep, find, ls. `none` for no tools |
|
|
120
|
+
| `extensions` | `true` | Inherit MCP/extension tools. `false` to disable |
|
|
121
|
+
| `skills` | `true` | Inherit skills from parent |
|
|
122
|
+
| `model` | inherit parent | Model as `provider/modelId` |
|
|
123
|
+
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
|
|
124
|
+
| `max_turns` | 50 | Max agentic turns before graceful shutdown |
|
|
125
|
+
| `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to default prompt |
|
|
126
|
+
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
127
|
+
| `run_in_background` | `false` | Run in background by default |
|
|
128
|
+
| `isolated` | `false` | No extension/MCP tools, only built-in |
|
|
129
|
+
|
|
130
|
+
Frontmatter sets defaults. Explicit `Agent` parameters always override them.
|
|
131
|
+
|
|
132
|
+
## Tools
|
|
133
|
+
|
|
134
|
+
### `Agent`
|
|
135
|
+
|
|
136
|
+
Launch a sub-agent.
|
|
137
|
+
|
|
138
|
+
| Parameter | Type | Required | Description |
|
|
139
|
+
|-----------|------|----------|-------------|
|
|
140
|
+
| `prompt` | string | yes | The task for the agent |
|
|
141
|
+
| `description` | string | yes | Short 3-5 word summary (shown in UI) |
|
|
142
|
+
| `subagent_type` | string | yes | Agent type (built-in or custom) |
|
|
143
|
+
| `model` | string | no | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
144
|
+
| `thinking` | string | no | Thinking level: off, minimal, low, medium, high, xhigh |
|
|
145
|
+
| `max_turns` | number | no | Max agentic turns (default: 50) |
|
|
146
|
+
| `run_in_background` | boolean | no | Run without blocking |
|
|
147
|
+
| `resume` | string | no | Agent ID to resume a previous session |
|
|
148
|
+
| `isolated` | boolean | no | No extension/MCP tools |
|
|
149
|
+
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
150
|
+
|
|
151
|
+
### `get_subagent_result`
|
|
152
|
+
|
|
153
|
+
Check status and retrieve results from a background agent.
|
|
154
|
+
|
|
155
|
+
| Parameter | Type | Required | Description |
|
|
156
|
+
|-----------|------|----------|-------------|
|
|
157
|
+
| `agent_id` | string | yes | Agent ID to check |
|
|
158
|
+
| `wait` | boolean | no | Wait for completion |
|
|
159
|
+
| `verbose` | boolean | no | Include full conversation log |
|
|
160
|
+
|
|
161
|
+
### `steer_subagent`
|
|
162
|
+
|
|
163
|
+
Send a steering message to a running agent. The message interrupts after the current tool execution.
|
|
164
|
+
|
|
165
|
+
| Parameter | Type | Required | Description |
|
|
166
|
+
|-----------|------|----------|-------------|
|
|
167
|
+
| `agent_id` | string | yes | Agent ID to steer |
|
|
168
|
+
| `message` | string | yes | Message to inject into agent conversation |
|
|
169
|
+
|
|
170
|
+
## Commands
|
|
171
|
+
|
|
172
|
+
| Command | Description |
|
|
173
|
+
|---------|-------------|
|
|
174
|
+
| `/agent <type> <prompt>` | Spawn a sub-agent interactively |
|
|
175
|
+
| `/agents` | List all agents with status tree |
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
/agent Explore Find all TypeScript files that handle authentication
|
|
179
|
+
/agent Plan Design a caching layer for the API
|
|
180
|
+
/agent auditor Review the payment processing module
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Graceful Max Turns
|
|
184
|
+
|
|
185
|
+
Instead of hard-aborting at the turn limit, agents get a graceful shutdown:
|
|
186
|
+
|
|
187
|
+
1. At `max_turns` — steering message: *"Wrap up immediately — provide your final answer now."*
|
|
188
|
+
2. Up to 5 grace turns to finish cleanly
|
|
189
|
+
3. Hard abort only after the grace period
|
|
190
|
+
|
|
191
|
+
| Status | Meaning | Icon |
|
|
192
|
+
|--------|---------|------|
|
|
193
|
+
| `completed` | Finished naturally | `✓` green |
|
|
194
|
+
| `steered` | Hit limit, wrapped up in time | `✓` yellow |
|
|
195
|
+
| `aborted` | Grace period exceeded | `✗` red |
|
|
196
|
+
| `stopped` | User-initiated abort | `■` dim |
|
|
197
|
+
|
|
198
|
+
## Concurrency
|
|
199
|
+
|
|
200
|
+
Background agents are subject to a configurable concurrency limit (default: 4). Excess agents are automatically queued and start as running agents complete. The widget shows queued agents as a collapsed count.
|
|
201
|
+
|
|
202
|
+
Foreground agents bypass the queue — they block the parent anyway.
|
|
203
|
+
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
src/
|
|
208
|
+
index.ts # Extension entry: tool/command registration, rendering
|
|
209
|
+
types.ts # Type definitions (SubagentType, AgentRecord, configs)
|
|
210
|
+
agent-types.ts # Agent type registry (built-in + custom), tool factories
|
|
211
|
+
agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
|
|
212
|
+
agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
|
|
213
|
+
custom-agents.ts # Load custom agents from .pi/agents/*.md
|
|
214
|
+
prompts.ts # System prompts per agent type
|
|
215
|
+
context.ts # Parent conversation context for inherit_context
|
|
216
|
+
env.ts # Environment detection (git, platform)
|
|
217
|
+
ui/
|
|
218
|
+
agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT — [tintinweb](https://github.com/tintinweb)
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tintinweb/pi-subagents",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A pi extension providing autonomous sub-agents with Claude Code-style UI",
|
|
5
|
+
"author": "tintinweb",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tintinweb/pi-subagents.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/tintinweb/pi-subagents#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/tintinweb/pi-subagents/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"subagent",
|
|
20
|
+
"agent",
|
|
21
|
+
"autonomous"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@mariozechner/pi-ai": "latest",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "latest",
|
|
26
|
+
"@mariozechner/pi-tui": "latest",
|
|
27
|
+
"@sinclair/typebox": "latest"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.0.0",
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vitest": "^4.0.18"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./src/index.ts"
|
|
42
|
+
],
|
|
43
|
+
"video": "https://github.com/tintinweb/pi-subagents/raw/master/media/demo.mp4",
|
|
44
|
+
"image": "https://github.com/tintinweb/pi-subagents/raw/master/media/screenshot.png"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
3
|
+
*
|
|
4
|
+
* Background agents are subject to a configurable concurrency limit (default: 4).
|
|
5
|
+
* Excess agents are queued and auto-started as running agents complete.
|
|
6
|
+
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import type { ExtensionContext, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
12
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
import { runAgent, resumeAgent, type ToolActivity } from "./agent-runner.js";
|
|
14
|
+
import type { SubagentType, AgentRecord, ThinkingLevel } from "./types.js";
|
|
15
|
+
|
|
16
|
+
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
17
|
+
|
|
18
|
+
/** Default max concurrent background agents. */
|
|
19
|
+
const DEFAULT_MAX_CONCURRENT = 4;
|
|
20
|
+
|
|
21
|
+
interface SpawnArgs {
|
|
22
|
+
pi: ExtensionAPI;
|
|
23
|
+
ctx: ExtensionContext;
|
|
24
|
+
type: SubagentType;
|
|
25
|
+
prompt: string;
|
|
26
|
+
options: SpawnOptions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SpawnOptions {
|
|
30
|
+
description: string;
|
|
31
|
+
model?: Model<any>;
|
|
32
|
+
maxTurns?: number;
|
|
33
|
+
isolated?: boolean;
|
|
34
|
+
inheritContext?: boolean;
|
|
35
|
+
thinkingLevel?: ThinkingLevel;
|
|
36
|
+
systemPromptOverride?: string;
|
|
37
|
+
systemPromptAppend?: string;
|
|
38
|
+
isBackground?: boolean;
|
|
39
|
+
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
40
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
41
|
+
/** Called on streaming text deltas from the assistant response. */
|
|
42
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
43
|
+
/** Called when the agent session is created (for accessing session stats). */
|
|
44
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class AgentManager {
|
|
48
|
+
private agents = new Map<string, AgentRecord>();
|
|
49
|
+
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
50
|
+
private onComplete?: OnAgentComplete;
|
|
51
|
+
private maxConcurrent: number;
|
|
52
|
+
|
|
53
|
+
/** Queue of background agents waiting to start. */
|
|
54
|
+
private queue: { id: string; args: SpawnArgs }[] = [];
|
|
55
|
+
/** Number of currently running background agents. */
|
|
56
|
+
private runningBackground = 0;
|
|
57
|
+
|
|
58
|
+
constructor(onComplete?: OnAgentComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
59
|
+
this.onComplete = onComplete;
|
|
60
|
+
this.maxConcurrent = maxConcurrent;
|
|
61
|
+
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
62
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Update the max concurrent background agents limit. */
|
|
66
|
+
setMaxConcurrent(n: number) {
|
|
67
|
+
this.maxConcurrent = Math.max(1, n);
|
|
68
|
+
// Start queued agents if the new limit allows
|
|
69
|
+
this.drainQueue();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getMaxConcurrent(): number {
|
|
73
|
+
return this.maxConcurrent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Spawn an agent and return its ID immediately (for background use).
|
|
78
|
+
* If the concurrency limit is reached, the agent is queued.
|
|
79
|
+
*/
|
|
80
|
+
spawn(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
ctx: ExtensionContext,
|
|
83
|
+
type: SubagentType,
|
|
84
|
+
prompt: string,
|
|
85
|
+
options: SpawnOptions,
|
|
86
|
+
): string {
|
|
87
|
+
const id = randomUUID().slice(0, 17);
|
|
88
|
+
const abortController = new AbortController();
|
|
89
|
+
const record: AgentRecord = {
|
|
90
|
+
id,
|
|
91
|
+
type,
|
|
92
|
+
description: options.description,
|
|
93
|
+
status: options.isBackground ? "queued" : "running",
|
|
94
|
+
toolUses: 0,
|
|
95
|
+
startedAt: Date.now(),
|
|
96
|
+
abortController,
|
|
97
|
+
};
|
|
98
|
+
this.agents.set(id, record);
|
|
99
|
+
|
|
100
|
+
const args: SpawnArgs = { pi, ctx, type, prompt, options };
|
|
101
|
+
|
|
102
|
+
if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
|
|
103
|
+
// Queue it — will be started when a running agent completes
|
|
104
|
+
this.queue.push({ id, args });
|
|
105
|
+
return id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.startAgent(id, record, args);
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Actually start an agent (called immediately or from queue drain). */
|
|
113
|
+
private startAgent(id: string, record: AgentRecord, { pi, ctx, type, prompt, options }: SpawnArgs) {
|
|
114
|
+
record.status = "running";
|
|
115
|
+
record.startedAt = Date.now();
|
|
116
|
+
if (options.isBackground) this.runningBackground++;
|
|
117
|
+
|
|
118
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
119
|
+
pi,
|
|
120
|
+
model: options.model,
|
|
121
|
+
maxTurns: options.maxTurns,
|
|
122
|
+
isolated: options.isolated,
|
|
123
|
+
inheritContext: options.inheritContext,
|
|
124
|
+
thinkingLevel: options.thinkingLevel,
|
|
125
|
+
systemPromptOverride: options.systemPromptOverride,
|
|
126
|
+
systemPromptAppend: options.systemPromptAppend,
|
|
127
|
+
signal: record.abortController!.signal,
|
|
128
|
+
onToolActivity: (activity) => {
|
|
129
|
+
if (activity.type === "end") record.toolUses++;
|
|
130
|
+
options.onToolActivity?.(activity);
|
|
131
|
+
},
|
|
132
|
+
onTextDelta: options.onTextDelta,
|
|
133
|
+
onSessionCreated: (session) => {
|
|
134
|
+
record.session = session;
|
|
135
|
+
options.onSessionCreated?.(session);
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
.then(({ responseText, session, aborted, steered }) => {
|
|
139
|
+
// Don't overwrite status if externally stopped via abort()
|
|
140
|
+
if (record.status !== "stopped") {
|
|
141
|
+
record.status = aborted ? "aborted" : steered ? "steered" : "completed";
|
|
142
|
+
}
|
|
143
|
+
record.result = responseText;
|
|
144
|
+
record.session = session;
|
|
145
|
+
record.completedAt ??= Date.now();
|
|
146
|
+
if (options.isBackground) {
|
|
147
|
+
this.runningBackground--;
|
|
148
|
+
this.onComplete?.(record);
|
|
149
|
+
this.drainQueue();
|
|
150
|
+
}
|
|
151
|
+
return responseText;
|
|
152
|
+
})
|
|
153
|
+
.catch((err) => {
|
|
154
|
+
// Don't overwrite status if externally stopped via abort()
|
|
155
|
+
if (record.status !== "stopped") {
|
|
156
|
+
record.status = "error";
|
|
157
|
+
}
|
|
158
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
159
|
+
record.completedAt ??= Date.now();
|
|
160
|
+
if (options.isBackground) {
|
|
161
|
+
this.runningBackground--;
|
|
162
|
+
this.onComplete?.(record);
|
|
163
|
+
this.drainQueue();
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
record.promise = promise;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Start queued agents up to the concurrency limit. */
|
|
172
|
+
private drainQueue() {
|
|
173
|
+
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
174
|
+
const next = this.queue.shift()!;
|
|
175
|
+
const record = this.agents.get(next.id);
|
|
176
|
+
if (!record || record.status !== "queued") continue;
|
|
177
|
+
this.startAgent(next.id, record, next.args);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Spawn an agent and wait for completion (foreground use).
|
|
183
|
+
* Foreground agents bypass the concurrency queue.
|
|
184
|
+
*/
|
|
185
|
+
async spawnAndWait(
|
|
186
|
+
pi: ExtensionAPI,
|
|
187
|
+
ctx: ExtensionContext,
|
|
188
|
+
type: SubagentType,
|
|
189
|
+
prompt: string,
|
|
190
|
+
options: Omit<SpawnOptions, "isBackground">,
|
|
191
|
+
): Promise<AgentRecord> {
|
|
192
|
+
const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
|
|
193
|
+
const record = this.agents.get(id)!;
|
|
194
|
+
await record.promise;
|
|
195
|
+
return record;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resume an existing agent session with a new prompt.
|
|
200
|
+
*/
|
|
201
|
+
async resume(
|
|
202
|
+
id: string,
|
|
203
|
+
prompt: string,
|
|
204
|
+
signal?: AbortSignal,
|
|
205
|
+
): Promise<AgentRecord | undefined> {
|
|
206
|
+
const record = this.agents.get(id);
|
|
207
|
+
if (!record?.session) return undefined;
|
|
208
|
+
|
|
209
|
+
record.status = "running";
|
|
210
|
+
record.startedAt = Date.now();
|
|
211
|
+
record.completedAt = undefined;
|
|
212
|
+
record.result = undefined;
|
|
213
|
+
record.error = undefined;
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const responseText = await resumeAgent(record.session, prompt, {
|
|
217
|
+
onToolActivity: (activity) => {
|
|
218
|
+
if (activity.type === "end") record.toolUses++;
|
|
219
|
+
},
|
|
220
|
+
signal,
|
|
221
|
+
});
|
|
222
|
+
record.status = "completed";
|
|
223
|
+
record.result = responseText;
|
|
224
|
+
record.completedAt = Date.now();
|
|
225
|
+
} catch (err) {
|
|
226
|
+
record.status = "error";
|
|
227
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
228
|
+
record.completedAt = Date.now();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return record;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getRecord(id: string): AgentRecord | undefined {
|
|
235
|
+
return this.agents.get(id);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
listAgents(): AgentRecord[] {
|
|
239
|
+
return [...this.agents.values()].sort(
|
|
240
|
+
(a, b) => b.startedAt - a.startedAt,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
abort(id: string): boolean {
|
|
245
|
+
const record = this.agents.get(id);
|
|
246
|
+
if (!record) return false;
|
|
247
|
+
|
|
248
|
+
// Remove from queue if queued
|
|
249
|
+
if (record.status === "queued") {
|
|
250
|
+
this.queue = this.queue.filter(q => q.id !== id);
|
|
251
|
+
record.status = "stopped";
|
|
252
|
+
record.completedAt = Date.now();
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (record.status !== "running") return false;
|
|
257
|
+
record.abortController?.abort();
|
|
258
|
+
record.status = "stopped";
|
|
259
|
+
record.completedAt = Date.now();
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private cleanup() {
|
|
264
|
+
const cutoff = Date.now() - 10 * 60_000;
|
|
265
|
+
for (const [id, record] of this.agents) {
|
|
266
|
+
if (record.status === "running" || record.status === "queued") continue;
|
|
267
|
+
if ((record.completedAt ?? 0) >= cutoff) continue;
|
|
268
|
+
|
|
269
|
+
// Dispose and clear session so memory can be reclaimed
|
|
270
|
+
if (record.session) {
|
|
271
|
+
record.session.dispose();
|
|
272
|
+
record.session = undefined;
|
|
273
|
+
}
|
|
274
|
+
this.agents.delete(id);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
dispose() {
|
|
279
|
+
clearInterval(this.cleanupInterval);
|
|
280
|
+
// Clear queue
|
|
281
|
+
this.queue = [];
|
|
282
|
+
for (const record of this.agents.values()) {
|
|
283
|
+
record.session?.dispose();
|
|
284
|
+
}
|
|
285
|
+
this.agents.clear();
|
|
286
|
+
}
|
|
287
|
+
}
|