@ferris1225/pi-subagents 0.5.0 → 0.7.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/README.md CHANGED
@@ -1,167 +1,177 @@
1
- # pi-subagents
2
-
3
- [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
- [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
- [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
- ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
- ![pi](https://img.shields.io/badge/pi-extension-orange)
8
-
9
- English | [中文](./README-zh.md)
10
-
11
- A focused [pi](https://pi.dev) extension that gives the main model **sub-agents it will
12
- actually use**: `explore`, `worker`, and `reviewer` (plus an opt-in `plan`), each running
13
- in an isolated `pi` process. The differentiator is not the agents themselves — it is the
14
- **proactive dispatch injection** that makes the model delegate on its own, so you can
15
- delete the dispatch/review rules from your global `AGENTS.md`.
16
-
17
- ## Why pi-subagents?
18
-
19
- Pi ships no sub-agents on purpose. The community fills the gap two ways, and both miss:
20
-
21
- - **Too heavy** — frameworks with 9 agents, chain pipelines, worktree swarms, and a
22
- slash-command for everything. Powerful, but a lot of machinery to carry.
23
- - **Too quiet** a bare `subagent` tool that the model *rarely calls*, because pi only
24
- shows the parent model the tool, never the per-agent descriptions. So the agents sit
25
- idle unless you force them in a global prompt.
26
-
27
- `pi-subagents` takes the middle path:
28
-
29
- | Advantage | What it means for you |
30
- |-----------|----------------------|
31
- | **Actually gets used** | A `before_agent_start` hook injects the agent catalog + a dispatch/review directive into the system prompt every turn, reinforced by tool `promptGuidelines` and `Use PROACTIVELY when …` descriptions. This is the lever the heavy frameworks rely on too — we just make it the default. |
32
- | **Right-sized** | 3 focused agents (+1 opt-in), not 9. No chain/worktree/swarm machinery. Single and parallel modes only. |
33
- | **Replaces your AGENTS.md rules** | The injected directive is a self-contained replacement for the "Sub-agent Dispatch" and "Review, Verification & Commit" sections. Install it, then delete those sections. |
34
- | **True isolation** | Each agent is a separate `pi` process (`--no-session`), so delegated work never pollutes the main context. |
35
- | **Read-only where it matters** | `explore`, `plan`, and `reviewer` are read-only. The `reviewer` runs in a *separate* context to avoid self-confirmation bias. |
36
- | **Selection-only setup** | No typing of values: a checkbox module picker and a fuzzy-filter, paginated model picker. |
37
- | **Sensible model defaults** | Per-agent model override; if you skip one, it uses the **main session's current model**. Unavailable saved overrides are repaired and persisted automatically. |
38
- | **Leaf sub-agents** | Child processes never receive the `subagent` tool, so delegation cannot recurse or run away. |
39
- | **Zero runtime deps** | Pure pi extension, peer dependencies only, no build step. |
40
-
41
- ## Install
42
-
43
- ```bash
44
- pi install npm:@ferris1225/pi-subagents
45
- ```
46
-
47
- Requires pi **≥ 0.80.6** — sub-agent thinking levels use the `--thinking` values
48
- introduced by that version.
49
-
50
- Then run the setup wizard (selection-only):
51
-
52
- ```text
53
- /subagents-setup
54
- ```
55
-
56
- ## Agents
57
-
58
- | Agent | Default | Tools | Role |
59
- |-------|:-------:|-------|------|
60
- | `explore` | ✅ | read-only | Fast codebase reconnaissance; returns compressed findings for handoff. |
61
- | `worker` | ✅ | all | Implements / fixes / refactors / tests a self-contained task. **Plans internally.** |
62
- | `reviewer` | ✅ | read-only | Adversarial pre-commit review in a separate context. |
63
- | `plan` | opt-in | read-only | A separate, human-reviewable implementation plan. A worker already plans internally, so this is only for when you want the plan as its own artifact. |
64
-
65
- Each agent is a Markdown file (`agents/*.md`: YAML frontmatter + body as system prompt).
66
- Override any of them by dropping a file with the same `name` into `~/.pi/agent/agents/`
67
- (user) or `.pi/agents/` (project).
68
-
69
- ## How proactive dispatch works
70
-
71
- Pi never shows the parent model the per-agent descriptions — it only sees the `subagent`
72
- tool. Three levers fix that:
73
-
74
- 1. **`before_agent_start` injection** — every turn, the enabled agents plus a
75
- dispatch/review directive are appended to the parent system prompt.
76
- 2. **Tool `promptSnippet` / `promptGuidelines`** — reinforce "when to delegate" whenever
77
- the tool is active.
78
- 3. **`Use PROACTIVELY when …`** descriptions the trigger phrasing proven across the
79
- Claude Code agent ecosystem.
80
-
81
- The directive encourages a clean flow: **`explore` → `worker` → `reviewer`**, parallel
82
- fan-out for independent tasks, and trust-but-verify handoffs.
83
-
84
- ## Configuration
85
-
86
- Stored at `~/.pi/agent/pi-subagents.json` (honors `PI_CODING_AGENT_DIR`):
87
-
88
- ```json
89
- {
90
- "enabledAgents": ["explore", "worker", "reviewer"],
91
- "agentModels": { "explore": "anthropic/claude-haiku-4-5" },
92
- "thinkingLevel": "max",
93
- "proactiveInjection": true,
94
- "agentScope": "user"
95
- }
96
- ```
97
-
98
- - `enabledAgents` — which agents are discoverable and injected.
99
- - `agentModels` — per-agent model override (`"provider/model-id"`). If a saved model is unavailable, it is replaced with the current main-window model and written back to this file.
100
- - `thinkingLevel` sub-agent reasoning strength: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` (default).
101
- - `proactiveInjection` — toggle the system-prompt injection.
102
- - `agentScope` — `"user"` (default), `"project"`, or `"both"`.
103
-
104
- **Model precedence** for each agent:
105
-
106
- ```
107
- available agentModels[name] → current session model → the agent's frontmatter default
108
- ```
109
-
110
- If a configured model is no longer available, it is switched to the current main-window model and persisted before the next run.
111
-
112
- ## Usage
113
-
114
- The main model calls `subagent` on its own, but you can also ask directly:
115
-
116
- ```text
117
- # single
118
- Use the explore sub-agent to map how authentication is wired up.
119
-
120
- # parallel (independent tasks)
121
- Run these in parallel sub-agents: explore the API layer, and explore the DB layer.
122
- ```
123
-
124
- Tool shape:
125
-
126
- ```jsonc
127
- // single
128
- { "agent": "worker", "task": "<self-contained brief>" }
129
- // parallel
130
- { "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
131
- ```
132
-
133
- ## Live status & notifications
134
-
135
- While sub-agents run, a widget above the editor shows one line per run — status
136
- icon, agent, model, token usage, elapsed time — plus a second, indented line
137
- with what the agent is doing right now: `thinking`, `writing`,
138
- `read src/index.ts`, `bash npm test`, … (never a raw JSON args blob).
139
-
140
- When a run finishes (done **or** failed), its row disappears from the widget and
141
- the main window gets a notification with the final summary
142
- (`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`). The tool result itself
143
- remains the durable record in the conversation.
144
-
145
- Sub-agents use the configured thinking level (default `--thinking max`);
146
- pi clamps it adaptively to what the resolved model supports
147
- (`max → xhigh → high → … → off`), so weaker models degrade gracefully.
148
- The task is sent through stdin; only the agent system prompt uses a short-lived
149
- file. Child output is streamed in memory, with a watchdog and process-tree cleanup
150
- for aborted or stuck runs.
151
-
152
- ## Development
153
-
154
- ```bash
155
- npm install
156
- npm run check # tsc --noEmit
157
- npm test # vitest
158
- ```
159
-
160
- ## See also
161
-
162
- - [pi-querit-search](https://www.npmjs.com/package/pi-querit-search) live web search &
163
- page fetching for pi, by the same author.
164
-
165
- ## License
166
-
167
- MIT
1
+ # pi-subagents
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
+ [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
+ [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
+ ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
+ ![pi](https://img.shields.io/badge/platform-pi-orange)
8
+
9
+ Focused background delegation for [pi](https://pi.dev). `pi-subagents` adds a small set of
10
+ specialized agents that run in isolated child processes, report results back to the main
11
+ agent, and keep the workflow moving without manual polling.
12
+
13
+ ## Highlights
14
+
15
+ - **Automatic delegation guidance** — injects the enabled agent catalog and routing rules into
16
+ the main agent's system prompt.
17
+ - **Isolated execution** — every sub-agent runs in its own `pi` process with `--no-session`.
18
+ - **Automatic continuation** — a completed result is sent to the main session as a custom
19
+ message and automatically starts a follow-up turn. If the main agent is busy, the result
20
+ waits in the follow-up queue.
21
+ - **Parallel fan-out** — run independent tasks together, with a bounded background queue.
22
+ - **Live progress** a TUI widget shows each agent's status, activity, model, usage, and
23
+ elapsed time; completion also produces a concise notification.
24
+ - **Per-agent configuration** enable agents, select models, set thinking strength, and
25
+ choose discovery scope from `/subagents-setup`.
26
+ - **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
27
+ recurse.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pi install npm:@ferris1225/pi-subagents
33
+ ```
34
+
35
+ Requires pi **>= 0.80.6**.
36
+
37
+ After installation, open the setup wizard in an interactive TUI session:
38
+
39
+ ```text
40
+ /subagents-setup
41
+ ```
42
+
43
+ The default configuration enables `explore`, `worker`, and `reviewer`. `plan` is available
44
+ but opt-in.
45
+
46
+ ## Included agents
47
+
48
+ | Agent | Default | Access | Purpose |
49
+ | --- | :---: | --- | --- |
50
+ | `explore` | Yes | Read-only | Fast codebase reconnaissance and structured findings. |
51
+ | `worker` | Yes | Full | Implements, fixes, refactors, and tests a self-contained task. |
52
+ | `reviewer` | Yes | Read-only | Independent adversarial review of a diff before completion. |
53
+ | `plan` | No | Read-only | Produces a separate implementation plan when one is useful. |
54
+
55
+ Agents are Markdown files in `agents/`. Each file contains YAML frontmatter and a system
56
+ prompt. User and project scopes can override a built-in agent with the same name.
57
+
58
+ ## Workflow
59
+
60
+ A typical flow is:
61
+
62
+ ```text
63
+ main agent
64
+
65
+ ├─ subagent(explore / worker / reviewer)
66
+ │ └─ isolated pi child process
67
+ │ └─ result message
68
+
69
+ └─ automatic follow-up turn with the result
70
+ ```
71
+
72
+ 1. The main agent calls `subagent` with a self-contained brief.
73
+ 2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
74
+ for input.
75
+ 3. The child process works independently. Up to four queued runs execute at once; a single
76
+ parallel request may contain up to eight tasks.
77
+ 4. On completion or failure, the extension sends a durable result message to the main
78
+ session. That message automatically wakes the main agent, or waits until its current turn
79
+ finishes.
80
+ 5. The main agent uses the result to verify the work and continue dependent steps. No later
81
+ user prompt is required to collect a result.
82
+
83
+ Switching sessions, reloading, or shutting down cancels remaining background runs.
84
+
85
+ ## Usage
86
+
87
+ The main agent is encouraged to delegate automatically, but you can also ask directly:
88
+
89
+ ```text
90
+ Use explore to map how authentication is wired up.
91
+ Ask worker to implement the API change after the exploration is complete.
92
+ Run reviewer on the final diff before reporting completion.
93
+ ```
94
+
95
+ ### Single task
96
+
97
+ ```json
98
+ {
99
+ "agent": "worker",
100
+ "task": "Implement the requested change. Inspect the existing conventions, update tests, and report the files changed and checks run."
101
+ }
102
+ ```
103
+
104
+ Optional `cwd` selects the working directory for that child.
105
+
106
+ ### Parallel tasks
107
+
108
+ Use parallel mode only for independent work:
109
+
110
+ ```json
111
+ {
112
+ "tasks": [
113
+ { "agent": "explore", "task": "Map the API layer and its tests." },
114
+ { "agent": "explore", "task": "Map the database layer and its tests." }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ Start dependent work after the relevant result has been delivered to the main agent.
120
+
121
+ ## Configuration
122
+
123
+ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
124
+ `PI_CODING_AGENT_DIR` when set.
125
+
126
+ ```json
127
+ {
128
+ "enabledAgents": ["explore", "worker", "reviewer"],
129
+ "agentModels": {
130
+ "explore": "anthropic/claude-haiku-4-5"
131
+ },
132
+ "thinkingLevel": "max",
133
+ "proactiveInjection": true,
134
+ "agentScope": "user"
135
+ }
136
+ ```
137
+
138
+ | Field | Description |
139
+ | --- | --- |
140
+ | `enabledAgents` | Agent names exposed to discovery and prompt injection. An empty array disables all agents. |
141
+ | `agentModels` | Optional `provider/model-id` override per agent. |
142
+ | `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
143
+ | `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
144
+ | `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
145
+
146
+ Model selection uses this precedence:
147
+
148
+ ```text
149
+ configured agent model current main-session model agent frontmatter model
150
+ ```
151
+
152
+ Unavailable configured models are replaced with a usable current-session model when possible
153
+ and the repaired configuration is saved.
154
+
155
+ ## Agent discovery and overrides
156
+
157
+ - Built-in agents are shipped with the package.
158
+ - User agents live in `~/.pi/agent/agents/`.
159
+ - Project agents live in the nearest `.pi/agents/` directory.
160
+ - For duplicate names, project overrides user and user overrides built-in.
161
+
162
+ Use a matching Markdown filename and `name` field to replace a built-in agent. Keep the task
163
+ brief explicit: include the goal, relevant paths, constraints, and expected handoff.
164
+
165
+ ## Development
166
+
167
+ ```bash
168
+ npm install
169
+ npm run check
170
+ npm test
171
+ ```
172
+
173
+ The package has no runtime dependencies beyond pi peer dependencies.
174
+
175
+ ## License
176
+
177
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ferris1225/pi-subagents",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Focused sub-agent delegation for pi: explore / plan / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,6 @@
18
18
  "src",
19
19
  "agents",
20
20
  "README.md",
21
- "README-zh.md",
22
21
  "LICENSE"
23
22
  ],
24
23
  "pi": {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Bounded background task scheduler.
3
+ *
4
+ * Tasks get their own AbortSignal rather than inheriting the foreground agent
5
+ * turn's signal. The owning extension cancels all work only on session teardown.
6
+ */
7
+
8
+ export type BackgroundTask = (signal: AbortSignal) => Promise<void>;
9
+
10
+ interface PendingTask {
11
+ task: BackgroundTask;
12
+ controller: AbortController;
13
+ onCancelled?: () => void;
14
+ }
15
+
16
+ export class BackgroundTaskQueue {
17
+ private readonly concurrency: number;
18
+ private readonly pending: PendingTask[] = [];
19
+ private readonly active = new Set<AbortController>();
20
+ private stopped = false;
21
+
22
+ constructor(concurrency: number) {
23
+ this.concurrency = Math.max(1, concurrency);
24
+ }
25
+
26
+ enqueue(task: BackgroundTask, onCancelled?: () => void): AbortController {
27
+ const controller = new AbortController();
28
+ if (this.stopped) {
29
+ controller.abort();
30
+ onCancelled?.();
31
+ return controller;
32
+ }
33
+
34
+ this.pending.push({ task, controller, onCancelled });
35
+ this.drain();
36
+ return controller;
37
+ }
38
+
39
+ /** Stop queued work and request cancellation for running work. */
40
+ cancelAll(): void {
41
+ if (this.stopped) return;
42
+ this.stopped = true;
43
+
44
+ for (const entry of this.pending.splice(0)) {
45
+ entry.controller.abort();
46
+ entry.onCancelled?.();
47
+ }
48
+ for (const controller of this.active) controller.abort();
49
+ }
50
+
51
+ private drain(): void {
52
+ while (!this.stopped && this.active.size < this.concurrency) {
53
+ const entry = this.pending.shift();
54
+ if (!entry) return;
55
+ if (entry.controller.signal.aborted) {
56
+ entry.onCancelled?.();
57
+ continue;
58
+ }
59
+
60
+ this.active.add(entry.controller);
61
+ void entry.task(entry.controller.signal).catch(() => undefined).finally(() => {
62
+ this.active.delete(entry.controller);
63
+ this.drain();
64
+ });
65
+ }
66
+ }
67
+ }
package/src/index.ts CHANGED
@@ -13,11 +13,11 @@
13
13
  */
14
14
 
15
15
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
16
- import { StringEnum } from "@earendil-works/pi-ai";
17
16
  import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
17
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
19
18
  import { Type } from "typebox";
20
19
  import { discoverAgents, type AgentConfig } from "./agents.ts";
20
+ import { BackgroundTaskQueue } from "./background.ts";
21
21
  import { getConfigPath, loadConfig, saveConfig } from "./config.ts";
22
22
  import { repairUnavailableModelOverrides } from "./models.ts";
23
23
  import { buildDelegationDirective } from "./prompt.ts";
@@ -30,9 +30,7 @@ import {
30
30
  getFinalOutput,
31
31
  getResultOutput,
32
32
  isFailedResult,
33
- mapWithConcurrencyLimit,
34
33
  runSingleAgent,
35
- type OnUpdateCallback,
36
34
  type SingleResult,
37
35
  type SubagentDetails,
38
36
  type SubagentLiveEvent,
@@ -57,6 +55,32 @@ function emptyUsage(): UsageStats {
57
55
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
58
56
  }
59
57
 
58
+ function queuedResult(agent: AgentConfig, task: string): SingleResult {
59
+ return {
60
+ agent: agent.name,
61
+ agentSource: agent.source,
62
+ task,
63
+ exitCode: -1,
64
+ messages: [],
65
+ stderr: "",
66
+ usage: emptyUsage(),
67
+ model: agent.model,
68
+ };
69
+ }
70
+
71
+ function failedStartResult(agentName: string, task: string, errorMessage: string): SingleResult {
72
+ return {
73
+ agent: agentName,
74
+ agentSource: "unknown",
75
+ task,
76
+ exitCode: 1,
77
+ messages: [],
78
+ stderr: errorMessage,
79
+ usage: emptyUsage(),
80
+ errorMessage,
81
+ };
82
+ }
83
+
60
84
  function aggregateUsage(results: SingleResult[]): UsageStats {
61
85
  const total = emptyUsage();
62
86
  for (const r of results) {
@@ -88,6 +112,8 @@ function formatUsage(usage: UsageStats): string {
88
112
 
89
113
  export default function (pi: ExtensionAPI): void {
90
114
  const configPath = getConfigPath(getAgentDir());
115
+ const backgroundQueue = new BackgroundTaskQueue(MAX_CONCURRENCY);
116
+ let sessionActive = true;
91
117
 
92
118
  // Recursion guard: child sub-agents are leaf processes and cannot delegate again.
93
119
  if (currentSubagentDepth() >= MAX_SUBAGENT_DEPTH) {
@@ -100,6 +126,19 @@ export default function (pi: ExtensionAPI): void {
100
126
  return;
101
127
  }
102
128
 
129
+ pi.registerMessageRenderer("subagent-result", (message, _options, theme) =>
130
+ new Text(
131
+ `${theme.fg("toolTitle", theme.bold("subagent result"))}\n${message.content}`,
132
+ 0,
133
+ 0,
134
+ ),
135
+ );
136
+
137
+ pi.on("session_shutdown", () => {
138
+ sessionActive = false;
139
+ backgroundQueue.cancelAll();
140
+ });
141
+
103
142
  pi.registerTool({
104
143
  name: "subagent",
105
144
  label: "Subagent",
@@ -107,17 +146,18 @@ export default function (pi: ExtensionAPI): void {
107
146
  "Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
108
147
  "Agents: explore (read-only codebase recon), plan (implementation plan, opt-in), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
109
148
  "Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
110
- "Use it to keep the main conversation clean: delegate the work, then orchestrate and verify the results yourself.",
111
- "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output).",
149
+ "It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
150
+ "Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
112
151
  ].join(" "),
113
152
  promptSnippet:
114
- "Delegate discrete tasks to isolated sub-agents: explore (read-only search), worker (implement), reviewer (adversarial pre-commit review); plan is opt-in.",
153
+ "Start background subagents: explore (read-only search), worker (implement), reviewer (adversarial review); completion automatically resumes the main agent.",
115
154
  promptGuidelines: [
116
155
  "Use subagent to delegate discrete, self-contained tasks so the main context stays clean; do orchestration and verification yourself.",
117
156
  "Use subagent with agent 'explore' for broad or open-ended code search before large changes.",
118
157
  "Use subagent with agent 'worker' to implement a well-scoped task; it plans internally.",
119
158
  "Use subagent with agent 'reviewer' for a fresh read-only review before reporting work done or committing.",
120
- "Run independent tasks in parallel by passing a tasks array to subagent; keep dependent work sequential.",
159
+ "subagent launches work in the background and ends the current turn; when a result arrives, the main agent is automatically resumed with it.",
160
+ "Run independent tasks in parallel by passing a tasks array to subagent; let the automatically resumed main agent start dependent work after results arrive.",
121
161
  ],
122
162
  parameters: SubagentParams,
123
163
 
@@ -143,12 +183,13 @@ export default function (pi: ExtensionAPI): void {
143
183
  }
144
184
  }
145
185
 
146
- // Finished runs leave the widget immediately; the main window gets a
147
- // notification instead (the tool result remains the durable record).
186
+ // Finished runs leave the widget immediately. Their final findings are sent
187
+ // back as a custom message that automatically starts a follow-up turn.
148
188
  const finishRun = (runId: number, status: "done" | "failed"): void => {
149
189
  monitor.setStatus(runId, status); // stamps endedAt for the elapsed time
150
190
  const run = monitor.removeRun(runId);
151
191
  if (!run) return; // already finished — stay idempotent
192
+ if (!sessionActive) return;
152
193
  const icon = status === "done" ? "✓" : "✗";
153
194
  ctx.ui.notify(`${icon} ${monitor.summarize(run)}`, status === "done" ? "info" : "error");
154
195
  };
@@ -174,7 +215,8 @@ export default function (pi: ExtensionAPI): void {
174
215
  monitor.setActivity(runId, "thinking");
175
216
  break;
176
217
  case "text":
177
- monitor.setActivity(runId, "writing");
218
+ // A text delta is model output, not a filesystem write.
219
+ monitor.setActivity(runId, "responding");
178
220
  break;
179
221
  }
180
222
  };
@@ -194,8 +236,8 @@ export default function (pi: ExtensionAPI): void {
194
236
  const hasSingle = Boolean(params.agent && params.task);
195
237
 
196
238
  const makeDetails =
197
- (mode: "single" | "parallel") =>
198
- (results: SingleResult[]): SubagentDetails => ({ mode, results });
239
+ (mode: "single" | "parallel", background = false) =>
240
+ (results: SingleResult[]): SubagentDetails => ({ mode, results, background });
199
241
 
200
242
  const catalog = agents.map((a) => a.name).join(", ") || "none";
201
243
 
@@ -211,124 +253,106 @@ export default function (pi: ExtensionAPI): void {
211
253
  };
212
254
  }
213
255
 
214
- // ---- Parallel mode ----
256
+ const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
257
+ const agent = agents.find((candidate) => candidate.name === agentName);
258
+ if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
259
+
260
+ const pending = queuedResult(agent, task);
261
+ const runId = monitor.addRun(agent.name, agent.model);
262
+ const onLive = makeLiveHandler(runId);
263
+
264
+ backgroundQueue.enqueue(
265
+ async (backgroundSignal) => {
266
+ let result: SingleResult;
267
+ try {
268
+ result = await runSingleAgent({
269
+ defaultCwd: ctx.cwd,
270
+ agent,
271
+ agentName,
272
+ task,
273
+ cwd,
274
+ thinkingLevel: config.thinkingLevel,
275
+ signal: backgroundSignal,
276
+ onLive,
277
+ makeDetails: makeDetails("single", true),
278
+ });
279
+ } catch (error) {
280
+ const errorMessage = error instanceof Error ? error.message : String(error);
281
+ result = {
282
+ ...pending,
283
+ exitCode: 1,
284
+ stderr: errorMessage,
285
+ stopReason: backgroundSignal.aborted ? "aborted" : "error",
286
+ errorMessage,
287
+ };
288
+ finishRun(runId, "failed");
289
+ }
290
+
291
+ if (!sessionActive) return;
292
+ const status = isFailedResult(result) ? "failed" : "completed";
293
+ const usage = formatUsage(result.usage);
294
+ pi.sendMessage(
295
+ {
296
+ customType: "subagent-result",
297
+ content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${getResultOutput(result)}`,
298
+ display: true,
299
+ },
300
+ // The result is both durable context and a wake-up signal. If the
301
+ // main agent is busy, followUp queues it until the current turn ends.
302
+ { deliverAs: "followUp", triggerTurn: true },
303
+ );
304
+ },
305
+ () => finishRun(runId, "failed"),
306
+ );
307
+
308
+ return pending;
309
+ };
310
+
311
+ // Sub-agents intentionally detach from the foreground turn. This makes the
312
+ // editor available immediately; completion messages later wake the main agent.
215
313
  if (params.tasks && params.tasks.length > 0) {
216
314
  if (params.tasks.length > MAX_PARALLEL_TASKS) {
217
315
  return {
218
316
  content: [
219
317
  { type: "text", text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` },
220
318
  ],
221
- details: makeDetails("parallel")([]),
319
+ details: makeDetails("parallel", true)([]),
222
320
  };
223
321
  }
224
322
 
225
- const allResults: SingleResult[] = params.tasks.map((t) => ({
226
- agent: t.agent,
227
- agentSource: "unknown",
228
- task: t.task,
229
- exitCode: -1,
230
- messages: [],
231
- stderr: "",
232
- usage: emptyUsage(),
233
- }));
234
-
235
- const emitParallelUpdate = (): void => {
236
- if (!onUpdate) return;
237
- const done = allResults.filter((r) => r.exitCode !== -1).length;
238
- onUpdate({
239
- content: [{ type: "text", text: `Parallel: ${done}/${allResults.length} done...` }],
240
- details: makeDetails("parallel")([...allResults]),
241
- });
242
- };
243
-
244
- const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
245
- const resolvedModel = agents.find((a) => a.name === t.agent)?.model;
246
- const runId = monitor.addRun(t.agent, resolvedModel);
247
- const onLive = makeLiveHandler(runId);
248
- const perTaskUpdate: OnUpdateCallback | undefined = onUpdate
249
- ? (partial) => {
250
- const current = partial.details?.results[0];
251
- if (current) {
252
- allResults[index] = current;
253
- emitParallelUpdate();
254
- }
255
- }
256
- : undefined;
257
- let result: SingleResult;
258
- try {
259
- result = await runSingleAgent({
260
- defaultCwd: ctx.cwd,
261
- agent: agents.find((a) => a.name === t.agent),
262
- agentName: t.agent,
263
- task: t.task,
264
- cwd: t.cwd,
265
- thinkingLevel: config.thinkingLevel,
266
- signal,
267
- onUpdate: perTaskUpdate,
268
- onLive,
269
- makeDetails: makeDetails("parallel"),
270
- });
271
- } catch (err) {
272
- finishRun(runId, "failed");
273
- throw err;
274
- }
275
- allResults[index] = result;
276
- emitParallelUpdate();
277
- return result;
278
- });
279
-
280
- const successCount = results.filter((r) => !isFailedResult(r)).length;
281
- const summaries = results.map((r) => {
282
- const output = getResultOutput(r);
283
- const status = isFailedResult(r) ? "failed" : "completed";
284
- const usage = formatUsage(r.usage);
285
- return `### [${r.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${output}`;
286
- });
323
+ const results = params.tasks.map((task) => startBackground(task.agent, task.task, task.cwd));
324
+ const started = results.filter((result) => result.exitCode === -1).length;
325
+ const failures = results.filter((result) => result.exitCode !== -1);
287
326
  return {
288
327
  content: [
289
328
  {
290
329
  type: "text",
291
- text: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n---\n\n")}`,
330
+ text:
331
+ started > 0
332
+ ? `Started ${started} background subagent${started === 1 ? "" : "s"}. Results will automatically resume the main agent when ready.`
333
+ : failures.map((result) => getResultOutput(result)).join("\n"),
292
334
  },
293
335
  ],
294
- details: makeDetails("parallel")(results),
336
+ details: makeDetails("parallel", true)(results),
337
+ isError: failures.length > 0,
338
+ terminate: true,
295
339
  };
296
340
  }
297
341
 
298
- // ---- Single mode ----
299
- const resolvedModel = agents.find((a) => a.name === params.agent)?.model;
300
- const runId = monitor.addRun(params.agent as string, resolvedModel);
301
- const onLive = makeLiveHandler(runId);
302
- let result: SingleResult;
303
- try {
304
- result = await runSingleAgent({
305
- defaultCwd: ctx.cwd,
306
- agent: agents.find((a) => a.name === params.agent),
307
- agentName: params.agent as string,
308
- task: params.task as string,
309
- cwd: params.cwd,
310
- thinkingLevel: config.thinkingLevel,
311
- signal,
312
- onUpdate,
313
- onLive,
314
- makeDetails: makeDetails("single"),
315
- });
316
- } catch (err) {
317
- finishRun(runId, "failed");
318
- throw err;
319
- }
320
-
321
- if (isFailedResult(result)) {
342
+ const result = startBackground(params.agent as string, params.task as string, params.cwd);
343
+ if (result.exitCode !== -1) {
322
344
  return {
323
- content: [{ type: "text", text: `Agent ${result.agent} ${result.stopReason || "failed"}: ${getResultOutput(result)}` }],
345
+ content: [{ type: "text", text: getResultOutput(result) }],
324
346
  details: makeDetails("single")([result]),
325
347
  isError: true,
326
348
  };
327
349
  }
328
350
  return {
329
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(no output)" }],
330
- details: makeDetails("single")([result]),
351
+ content: [{ type: "text", text: `Started ${result.agent} in the background. Its result will automatically resume the main agent when ready.` }],
352
+ details: makeDetails("single", true)([result]),
353
+ terminate: true,
331
354
  };
355
+
332
356
  },
333
357
 
334
358
  renderCall(args, theme) {
@@ -356,10 +380,11 @@ export default function (pi: ExtensionAPI): void {
356
380
 
357
381
  if (details.mode === "single") {
358
382
  const r = details.results[0];
359
- const icon = statusIcon(isFailedResult(r) ? "failed" : "done", theme);
383
+ const pending = r.exitCode === -1;
384
+ const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
360
385
  const usage = formatUsage(r.usage);
361
386
  const model = r.model ?? "?";
362
- const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${usage ? ` · ${usage}` : ""}`)}`;
387
+ const line = `${theme.fg("toolTitle", theme.bold("subagent "))}${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`;
363
388
  return new Text(line, 0, 0);
364
389
  }
365
390
 
@@ -368,10 +393,11 @@ export default function (pi: ExtensionAPI): void {
368
393
  `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `parallel (${details.results.length})`)}`,
369
394
  ];
370
395
  for (const r of details.results) {
371
- const icon = statusIcon(isFailedResult(r) ? "failed" : "done", theme);
396
+ const pending = r.exitCode === -1;
397
+ const icon = statusIcon(pending ? "running" : isFailedResult(r) ? "failed" : "done", theme);
372
398
  const usage = formatUsage(r.usage);
373
399
  const model = r.model ?? "?";
374
- lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${usage ? ` · ${usage}` : ""}`)}`);
400
+ lines.push(` ${icon} ${theme.fg("accent", r.agent)} ${theme.fg("dim", `· ${model}${pending ? " · background" : ""}${usage ? ` · ${usage}` : ""}`)}`);
375
401
  }
376
402
  return new Text(lines.join("\n"), 0, 0);
377
403
  },
package/src/prompt.ts CHANGED
@@ -37,18 +37,19 @@ export function buildDelegationDirective(agents: AgentConfig[]): string {
37
37
  return `
38
38
  ## Sub-agent delegation (pi-subagents)
39
39
 
40
- You have a \`subagent\` tool that runs specialized agents in ISOLATED context windows.
41
- Delegate discrete, self-contained tasks to it instead of doing everything inline, so the
42
- main window stays focused on orchestration, synthesis, and verification.
40
+ You have a \`subagent\` tool that starts specialized agents in ISOLATED background processes.
41
+ It immediately ends the current main-agent turn so the user can keep working. When a child
42
+ finishes, its result is sent back as a message that automatically resumes the main agent;
43
+ if the main agent is busy, the result waits as a follow-up.
43
44
 
44
45
  Available agents:
45
46
  ${catalog}
46
47
 
47
48
  ${routing ? `Routing:\n${routing}\n` : ""}Dispatch discipline:
48
- - Default to delegating every discrete task to a sub-agent; do the orchestration and verification yourself in the main window.
49
+ - Default to delegating every discrete task to a sub-agent; use the automatically delivered findings for orchestration and verification.
49
50
  - Only handle inline: pure Q&A, a single trivial edit/lookup, or when the user explicitly says to do it directly. When in doubt, delegate.
50
51
  - For an already-known or trivial target, use a direct search/read tool (e.g. grep/find/read) — do not over-delegate a one-line lookup.
51
- ${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Keep dependent work sequential (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
52
+ ${hasMultiple ? "- Run INDEPENDENT tasks in parallel: one subagent call with a `tasks` array, and track them with your todo list. Let the automatically resumed main agent launch dependent work only after its prerequisite result arrives (e.g. explore, then worker, then reviewer).\n" : ""}- Brief each sub-agent as self-contained: goal, exact paths, constraints, expected output. It has NO memory of this conversation.
52
53
  - Treat delegated agents as leaf workers: do not ask a sub-agent to dispatch another sub-agent; child processes do not have this tool.
53
54
  - Trust but verify: a sub-agent's summary describes intent, not outcome. Check the actual changes/results before reporting work done.
54
55
 
package/src/spawn.ts CHANGED
@@ -27,8 +27,8 @@ export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
27
27
  /** Child processes are leaf agents: they never receive the subagent tool. */
28
28
  export const MAX_SUBAGENT_DEPTH = 1;
29
29
  export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
30
- /** Absolute limit so a child that stops emitting events cannot hang the parent forever. */
31
- export const SUBAGENT_TIMEOUT_MS = 10 * 60 * 1000;
30
+ /** No default deadline: sub-agents may run until completion or explicit cancellation. */
31
+ export const SUBAGENT_TIMEOUT_MS = 0;
32
32
  export const SUBAGENT_KILL_GRACE_MS = 5_000;
33
33
 
34
34
  export interface UsageStats {
@@ -57,6 +57,8 @@ export interface SingleResult {
57
57
  export interface SubagentDetails {
58
58
  mode: "single" | "parallel";
59
59
  results: SingleResult[];
60
+ /** The tool returned immediately while the child process continues in the background. */
61
+ background?: boolean;
60
62
  }
61
63
 
62
64
  export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
@@ -179,7 +181,7 @@ export interface RunSingleOptions {
179
181
  cwd?: string;
180
182
  /** Thinking level passed to the child pi process. */
181
183
  thinkingLevel?: ThinkingLevel;
182
- /** Override the watchdog timeout; intended for tests and controlled callers. */
184
+ /** Optional timeout; zero (the default) disables it. Intended for tests and controlled callers. */
183
185
  timeoutMs?: number;
184
186
  signal?: AbortSignal;
185
187
  onUpdate?: OnUpdateCallback;
package/README-zh.md DELETED
@@ -1,153 +0,0 @@
1
- # pi-subagents
2
-
3
- [![npm version](https://img.shields.io/npm/v/@ferris1225/pi-subagents?color=blue)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
4
- [![downloads](https://img.shields.io/npm/dm/@ferris1225/pi-subagents)](https://www.npmjs.com/package/@ferris1225/pi-subagents)
5
- [![license](https://img.shields.io/npm/l/@ferris1225/pi-subagents)](./LICENSE)
6
- ![platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)
7
- ![pi](https://img.shields.io/badge/pi-extension-orange)
8
-
9
- [English](./README.md) | 中文
10
-
11
- 一个聚焦的 [pi](https://pi.dev) 扩展,给主模型提供**它真的会去用**的 sub-agent:
12
- `explore`、`worker`、`reviewer`(外加可选的 `plan`),每个都跑在独立的 `pi` 进程里。
13
- 真正的差异点不是 agent 本身,而是**主动派发注入**——让模型自己主动去委派任务,
14
- 于是你可以把全局 `AGENTS.md` 里那两段派发/审查规则删掉。
15
-
16
- ## 为什么选 pi-subagents?
17
-
18
- pi 故意不内置 sub-agent。社区的补位方案分两种,但都没踩中:
19
-
20
- - **太重** —— 9 个 agent、链式流水线、worktree 集群、到处都是 slash 命令。强大,但机器太多。
21
- - **太安静** —— 只给一个 `subagent` 工具,模型**很少主动调用**,因为 pi 只把工具本身展示给主模型,
22
- 从不展示每个 agent 的描述。于是除非你在全局提示词里强制,否则这些 agent 一直吃灰。
23
-
24
- `pi-subagents` 走中间路线:
25
-
26
- | 优势 | 对你意味着什么 |
27
- |------|----------------|
28
- | **真的会被用** | `before_agent_start` hook 每轮把 agent 清单 + 派发/审查指令注入系统提示词,再由 tool `promptGuidelines` 和 `Use PROACTIVELY when …` 描述加强。这正是重型框架依赖的那根杠杆——我们只是把它变成默认行为。 |
29
- | **体量合适** | 3 个聚焦的 agent(+1 可选),不是 9 个。没有链式/worktree/集群机器。只有 single 和 parallel 两种模式。 |
30
- | **替代你的 AGENTS.md 规则** | 注入的指令是 "Sub-agent Dispatch" 和 "Review, Verification & Commit" 两段的自包含替代。装上它,然后把那两段删掉。 |
31
- | **真隔离** | 每个 agent 都是独立 `pi` 进程(`--no-session`),委派出去的活绝不污染主上下文。 |
32
- | **该只读就只读** | `explore`、`plan`、`reviewer` 都是只读。`reviewer` 跑在**独立**上下文,避免自我确认偏差。 |
33
- | **纯选择式配置** | 不用手敲值:勾选式模块选择器 + 模糊过滤、可翻页的模型选择器。 |
34
- | **合理的模型默认** | 每 agent 可单独覆盖模型;不选就**用主窗口当前 session 的模型**。配置中的不可用模型会自动修复并写回。 |
35
- | **子代理是叶节点** | 子进程不会获得 `subagent` 工具,因此不会递归派发或无限运行。 |
36
- | **零运行时依赖** | 纯 pi 扩展,仅 peer 依赖,无需构建步骤。 |
37
-
38
- ## 安装
39
-
40
- ```bash
41
- pi install npm:@ferris1225/pi-subagents
42
- ```
43
-
44
- 要求 pi **≥ 0.80.6**——子代理思考强度使用该版本引入的 `--thinking` 参数值。
45
-
46
- 然后运行配置向导(纯选择):
47
-
48
- ```text
49
- /subagents-setup
50
- ```
51
-
52
- ## Agent 一览
53
-
54
- | Agent | 默认启用 | 工具 | 职责 |
55
- |-------|:--------:|------|------|
56
- | `explore` | ✅ | 只读 | 快速代码侦察;返回压缩后的发现以便交接。 |
57
- | `worker` | ✅ | 全部 | 实现/修复/重构/测试一个自包含任务。**内部先规划后动手。** |
58
- | `reviewer` | ✅ | 只读 | 在独立上下文做对抗式提交前审查。 |
59
- | `plan` | 可选 | 只读 | 产出可人工审阅的独立实现计划。worker 本就会内部规划,所以只在你需要把计划作为独立产物时才用它。 |
60
-
61
- 每个 agent 都是一个 Markdown 文件(`agents/*.md`:YAML frontmatter + 正文作为 system prompt)。
62
- 想覆盖任意一个,只需把同名 `name` 的文件放进 `~/.pi/agent/agents/`(用户级)或 `.pi/agents/`(项目级)。
63
-
64
- ## 主动派发是怎么工作的
65
-
66
- pi 从不把每个 agent 的描述展示给主模型——它只看到 `subagent` 这个工具。三根杠杆解决这一点:
67
-
68
- 1. **`before_agent_start` 注入** —— 每一轮,把启用的 agent 加上一段派发/审查指令追加进父模型系统提示词。
69
- 2. **tool `promptSnippet` / `promptGuidelines`** —— 在工具激活时持续强化「何时该委派」。
70
- 3. **`Use PROACTIVELY when …` 描述** —— 在 Claude Code agent 生态被验证过的触发措辞。
71
-
72
- 这段指令会引导出一条干净的流程:**`explore` → `worker` → `reviewer`**,独立任务并行扇出,
73
- 以及「信任但需验证」的交接。
74
-
75
- ## 配置
76
-
77
- 存放在 `~/.pi/agent/pi-subagents.json`(尊重 `PI_CODING_AGENT_DIR`):
78
-
79
- ```json
80
- {
81
- "enabledAgents": ["explore", "worker", "reviewer"],
82
- "agentModels": { "explore": "anthropic/claude-haiku-4-5" },
83
- "thinkingLevel": "max",
84
- "proactiveInjection": true,
85
- "agentScope": "user"
86
- }
87
- ```
88
-
89
- - `enabledAgents` —— 哪些 agent 可被发现并注入。
90
- - `agentModels` —— 每 agent 的模型覆盖(`"provider/model-id"`)。如果已保存的模型不可用,会切换到主窗口当前模型并写回此文件。
91
- - `thinkingLevel` —— 子代理思考强度:`off`、`minimal`、`low`、`medium`、`high`、`xhigh` 或 `max`(默认)。
92
- - `proactiveInjection` —— 开关系统提示词注入。
93
- - `agentScope` —— `"user"`(默认)、`"project"` 或 `"both"`。
94
-
95
- **每个 agent 的模型优先级**:
96
-
97
- ```
98
- 可用的 agentModels[name] → 当前 session 模型 → agent frontmatter 里的默认
99
- ```
100
-
101
- 如果配置的模型不再可用,会切换到主窗口当前模型,并在下一次运行前写回配置文件。
102
-
103
- ## 使用
104
-
105
- 主模型会自己调用 `subagent`,你也可以直接要求:
106
-
107
- ```text
108
- # 单个
109
- 用 explore sub-agent 梳理一下认证是怎么接起来的。
110
-
111
- # 并行(独立任务)
112
- 用并行 sub-agent 跑这两件:探索 API 层,以及探索 DB 层。
113
- ```
114
-
115
- 工具参数形态:
116
-
117
- ```jsonc
118
- // 单个
119
- { "agent": "worker", "task": "<自包含的任务简报>" }
120
- // 并行
121
- { "tasks": [ { "agent": "explore", "task": "..." }, { "agent": "explore", "task": "..." } ] }
122
- ```
123
-
124
- ## 实时状态与通知
125
-
126
- 子代理运行期间,编辑器上方的挂件为每个运行显示一行状态(图标、agent、模型、
127
- token 用量、耗时),其下缩进一行显示它正在做什么:`thinking`、`writing`、
128
- `read src/index.ts`、`bash npm test`……(不会是一坨 JSON 参数)。
129
-
130
- 运行结束(成功**或**失败)时,该行立即从挂件消失,主窗口收到一条通知,
131
- 给出最终摘要(`✓ worker · openai/gpt-5 · ↑12.4k ↓3.1k · 47s`)。工具结果
132
- 本身仍是对话里的持久记录。
133
-
134
- 子代理使用配置的思考强度(默认 `--thinking max`);pi 会按目标模型实际支持
135
- 的级别自适应降级(`max → xhigh → high → … → off`),弱模型也能平稳运行。
136
- 任务内容通过 stdin 传递,只有 agent system prompt 使用短生命周期临时文件。
137
- 子进程输出在内存中流式处理;中止或卡住时会通过 watchdog 清理整个进程树。
138
-
139
- ## 开发
140
-
141
- ```bash
142
- npm install
143
- npm run check # tsc --noEmit
144
- npm test # vitest
145
- ```
146
-
147
- ## 相关项目
148
-
149
- - [pi-querit-search](https://www.npmjs.com/package/pi-querit-search) —— 为 pi 提供实时网络搜索与网页抓取,同一作者。
150
-
151
- ## 许可证
152
-
153
- MIT