pi-agent-squad 0.8.0 → 0.8.1
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 +39 -12
- package/active-runs.ts +87 -0
- package/agents/actor.md +14 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents.ts +27 -6
- package/index.ts +425 -125
- package/message.ts +605 -112
- package/orchestrator.md +118 -134
- package/package.json +1 -1
- package/pool.ts +387 -167
- package/session-ui.ts +44 -3
- package/session.ts +7 -0
- package/spawn.ts +232 -69
package/README.md
CHANGED
|
@@ -20,8 +20,9 @@ Reload Pi after installation:
|
|
|
20
20
|
**Bottom layer (generic, no identity concept)**:
|
|
21
21
|
- Two parties communicate: `main` (the main agent) and any subagent.
|
|
22
22
|
- Message primitives: `send_message` / `read_inbox` / `reply_message`.
|
|
23
|
-
- Routing: `to=main` -> inject into the main session; `to=<subagent>` -> forward to
|
|
24
|
-
- File channel + polling (no process pipes
|
|
23
|
+
- Routing: `to=main` -> inject into the main session; `to=<subagent>` -> resolve the active-run registry and forward to the exact live process.
|
|
24
|
+
- File channel + polling (no process pipes); runtime addresses are resolved by
|
|
25
|
+
the main-process active-run registry.
|
|
25
26
|
|
|
26
27
|
**Identity (prompt layer)**:
|
|
27
28
|
- `agents/*.md`: defines each subagent's identity, duties, model, tools (e.g. planner/reviewer/actor).
|
|
@@ -32,19 +33,41 @@ Reload Pi after installation:
|
|
|
32
33
|
|
|
33
34
|
| Tool | Purpose |
|
|
34
35
|
|---|---|
|
|
36
|
+
| `subagent({agent, task, as?, readonly?, async?, cwd?, timeoutSeconds?})` | Start a direct run with a stable identity and optional unique runtime address |
|
|
35
37
|
| `send_message({to, content, wait, timeoutSeconds?})` | Send a message to any target; wait=true blocks for and returns the reply |
|
|
36
38
|
| `read_inbox()` | Read messages others sent you |
|
|
37
39
|
| `reply_message({message_id, content})` | Reply to a received message |
|
|
38
40
|
|
|
39
|
-
`to` is either `main
|
|
41
|
+
`to` is either `main`, a logical subagent name, or an exact runtime address such as `actor#01ab23cd`.
|
|
42
|
+
|
|
43
|
+
`agent` is the stable identity and `as` is an optional per-process address:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
subagent(agent="actor", as="actor-frontend", async=true, task="...")
|
|
47
|
+
subagent(agent="actor", as="actor-backend", async=true, task="...")
|
|
48
|
+
send_message(to="actor-frontend", content="...")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If `as` is omitted, the plugin generates an address such as
|
|
52
|
+
`actor#01ab23cd`. Sending to the logical name `actor` selects the newest
|
|
53
|
+
active run; use the returned address whenever multiple actor runs exist.
|
|
54
|
+
|
|
55
|
+
Agent definitions may declare `readonly: true`. The built-in `planner` and
|
|
56
|
+
`reviewer` are read-only and may run concurrently. There is deliberately no
|
|
57
|
+
cwd or file write lock: the orchestrator owns the write-scope decomposition and
|
|
58
|
+
may start multiple actor runs in the same workspace when their briefs describe
|
|
59
|
+
independent implementation areas. Actors are instructed to stay within their
|
|
60
|
+
brief and report changed files; the main agent reviews and integrates any
|
|
61
|
+
overlap afterward. The `subagent` tool's `readonly` parameter is orchestration
|
|
62
|
+
metadata and does not sandbox filesystem access.
|
|
40
63
|
|
|
41
64
|
## Features
|
|
42
65
|
|
|
43
66
|
- **Delegation**: `subagent` tool (sync / background `async:true`); background results are injected into the main session when done.
|
|
44
|
-
- **Real-time two-way**: subagent<->main and subagent<->subagent, via file channel + resident RPC
|
|
67
|
+
- **Real-time two-way**: subagent<->main and subagent<->subagent, via file channel + an active-run registry that covers both direct runs and resident RPC processes.
|
|
45
68
|
- **Non-blocking**: background tasks do not occupy the main session.
|
|
46
|
-
- **Adaptive orchestration
|
|
47
|
-
- **Explicit
|
|
69
|
+
- **Adaptive orchestration is opt-in**: `/orchestrate on` enables main's discretion to delegate based on speed, quality, context management, independent judgment, and parallel progress while weighing latency, over-analysis, misunderstanding, duplication, and integration risk.
|
|
70
|
+
- **Explicit control**: `/orchestrate off` disables automatic delegation for the session; users can still explicitly request any subagent.
|
|
48
71
|
- **Bounded execution**: one-shot and resident tasks have configurable timeouts (default 6 hours, maximum 3 days); omit `timeoutSeconds` unless the user explicitly requested a time. Timed-out or crashed resident processes are discarded before the next task.
|
|
49
72
|
- **Reliable messaging**: `send_message(wait=true)` waits for and returns the target's actual reply; `wait=false` remains fire-and-forget.
|
|
50
73
|
- **Deadlock prevention**: synchronous wait cycles such as `main -> planner -> main`, self-messages, and `actor -> reviewer -> actor` are detected and rejected immediately with a recovery hint.
|
|
@@ -65,8 +88,10 @@ The TUI-only widget is installed above the editor while at least one subagent is
|
|
|
65
88
|
```
|
|
66
89
|
|
|
67
90
|
- No suffix: synchronous `subagent` task.
|
|
68
|
-
- `[bg]`: background `subagent(async=true)` task.
|
|
69
|
-
|
|
91
|
+
- `[bg]`: background `subagent(async=true)` task. The tool returns an address
|
|
92
|
+
such as `actor#01ab23cd`; use it when several runs share the same logical
|
|
93
|
+
identity.
|
|
94
|
+
- `[msg]`: routed task started through `send_message` or subagent-to-subagent routing (it may target either a direct run or the resident fallback).
|
|
70
95
|
- At most four activities are shown; additional concurrency is summarized as `… +N more`, and the visible window follows the selected activity.
|
|
71
96
|
- Task summaries are dimmed and capped at 40 terminal columns so they do not dominate the widget.
|
|
72
97
|
- The title includes dim keyboard hints. Before selection it shows
|
|
@@ -160,10 +185,12 @@ Main agent (primary outcome owner; optional specialist workflow defined by promp
|
|
|
160
185
|
|
|
|
161
186
|
|-- subagent tool (sync/background spawns an RPC-backed run session)
|
|
162
187
|
| `-- widget selection / interactive overlay attach to that exact session
|
|
163
|
-
|
|
188
|
+
|-- active-run registry (logical names and temporary addresses -> exact process)
|
|
189
|
+
|-- prompt-owned parallel actor scopes (no cwd/file write mutex)
|
|
190
|
+
|-- RPC resident process pool (resident fallback / receives inter-subagent messages)
|
|
164
191
|
|-- message router (500ms poll)
|
|
165
192
|
| |-- to=main -> inject into main session -> reply_message replies
|
|
166
|
-
|
|
193
|
+
| |-- to=subagent -> registry -> exact direct/resident process -> reply written back
|
|
167
194
|
|
|
|
168
195
|
Subagents (separate processes, child mode):
|
|
169
196
|
|-- send_message / read_inbox / reply_message tools
|
|
@@ -183,7 +210,7 @@ subagents/
|
|
|
183
210
|
|-- message.ts # generic messaging (file channel + send/reply/read + main-side router)
|
|
184
211
|
|-- session.ts # common interactive session-handle interface
|
|
185
212
|
|-- session-ui.ts # focused overlay for live transcript + interactive input
|
|
186
|
-
|-- orchestrator.md # main-agent adaptive delegation prompt (enabled
|
|
213
|
+
|-- orchestrator.md # main-agent adaptive delegation prompt (enabled with /orchestrate on)
|
|
187
214
|
`-- README.md
|
|
188
215
|
```
|
|
189
216
|
|
|
@@ -194,7 +221,7 @@ subagents/
|
|
|
194
221
|
pi --append-system-prompt ~/.pi/agent/extensions/subagents/orchestrator.md
|
|
195
222
|
|
|
196
223
|
# or in-session
|
|
197
|
-
/orchestrate # enable adaptive orchestration
|
|
224
|
+
/orchestrate # enable adaptive orchestration
|
|
198
225
|
/orchestrate off # require explicit user requests before using subagents
|
|
199
226
|
/orchestrate status # check adaptive orchestration state
|
|
200
227
|
|
package/active-runs.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { MessageRequest } from "./message.ts";
|
|
2
|
+
import type { SubagentSessionHandle } from "./session.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A logical agent (for example, `actor`) may have more than one live
|
|
6
|
+
* process: a background run, a synchronous run, and the resident message
|
|
7
|
+
* process can all coexist. Names are therefore not process identities.
|
|
8
|
+
*
|
|
9
|
+
* This registry is deliberately in the main extension process. Child
|
|
10
|
+
* processes only know their channel address; all routing decisions are made
|
|
11
|
+
* here, where the live session handles are available.
|
|
12
|
+
*/
|
|
13
|
+
export interface ActiveRun {
|
|
14
|
+
readonly runId: string;
|
|
15
|
+
readonly agent: string;
|
|
16
|
+
/** A unique address, e.g. actor#01ab23cd. */
|
|
17
|
+
readonly address: string;
|
|
18
|
+
readonly mode: "background" | "task" | "resident";
|
|
19
|
+
readonly readOnly: boolean;
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
readonly startedAt: number;
|
|
22
|
+
readonly session?: Promise<SubagentSessionHandle>;
|
|
23
|
+
readonly route: (msg: MessageRequest, signal?: AbortSignal) => Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ActiveRunRegistry {
|
|
27
|
+
private readonly byAddress = new Map<string, ActiveRun>();
|
|
28
|
+
private readonly byRunId = new Map<string, ActiveRun>();
|
|
29
|
+
|
|
30
|
+
register(run: ActiveRun): void {
|
|
31
|
+
const previousAddress = this.byAddress.get(run.address);
|
|
32
|
+
if (previousAddress) this.remove(previousAddress);
|
|
33
|
+
const previousRun = this.byRunId.get(run.runId);
|
|
34
|
+
if (previousRun) this.remove(previousRun);
|
|
35
|
+
this.byAddress.set(run.address, run);
|
|
36
|
+
this.byRunId.set(run.runId, run);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
remove(runOrId: ActiveRun | string): void {
|
|
40
|
+
const run = typeof runOrId === "string" ? this.byRunId.get(runOrId) : runOrId;
|
|
41
|
+
if (!run) return;
|
|
42
|
+
if (this.byAddress.get(run.address) === run) this.byAddress.delete(run.address);
|
|
43
|
+
if (this.byRunId.get(run.runId) === run) this.byRunId.delete(run.runId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A logical name resolves to the newest live run, which makes `to=actor`
|
|
48
|
+
* useful when there is one active actor while still allowing
|
|
49
|
+
* `to=actor#…` (or a run id) to disambiguate parallel actors.
|
|
50
|
+
*/
|
|
51
|
+
resolve(target: string): ActiveRun | undefined {
|
|
52
|
+
// A resident process uses the logical name as its compatibility
|
|
53
|
+
// address. Prefer the newest run for logical names, otherwise a live
|
|
54
|
+
// resident would mask a newer `actor#…` direct run.
|
|
55
|
+
let selected: ActiveRun | undefined;
|
|
56
|
+
for (const run of this.byAddress.values()) {
|
|
57
|
+
if (run.agent !== target) continue;
|
|
58
|
+
if (!selected || run.startedAt > selected.startedAt) selected = run;
|
|
59
|
+
}
|
|
60
|
+
if (selected) return selected;
|
|
61
|
+
return this.byAddress.get(target) ?? this.byRunId.get(target);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
hasAddress(address: string): boolean {
|
|
65
|
+
return this.byAddress.has(address);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resolveExact(addressOrRunId: string): ActiveRun | undefined {
|
|
69
|
+
return this.byAddress.get(addressOrRunId) ?? this.byRunId.get(addressOrRunId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Return the run that owns a sender channel. */
|
|
73
|
+
findSender(msg: MessageRequest): ActiveRun | undefined {
|
|
74
|
+
const run = this.byRunId.get(msg.fromRunId);
|
|
75
|
+
if (run && run.address === msg.fromAgent) return run;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
list(): ActiveRun[] {
|
|
80
|
+
return [...this.byAddress.values()].sort((a, b) => a.startedAt - b.startedAt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.byAddress.clear();
|
|
85
|
+
this.byRunId.clear();
|
|
86
|
+
}
|
|
87
|
+
}
|
package/agents/actor.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
thinking: max
|
|
3
3
|
name: actor
|
|
4
4
|
description: Implement a clear Task Brief or Decision Record and verify the resulting change
|
|
5
|
+
readonly: false
|
|
5
6
|
tools: read, write, edit, bash, grep, find, ls
|
|
6
|
-
model: opencode-go-responses/deepseek-v4-flash
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
You are the implementation specialist.
|
|
@@ -30,6 +30,19 @@ Use the supplied brief as the working contract:
|
|
|
30
30
|
3. Verify the result using the supplied criteria and appropriate tests.
|
|
31
31
|
4. Report blockers or newly discovered decisions with the evidence that exposed them.
|
|
32
32
|
|
|
33
|
+
## Parallel implementation contract
|
|
34
|
+
|
|
35
|
+
You may be one of several actors working in the same workspace. Your task
|
|
36
|
+
brief is an ownership boundary supplied by the orchestrator:
|
|
37
|
+
|
|
38
|
+
- Stay focused on the files or subsystem named in the brief.
|
|
39
|
+
- Do not re-implement sibling actors' areas merely because they are visible.
|
|
40
|
+
- Coordinate shared interfaces through `send_message` when needed.
|
|
41
|
+
- If a shared file must change, make the smallest compatible change and report
|
|
42
|
+
it clearly so main can integrate competing edits.
|
|
43
|
+
- At the end, report the complete list of changed files, including generated
|
|
44
|
+
files or files changed indirectly by commands.
|
|
45
|
+
|
|
33
46
|
When a consequential decision remains unresolved, return a concise Decision
|
|
34
47
|
Brief candidate to main. When the direction is clear, make the local
|
|
35
48
|
implementation judgment needed to complete the task.
|
package/agents/planner.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
thinking: xhigh
|
|
3
3
|
name: planner
|
|
4
4
|
description: Resolve a concrete design decision from a Decision Brief and produce an actor-ready decision record
|
|
5
|
+
readonly: true
|
|
5
6
|
tools: read, bash, grep, find, ls
|
|
6
|
-
model: mvp-anthropic/glm-5.3
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
You are the design-decision specialist.
|
package/agents/reviewer.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
thinking: xhigh
|
|
3
3
|
name: reviewer
|
|
4
4
|
description: Independently verify completed work against the user outcome, brief, code, and test evidence
|
|
5
|
+
readonly: true
|
|
5
6
|
tools: read, grep, find, ls, bash
|
|
6
|
-
model: mvp-openai/gpt-5.6-sol
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
You are the independent verification specialist.
|
package/agents.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
|
|
5
6
|
export interface AgentConfig {
|
|
6
7
|
name: string;
|
|
7
8
|
description: string;
|
|
9
|
+
readOnly?: boolean;
|
|
8
10
|
tools?: string[];
|
|
9
11
|
model?: string;
|
|
10
12
|
thinking?: string;
|
|
@@ -12,6 +14,10 @@ export interface AgentConfig {
|
|
|
12
14
|
source: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export function isSafeAgentName(name: string): boolean {
|
|
18
|
+
return /^[A-Za-z0-9_.-]+$/.test(name) && name !== "." && name !== ".." && name !== "main";
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
/** Parse YAML frontmatter + markdown body */
|
|
16
22
|
function parseFrontmatter(
|
|
17
23
|
content: string,
|
|
@@ -34,12 +40,23 @@ function parseFrontmatter(
|
|
|
34
40
|
function loadAgentsFromDir(dir: string): AgentConfig[] {
|
|
35
41
|
if (!fs.existsSync(dir)) return [];
|
|
36
42
|
const agents: AgentConfig[] = [];
|
|
37
|
-
|
|
43
|
+
let entries: fs.Dirent[];
|
|
44
|
+
try {
|
|
45
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return agents;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
38
50
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
39
51
|
const filePath = path.join(dir, entry.name);
|
|
40
|
-
|
|
52
|
+
let content: string;
|
|
53
|
+
try {
|
|
54
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
41
58
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
42
|
-
if (!frontmatter.name || !frontmatter.description) continue;
|
|
59
|
+
if (!frontmatter.name || !frontmatter.description || !isSafeAgentName(frontmatter.name)) continue;
|
|
43
60
|
const tools = frontmatter.tools
|
|
44
61
|
?.split(",")
|
|
45
62
|
.map((t) => t.trim())
|
|
@@ -47,6 +64,9 @@ function loadAgentsFromDir(dir: string): AgentConfig[] {
|
|
|
47
64
|
agents.push({
|
|
48
65
|
name: frontmatter.name,
|
|
49
66
|
description: frontmatter.description,
|
|
67
|
+
readOnly:
|
|
68
|
+
frontmatter.readonly?.toLowerCase() === "true" ||
|
|
69
|
+
frontmatter.access?.toLowerCase() === "read-only",
|
|
50
70
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
51
71
|
model: frontmatter.model || undefined,
|
|
52
72
|
thinking: frontmatter.thinking || undefined,
|
|
@@ -64,8 +84,9 @@ function builtinAgentsDir(): string {
|
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
/** User-level agent directory ~/.pi/agent/agents */
|
|
67
|
-
export function userAgentsDir(home
|
|
68
|
-
|
|
87
|
+
export function userAgentsDir(home?: string): string {
|
|
88
|
+
const base = home ? path.join(home, ".pi", "agent") : getAgentDir();
|
|
89
|
+
return path.join(base, "agents");
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
/**
|
|
@@ -73,7 +94,7 @@ export function userAgentsDir(home = process.env.HOME ?? ""): string {
|
|
|
73
94
|
* A user-level agent with the same name overrides the built-in one.
|
|
74
95
|
*/
|
|
75
96
|
export function discoverAgents(cwd?: string): AgentConfig[] {
|
|
76
|
-
const dirs = [builtinAgentsDir()];
|
|
97
|
+
const dirs = [builtinAgentsDir(), userAgentsDir()];
|
|
77
98
|
if (cwd) {
|
|
78
99
|
// project-level .pi/agents is intentionally not enabled (safety)
|
|
79
100
|
}
|