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 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 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).
@@ -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` (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
- - **Adaptive orchestration by default**: main decides whether delegation improves speed, quality, context management, independent judgment, or parallel progress, and also weighs latency, over-analysis, misunderstanding, duplication, and integration risk.
47
- - **Explicit opt-out**: `/orchestrate off` disables automatic delegation for the session; users can still explicitly request any subagent. `/orchestrate on` restores main-agent discretion.
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
- - `[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).
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
- |-- 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)
164
191
  |-- message router (500ms poll)
165
192
  | |-- to=main -> inject into main session -> reply_message replies
166
- | |-- to=subagent -> route to its resident process -> reply written back
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 by default)
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 (default)
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.
@@ -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
  }