pi-agent-squad 0.7.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 CHANGED
@@ -20,30 +20,54 @@ 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 its resident process.
24
- - File channel + polling (no process pipes, no identity required).
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).
28
- - `orchestrator.md`: defines the main agent's outcome-oriented Discover Decide Execute Verify workflow (optional injection).
29
+ - `orchestrator.md`: gives main adaptive authority to choose direct work or any useful combination of planner/actor/reviewer, while weighing each role's value and risks.
29
30
  - The bottom layer never cares who is who — it only delivers messages.
30
31
 
31
32
  ## Messaging tools (shared by all agents)
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` (the main agent) or any subagent name.
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 process pool.
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
- - **Default safety gate**: unless the current user explicitly requests subagent involvement or `/orchestrate` is enabled, the main-agent system prompt forbids subagent delegation.
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.
47
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.
48
72
  - **Reliable messaging**: `send_message(wait=true)` waits for and returns the target's actual reply; `wait=false` remains fire-and-forget.
49
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.
@@ -64,8 +88,10 @@ The TUI-only widget is installed above the editor while at least one subagent is
64
88
  ```
65
89
 
66
90
  - No suffix: synchronous `subagent` task.
67
- - `[bg]`: background `subagent(async=true)` task.
68
- - `[msg]`: resident task started through `send_message` or subagent-to-subagent routing.
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).
69
95
  - At most four activities are shown; additional concurrency is summarized as `… +N more`, and the visible window follows the selected activity.
70
96
  - Task summaries are dimmed and capped at 40 terminal columns so they do not dominate the widget.
71
97
  - The title includes dim keyboard hints. Before selection it shows
@@ -159,10 +185,12 @@ Main agent (primary outcome owner; optional specialist workflow defined by promp
159
185
  |
160
186
  |-- subagent tool (sync/background spawns an RPC-backed run session)
161
187
  | `-- widget selection / interactive overlay attach to that exact session
162
- |-- RPC resident process pool (receives inter-subagent messages)
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)
163
191
  |-- message router (500ms poll)
164
192
  | |-- to=main -> inject into main session -> reply_message replies
165
- | |-- to=subagent -> route to its resident process -> reply written back
193
+ | |-- to=subagent -> registry -> exact direct/resident process -> reply written back
166
194
  |
167
195
  Subagents (separate processes, child mode):
168
196
  |-- send_message / read_inbox / reply_message tools
@@ -182,20 +210,20 @@ subagents/
182
210
  |-- message.ts # generic messaging (file channel + send/reply/read + main-side router)
183
211
  |-- session.ts # common interactive session-handle interface
184
212
  |-- session-ui.ts # focused overlay for live transcript + interactive input
185
- |-- orchestrator.md # main-agent outcome/workflow prompt (optional injection)
213
+ |-- orchestrator.md # main-agent adaptive delegation prompt (enabled with /orchestrate on)
186
214
  `-- README.md
187
215
  ```
188
216
 
189
217
  ## Usage
190
218
 
191
219
  ```bash
192
- # optional: inject the main-agent specialist-workflow identity
220
+ # optional CLI equivalent of forcing adaptive orchestration on
193
221
  pi --append-system-prompt ~/.pi/agent/extensions/subagents/orchestrator.md
194
222
 
195
223
  # or in-session
196
- /orchestrate # enable orchestrator mode (injects the triage identity every turn)
197
- /orchestrate off # disable
198
- /orchestrate status # check state
224
+ /orchestrate # enable adaptive orchestration
225
+ /orchestrate off # require explicit user requests before using subagents
226
+ /orchestrate status # check adaptive orchestration state
199
227
 
200
228
  # in conversation
201
229
  "Resolve this architecture decision, then implement it" # planner is used when a Decision Brief is needed
@@ -203,3 +231,8 @@ pi --append-system-prompt ~/.pi/agent/extensions/subagents/orchestrator.md
203
231
  "Use subagent agent=reviewer timeoutSeconds=120 ..." # override the default 6h task timeout (only when the user asked)
204
232
  "Have reviewer review the recent changes" # main agent delegates to reviewer
205
233
  ```
234
+
235
+ Main is not required to follow a fixed planner → actor → reviewer chain. It may
236
+ work directly, use only actor for high-throughput implementation, ask planner
237
+ for a decision and implement it itself, or request reviewer only when an
238
+ independent pass is worth its latency and false-positive risk.
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.
@@ -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
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
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
- const content = fs.readFileSync(filePath, "utf-8");
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 = process.env.HOME ?? ""): string {
68
- return path.join(home, ".pi", "agent", "agents");
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
  }