@pi-unipi/subagents 0.1.9 → 0.1.11
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 +114 -0
- package/package.json +1 -1
- package/src/index.ts +77 -106
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# @pi-unipi/subagents
|
|
2
|
+
|
|
3
|
+
Parallel sub-agent execution for [Pi coding agent](https://github.com/badlogic/pi-mono). Spawn background or foreground agents, track activity, and manage concurrent work.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@pi-unipi/subagents
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or as part of the full suite:
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:unipi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Tools
|
|
17
|
+
|
|
18
|
+
| Tool | Description |
|
|
19
|
+
|------|-------------|
|
|
20
|
+
| `spawn_helper` | Launch a sub-agent for parallel work |
|
|
21
|
+
| `get_helper_result` | Check status and retrieve results from a background agent |
|
|
22
|
+
|
|
23
|
+
## Agent Types
|
|
24
|
+
|
|
25
|
+
| Type | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `explore` | Parallel file reads, research, analysis |
|
|
28
|
+
| `work` | Parallel file writes with transparent locking |
|
|
29
|
+
| Custom | Define your own in `~/.unipi/config/agents/<name>.md` |
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Foreground (blocks until done)
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
spawn_helper(
|
|
37
|
+
type: "explore",
|
|
38
|
+
prompt: "Find all auth-related files",
|
|
39
|
+
description: "Research auth files"
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Background (returns immediately)
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
spawn_helper(
|
|
47
|
+
type: "work",
|
|
48
|
+
prompt: "Fix all lint errors in src/",
|
|
49
|
+
description: "Fix lint errors",
|
|
50
|
+
run_in_background: true
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Check Background Result
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
get_helper_result(agent_id: "helper_abc123")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Options
|
|
61
|
+
|
|
62
|
+
| Parameter | Description |
|
|
63
|
+
|-----------|-------------|
|
|
64
|
+
| `type` | Agent type (`explore`, `work`, or custom) |
|
|
65
|
+
| `prompt` | Task for the agent |
|
|
66
|
+
| `description` | Short description (3-5 words) |
|
|
67
|
+
| `run_in_background` | Return immediately, notify on completion |
|
|
68
|
+
| `max_turns` | Max agentic turns before stopping |
|
|
69
|
+
| `model` | Model override (e.g. `"haiku"`, `"sonnet"`) |
|
|
70
|
+
| `thinking` | Thinking level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`) |
|
|
71
|
+
|
|
72
|
+
## Custom Agent Types
|
|
73
|
+
|
|
74
|
+
Create markdown files defining agent behavior:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Global agents
|
|
78
|
+
~/.unipi/config/agents/reviewer.md
|
|
79
|
+
|
|
80
|
+
# Project agents
|
|
81
|
+
<workspace>/.unipi/config/agents/deployer.md
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
// ~/.unipi/config/subagents.json
|
|
88
|
+
{
|
|
89
|
+
"enabled": true,
|
|
90
|
+
"maxConcurrent": 3,
|
|
91
|
+
"types": {
|
|
92
|
+
"explore": { "enabled": true },
|
|
93
|
+
"work": { "enabled": true }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Features
|
|
99
|
+
|
|
100
|
+
- **Concurrent execution** — run up to N agents simultaneously
|
|
101
|
+
- **File locking** — transparent locking for parallel writes
|
|
102
|
+
- **ESC propagation** — kill all agents with ESC
|
|
103
|
+
- **Activity tracking** — real-time widget showing agent progress
|
|
104
|
+
- **Info screen integration** — agent status in dashboard
|
|
105
|
+
|
|
106
|
+
## Dependencies
|
|
107
|
+
|
|
108
|
+
- `@pi-unipi/core` — shared utilities
|
|
109
|
+
- `@pi-unipi/workflow` — workflow integration
|
|
110
|
+
- `@pi-unipi/info-screen` — dashboard registration
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -5,23 +5,24 @@
|
|
|
5
5
|
* ESC propagation: all children abort on parent ESC
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { defineTool, type ExtensionAPI
|
|
9
|
-
import { Text } from "@mariozechner/pi-tui";
|
|
8
|
+
import { defineTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
10
9
|
import { Type } from "@sinclair/typebox";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { emitEvent, MODULES, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
14
|
+
import { AgentManager } from "./agent-manager.js";
|
|
15
|
+
import { initConfig } from "./config.js";
|
|
16
|
+
import { type AgentActivity, BUILTIN_TYPES } from "./types.js";
|
|
17
|
+
import { AgentWidget } from "./widget.js";
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
/** Get info registry from global */
|
|
15
20
|
function getInfoRegistry() {
|
|
16
21
|
const g = globalThis as any;
|
|
17
22
|
return g.__unipi_info_registry;
|
|
18
23
|
}
|
|
19
|
-
import { AgentManager } from "./agent-manager.js";
|
|
20
|
-
import { initConfig, saveGlobalConfig } from "./config.js";
|
|
21
|
-
import { type AgentActivity, type AgentRecord, BUILTIN_TYPES } from "./types.js";
|
|
22
|
-
import { AgentWidget } from "./widget.js";
|
|
23
24
|
|
|
24
|
-
/** Format tokens safely
|
|
25
|
+
/** Format tokens safely */
|
|
25
26
|
function safeFormatTokens(session: any): string {
|
|
26
27
|
if (!session) return "";
|
|
27
28
|
try {
|
|
@@ -35,7 +36,7 @@ function safeFormatTokens(session: any): string {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/** Build result text
|
|
39
|
+
/** Build result text */
|
|
39
40
|
function textResult(msg: string, details?: any) {
|
|
40
41
|
return { content: [{ type: "text" as const, text: msg }], details };
|
|
41
42
|
}
|
|
@@ -45,17 +46,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
45
46
|
const config = initConfig(process.cwd());
|
|
46
47
|
if (!config.enabled) return;
|
|
47
48
|
|
|
49
|
+
// Compute paths at factory time
|
|
50
|
+
const homeDir = homedir();
|
|
51
|
+
const cwd = process.cwd();
|
|
52
|
+
const globalAgentsDir = join(homeDir, ".unipi", "config", "agents");
|
|
53
|
+
const workspaceAgentsDir = join(cwd, ".unipi", "config", "agents");
|
|
54
|
+
|
|
48
55
|
// Activity tracking for widget
|
|
49
56
|
const agentActivity = new Map<string, AgentActivity>();
|
|
50
57
|
|
|
51
58
|
// Create manager with completion callback
|
|
52
59
|
const manager = new AgentManager(
|
|
53
60
|
(record) => {
|
|
54
|
-
// On complete: clean up activity, emit event
|
|
55
61
|
agentActivity.delete(record.id);
|
|
56
62
|
widget.markFinished(record.id);
|
|
57
63
|
widget.update();
|
|
58
|
-
|
|
59
64
|
pi.events.emit("subagents:completed", {
|
|
60
65
|
id: record.id,
|
|
61
66
|
type: record.type,
|
|
@@ -67,7 +72,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
72
|
},
|
|
68
73
|
config.maxConcurrent,
|
|
69
74
|
(record) => {
|
|
70
|
-
// On start: emit event
|
|
71
75
|
pi.events.emit("subagents:started", {
|
|
72
76
|
id: record.id,
|
|
73
77
|
type: record.type,
|
|
@@ -79,103 +83,78 @@ export default function (pi: ExtensionAPI) {
|
|
|
79
83
|
// Create widget
|
|
80
84
|
const widget = new AgentWidget(manager, agentActivity);
|
|
81
85
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
},
|
|
107
|
-
dataProvider: async () => {
|
|
108
|
-
// Get available agent types
|
|
109
|
-
const types = config.types || {};
|
|
110
|
-
const builtinTypes = ["explore", "work"];
|
|
111
|
-
|
|
112
|
-
// Check for custom agent types in filesystem
|
|
113
|
-
const customTypes: string[] = [];
|
|
86
|
+
// Register info group at factory time (not session_start)
|
|
87
|
+
const registry = getInfoRegistry();
|
|
88
|
+
if (registry) {
|
|
89
|
+
registry.registerGroup({
|
|
90
|
+
id: "subagents",
|
|
91
|
+
name: "Subagents",
|
|
92
|
+
icon: "🤖",
|
|
93
|
+
priority: 80,
|
|
94
|
+
config: {
|
|
95
|
+
showByDefault: true,
|
|
96
|
+
stats: [
|
|
97
|
+
{ id: "maxConcurrent", label: "Max Concurrent", show: true },
|
|
98
|
+
{ id: "activeCount", label: "Active Agents", show: true },
|
|
99
|
+
{ id: "enabled", label: "Enabled", show: true },
|
|
100
|
+
{ id: "types", label: "Available Types", show: true },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
dataProvider: async () => {
|
|
104
|
+
const types = config.types || {};
|
|
105
|
+
const builtinTypes = ["explore", "work"];
|
|
106
|
+
|
|
107
|
+
// Scan for custom agent types
|
|
108
|
+
const customTypes: string[] = [];
|
|
109
|
+
for (const dir of [globalAgentsDir, workspaceAgentsDir]) {
|
|
114
110
|
try {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Check global agents directory
|
|
119
|
-
if (fs.existsSync(globalAgents)) {
|
|
120
|
-
const files = fs.readdirSync(globalAgents);
|
|
121
|
-
for (const file of files) {
|
|
122
|
-
if (file.endsWith(".md")) {
|
|
111
|
+
if (existsSync(dir)) {
|
|
112
|
+
for (const file of readdirSync(dir)) {
|
|
113
|
+
if (file.endsWith(".md") && !customTypes.includes(file.replace(".md", ""))) {
|
|
123
114
|
customTypes.push(file.replace(".md", ""));
|
|
124
115
|
}
|
|
125
116
|
}
|
|
126
117
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
maxConcurrent: { value: String(manager.getMaxConcurrent()) },
|
|
158
|
-
activeCount: { value: String(activeAgents) },
|
|
159
|
-
enabled: { value: config.enabled ? "yes" : "no" },
|
|
160
|
-
types: {
|
|
161
|
-
value: allTypes.length > 0 ? allTypes[0] : "none",
|
|
162
|
-
detail: allTypes.length > 1 ? typeList : undefined,
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
}
|
|
118
|
+
} catch { /* ignore */ }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const allTypes = [...new Set([...builtinTypes, ...Object.keys(types), ...customTypes])];
|
|
122
|
+
const typeList = allTypes.map(t => {
|
|
123
|
+
const isEnabled = types[t]?.enabled !== false;
|
|
124
|
+
const isBuiltin = builtinTypes.includes(t);
|
|
125
|
+
const scope = customTypes.includes(t) ? "project" : "global";
|
|
126
|
+
return `${t}(${scope})${isEnabled ? "" : " [disabled]"}`;
|
|
127
|
+
}).join(", ");
|
|
128
|
+
|
|
129
|
+
const activeAgents = manager.listAgents().filter(a => a.status === "running").length;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
maxConcurrent: { value: String(manager.getMaxConcurrent()) },
|
|
133
|
+
activeCount: { value: String(activeAgents) },
|
|
134
|
+
enabled: { value: config.enabled ? "yes" : "no" },
|
|
135
|
+
types: {
|
|
136
|
+
value: allTypes.length > 0 ? allTypes[0] : "none",
|
|
137
|
+
detail: allTypes.length > 1 ? typeList : undefined,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Session start: emit MODULE_READY
|
|
145
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
146
|
+
const globalConfig = `${homeDir}/.unipi/config/subagents.json`;
|
|
147
|
+
const workspaceConfig = `${cwd}/.unipi/config/subagents.json`;
|
|
168
148
|
|
|
169
149
|
ctx.ui.notify(
|
|
170
150
|
`UniPi Subagents config:\n` +
|
|
171
151
|
`• Global: ${globalConfig}\n` +
|
|
172
|
-
`• Global agents: ${
|
|
152
|
+
`• Global agents: ${globalAgentsDir}\n` +
|
|
173
153
|
`• Workspace: ${workspaceConfig}\n` +
|
|
174
|
-
`• Workspace agents: ${
|
|
154
|
+
`• Workspace agents: ${workspaceAgentsDir}`,
|
|
175
155
|
"info",
|
|
176
156
|
);
|
|
177
157
|
|
|
178
|
-
// Emit module ready event
|
|
179
158
|
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
180
159
|
name: MODULES.SUBAGENTS || "subagents",
|
|
181
160
|
version: "0.1.8",
|
|
@@ -305,11 +284,9 @@ Guidelines:
|
|
|
305
284
|
const modelInput = params.model as string | undefined;
|
|
306
285
|
const thinkingLevel = params.thinking as any | undefined;
|
|
307
286
|
|
|
308
|
-
// Create activity tracker
|
|
309
287
|
const { state: bgState, callbacks: bgCallbacks } = createActivityTracker(maxTurns);
|
|
310
288
|
|
|
311
289
|
if (runInBackground) {
|
|
312
|
-
// Background execution
|
|
313
290
|
const id = manager.spawn(pi, ctx, type, prompt, {
|
|
314
291
|
description,
|
|
315
292
|
maxTurns,
|
|
@@ -342,7 +319,6 @@ Guidelines:
|
|
|
342
319
|
// Foreground execution
|
|
343
320
|
let spinnerFrame = 0;
|
|
344
321
|
const startedAt = Date.now();
|
|
345
|
-
let fgId: string | undefined;
|
|
346
322
|
|
|
347
323
|
const streamUpdate = () => {
|
|
348
324
|
onUpdate?.({
|
|
@@ -382,11 +358,6 @@ Guidelines:
|
|
|
382
358
|
|
|
383
359
|
clearInterval(spinnerInterval);
|
|
384
360
|
|
|
385
|
-
if (fgId) {
|
|
386
|
-
agentActivity.delete(fgId);
|
|
387
|
-
widget.markFinished(fgId);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
361
|
const tokenText = safeFormatTokens(bgState.session);
|
|
391
362
|
const durationMs = (record.completedAt ?? Date.now()) - record.startedAt;
|
|
392
363
|
|