@tintinweb/pi-subagents 0.3.1 → 0.4.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/CHANGELOG.md +29 -1
- package/README.md +17 -15
- package/dist/agent-manager.d.ts +70 -0
- package/dist/agent-manager.js +236 -0
- package/dist/agent-runner.d.ts +60 -0
- package/dist/agent-runner.js +265 -0
- package/dist/agent-types.d.ts +41 -0
- package/dist/agent-types.js +130 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.js +100 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +126 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1270 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +62 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.js +48 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +101 -0
- package/dist/ui/agent-widget.js +333 -0
- package/dist/ui/conversation-viewer.d.ts +31 -0
- package/dist/ui/conversation-viewer.js +236 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +22 -4
- package/src/agent-runner.ts +11 -45
- package/src/agent-types.ts +4 -15
- package/src/default-agents.ts +2 -36
- package/src/index.ts +30 -24
- package/src/prompts.ts +35 -20
- package/src/ui/agent-widget.ts +100 -24
- package/src/ui/conversation-viewer.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-03-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Graceful shutdown in headless mode** — the CLI now waits for all running and queued background agents to complete before exiting (`waitForAll` on `session_shutdown`). Previously, background agents could be silently killed mid-execution when the session ended. Only affects headless/non-interactive mode; interactive sessions already kept the process alive.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `hasRunning()` / `waitForAll()` methods on `AgentManager`.
|
|
15
|
+
- **Cross-package manager access** — agent manager exposed via `Symbol.for("pi-subagents:manager")` on `globalThis` for other extensions to check status or await completion.
|
|
16
|
+
|
|
17
|
+
## [0.4.0] - 2026-03-11
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **XML-delimited prompt sections** — append-mode agents now wrap inherited content in `<inherited_system_prompt>`, `<sub_agent_context>`, and `<agent_instructions>` XML tags, giving the model explicit structure to distinguish inherited rules from sub-agent-specific instructions. Replace mode is unchanged.
|
|
21
|
+
- **Token count in agent results** — foreground agent results, background completion notifications, and `get_subagent_result` now include the token count alongside tool uses and duration (e.g. `Agent completed in 4.2s (12 tool uses, 33.8k token)`).
|
|
22
|
+
- **Widget overflow cap** — the running agents widget now caps at 12 lines. When exceeded, running agents are prioritized over finished ones and an overflow summary line shows hidden counts (e.g. `+3 more (1 running, 2 finished)`).
|
|
23
|
+
|
|
24
|
+
### Changed - **changing behavior**
|
|
25
|
+
- **General-purpose agent inherits parent prompt** — the default `general-purpose` agent now uses `promptMode: "append"` with an empty system prompt, making it a "parent twin" that inherits the full parent system prompt (including CLAUDE.md rules, project conventions, and safety guardrails). Previously it used a standalone prompt that duplicated a subset of the parent's rules. Explore and Plan are unchanged (standalone prompts). To customize: eject via `/agents` → select `general-purpose` → Eject, then edit the resulting `.md` file. Set `prompt_mode: replace` to go back to a standalone prompt, or keep `prompt_mode: append` and add extra instructions in the body.
|
|
26
|
+
- **Append-mode agents receive parent system prompt** — `buildAgentPrompt` now accepts the parent's system prompt and threads it into append-mode agents (env header + parent prompt + sub-agent context bridge + optional custom instructions). Replace-mode agents are unchanged.
|
|
27
|
+
- **Prompt pipeline simplified** — removed `systemPromptOverride`/`systemPromptAppend` from `SpawnOptions` and `RunOptions`. These were a separate code path where `index.ts` pre-resolved the prompt mode and passed raw strings into the runner, bypassing `buildAgentPrompt`. Now all prompt assembly flows through `buildAgentPrompt` using the agent's `promptMode` config — one code path, no special cases.
|
|
28
|
+
|
|
29
|
+
### Removed
|
|
30
|
+
- Deprecated backwards-compat aliases: `registerCustomAgents`, `getCustomAgentConfig`, `getCustomAgentNames` (use `registerAgents`, `getAgentConfig`, `getUserAgentNames`).
|
|
31
|
+
- `resolveCustomPrompt()` helper in index.ts — no longer needed now that prompt routing is config-driven.
|
|
32
|
+
|
|
8
33
|
## [0.3.1] - 2026-03-09
|
|
9
34
|
|
|
10
35
|
### Added
|
|
@@ -139,7 +164,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
139
164
|
### Added
|
|
140
165
|
- **Claude Code-style UI rendering** — `renderCall`/`renderResult`/`onUpdate` for live streaming progress
|
|
141
166
|
- Live activity descriptions: "searching, reading 3 files…"
|
|
142
|
-
- Token count display: "33.8k
|
|
167
|
+
- Token count display: "33.8k token"
|
|
143
168
|
- Per-agent tool use counter
|
|
144
169
|
- Expandable completed results (ctrl+o)
|
|
145
170
|
- Distinct states: running, background, completed, error, aborted
|
|
@@ -172,6 +197,9 @@ Initial release.
|
|
|
172
197
|
- **Thinking level** — per-agent extended thinking control
|
|
173
198
|
- **`/agent` and `/agents` commands**
|
|
174
199
|
|
|
200
|
+
[0.4.1]: https://github.com/tintinweb/pi-subagents/compare/v0.4.0...v0.4.1
|
|
201
|
+
[0.4.0]: https://github.com/tintinweb/pi-subagents/compare/v0.3.1...v0.4.0
|
|
202
|
+
[0.3.1]: https://github.com/tintinweb/pi-subagents/compare/v0.3.0...v0.3.1
|
|
175
203
|
[0.3.0]: https://github.com/tintinweb/pi-subagents/compare/v0.2.7...v0.3.0
|
|
176
204
|
[0.2.7]: https://github.com/tintinweb/pi-subagents/compare/v0.2.6...v0.2.7
|
|
177
205
|
[0.2.6]: https://github.com/tintinweb/pi-subagents/compare/v0.2.5...v0.2.6
|
package/README.md
CHANGED
|
@@ -57,9 +57,9 @@ The extension renders a persistent widget above the editor showing all active ag
|
|
|
57
57
|
|
|
58
58
|
```
|
|
59
59
|
● Agents
|
|
60
|
-
├─ ⠹ Agent Refactor auth module · 5 tool uses · 33.8k
|
|
60
|
+
├─ ⠹ Agent Refactor auth module · 5 tool uses · 33.8k token · 12.3s
|
|
61
61
|
│ ⎿ editing 2 files…
|
|
62
|
-
├─ ⠹ Explore Find auth files · 3 tool uses · 12.4k
|
|
62
|
+
├─ ⠹ Explore Find auth files · 3 tool uses · 12.4k token · 4.1s
|
|
63
63
|
│ ⎿ searching…
|
|
64
64
|
└─ 2 queued
|
|
65
65
|
```
|
|
@@ -68,24 +68,26 @@ Individual agent results render Claude Code-style in the conversation:
|
|
|
68
68
|
|
|
69
69
|
| State | Example |
|
|
70
70
|
|-------|---------|
|
|
71
|
-
| **Running** | `⠹ 3 tool uses · 12.4k
|
|
72
|
-
| **Completed** | `✓ 5 tool uses · 33.8k
|
|
73
|
-
| **Wrapped up** | `✓ 50 tool uses · 89.1k
|
|
74
|
-
| **Stopped** | `■ 3 tool uses · 12.4k
|
|
75
|
-
| **Error** | `✗ 3 tool uses · 12.4k
|
|
76
|
-
| **Aborted** | `✗ 55 tool uses · 102.3k
|
|
71
|
+
| **Running** | `⠹ 3 tool uses · 12.4k token` / `⎿ searching, reading 3 files…` |
|
|
72
|
+
| **Completed** | `✓ 5 tool uses · 33.8k token · 12.3s` / `⎿ Done` |
|
|
73
|
+
| **Wrapped up** | `✓ 50 tool uses · 89.1k token · 45.2s` / `⎿ Wrapped up (turn limit)` |
|
|
74
|
+
| **Stopped** | `■ 3 tool uses · 12.4k token` / `⎿ Stopped` |
|
|
75
|
+
| **Error** | `✗ 3 tool uses · 12.4k token` / `⎿ Error: timeout` |
|
|
76
|
+
| **Aborted** | `✗ 55 tool uses · 102.3k token` / `⎿ Aborted (max turns exceeded)` |
|
|
77
77
|
|
|
78
78
|
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
79
79
|
|
|
80
80
|
## Default Agent Types
|
|
81
81
|
|
|
82
|
-
| Type | Tools | Model | Description |
|
|
83
|
-
|
|
84
|
-
| `general-purpose` | all 7 | inherit |
|
|
85
|
-
| `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | Fast codebase exploration (read-only) |
|
|
86
|
-
| `Plan` | read, bash, grep, find, ls | inherit | Software architect for implementation planning (read-only) |
|
|
82
|
+
| Type | Tools | Model | Prompt Mode | Description |
|
|
83
|
+
|------|-------|-------|-------------|-------------|
|
|
84
|
+
| `general-purpose` | all 7 | inherit | `append` (parent twin) | Inherits the parent's full system prompt — same rules, CLAUDE.md, project conventions |
|
|
85
|
+
| `Explore` | read, bash, grep, find, ls | haiku (falls back to inherit) | `replace` (standalone) | Fast codebase exploration (read-only) |
|
|
86
|
+
| `Plan` | read, bash, grep, find, ls | inherit | `replace` (standalone) | Software architect for implementation planning (read-only) |
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
The `general-purpose` agent is a **parent twin** — it receives the parent's entire system prompt plus a sub-agent context bridge, so it follows the same rules the parent does. Explore and Plan use standalone prompts tailored to their read-only roles.
|
|
89
|
+
|
|
90
|
+
Default agents can be **ejected** (`/agents` → select agent → Eject) to export them as `.md` files for customization, **overridden** by creating a `.md` file with the same name (e.g. `.pi/agents/general-purpose.md`), or **disabled** per-project with `enabled: false` frontmatter.
|
|
89
91
|
|
|
90
92
|
## Custom Agents
|
|
91
93
|
|
|
@@ -140,7 +142,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
140
142
|
| `model` | inherit parent | Model — `provider/modelId` or fuzzy name (`"haiku"`, `"sonnet"`) |
|
|
141
143
|
| `thinking` | inherit | off, minimal, low, medium, high, xhigh |
|
|
142
144
|
| `max_turns` | 50 | Max agentic turns before graceful shutdown |
|
|
143
|
-
| `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to
|
|
145
|
+
| `prompt_mode` | `replace` | `replace`: body is the full system prompt. `append`: body appended to parent's prompt (agent acts as a "parent twin" with optional extra instructions) |
|
|
144
146
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
145
147
|
| `run_in_background` | `false` | Run in background by default |
|
|
146
148
|
| `isolated` | `false` | No extension/MCP tools, only built-in |
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
3
|
+
*
|
|
4
|
+
* Background agents are subject to a configurable concurrency limit (default: 4).
|
|
5
|
+
* Excess agents are queued and auto-started as running agents complete.
|
|
6
|
+
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
|
+
*/
|
|
8
|
+
import type { ExtensionContext, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
10
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
import { type ToolActivity } from "./agent-runner.js";
|
|
12
|
+
import type { SubagentType, AgentRecord, ThinkingLevel } from "./types.js";
|
|
13
|
+
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
14
|
+
interface SpawnOptions {
|
|
15
|
+
description: string;
|
|
16
|
+
model?: Model<any>;
|
|
17
|
+
maxTurns?: number;
|
|
18
|
+
isolated?: boolean;
|
|
19
|
+
inheritContext?: boolean;
|
|
20
|
+
thinkingLevel?: ThinkingLevel;
|
|
21
|
+
isBackground?: boolean;
|
|
22
|
+
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
23
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
24
|
+
/** Called on streaming text deltas from the assistant response. */
|
|
25
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
26
|
+
/** Called when the agent session is created (for accessing session stats). */
|
|
27
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare class AgentManager {
|
|
30
|
+
private agents;
|
|
31
|
+
private cleanupInterval;
|
|
32
|
+
private onComplete?;
|
|
33
|
+
private maxConcurrent;
|
|
34
|
+
/** Queue of background agents waiting to start. */
|
|
35
|
+
private queue;
|
|
36
|
+
/** Number of currently running background agents. */
|
|
37
|
+
private runningBackground;
|
|
38
|
+
constructor(onComplete?: OnAgentComplete, maxConcurrent?: number);
|
|
39
|
+
/** Update the max concurrent background agents limit. */
|
|
40
|
+
setMaxConcurrent(n: number): void;
|
|
41
|
+
getMaxConcurrent(): number;
|
|
42
|
+
/**
|
|
43
|
+
* Spawn an agent and return its ID immediately (for background use).
|
|
44
|
+
* If the concurrency limit is reached, the agent is queued.
|
|
45
|
+
*/
|
|
46
|
+
spawn(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: SpawnOptions): string;
|
|
47
|
+
/** Actually start an agent (called immediately or from queue drain). */
|
|
48
|
+
private startAgent;
|
|
49
|
+
/** Start queued agents up to the concurrency limit. */
|
|
50
|
+
private drainQueue;
|
|
51
|
+
/**
|
|
52
|
+
* Spawn an agent and wait for completion (foreground use).
|
|
53
|
+
* Foreground agents bypass the concurrency queue.
|
|
54
|
+
*/
|
|
55
|
+
spawnAndWait(pi: ExtensionAPI, ctx: ExtensionContext, type: SubagentType, prompt: string, options: Omit<SpawnOptions, "isBackground">): Promise<AgentRecord>;
|
|
56
|
+
/**
|
|
57
|
+
* Resume an existing agent session with a new prompt.
|
|
58
|
+
*/
|
|
59
|
+
resume(id: string, prompt: string, signal?: AbortSignal): Promise<AgentRecord | undefined>;
|
|
60
|
+
getRecord(id: string): AgentRecord | undefined;
|
|
61
|
+
listAgents(): AgentRecord[];
|
|
62
|
+
abort(id: string): boolean;
|
|
63
|
+
private cleanup;
|
|
64
|
+
/** Whether any agents are still running or queued. */
|
|
65
|
+
hasRunning(): boolean;
|
|
66
|
+
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
67
|
+
waitForAll(): Promise<void>;
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
3
|
+
*
|
|
4
|
+
* Background agents are subject to a configurable concurrency limit (default: 4).
|
|
5
|
+
* Excess agents are queued and auto-started as running agents complete.
|
|
6
|
+
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { runAgent, resumeAgent } from "./agent-runner.js";
|
|
10
|
+
/** Default max concurrent background agents. */
|
|
11
|
+
const DEFAULT_MAX_CONCURRENT = 4;
|
|
12
|
+
export class AgentManager {
|
|
13
|
+
agents = new Map();
|
|
14
|
+
cleanupInterval;
|
|
15
|
+
onComplete;
|
|
16
|
+
maxConcurrent;
|
|
17
|
+
/** Queue of background agents waiting to start. */
|
|
18
|
+
queue = [];
|
|
19
|
+
/** Number of currently running background agents. */
|
|
20
|
+
runningBackground = 0;
|
|
21
|
+
constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
22
|
+
this.onComplete = onComplete;
|
|
23
|
+
this.maxConcurrent = maxConcurrent;
|
|
24
|
+
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
25
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
26
|
+
}
|
|
27
|
+
/** Update the max concurrent background agents limit. */
|
|
28
|
+
setMaxConcurrent(n) {
|
|
29
|
+
this.maxConcurrent = Math.max(1, n);
|
|
30
|
+
// Start queued agents if the new limit allows
|
|
31
|
+
this.drainQueue();
|
|
32
|
+
}
|
|
33
|
+
getMaxConcurrent() {
|
|
34
|
+
return this.maxConcurrent;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Spawn an agent and return its ID immediately (for background use).
|
|
38
|
+
* If the concurrency limit is reached, the agent is queued.
|
|
39
|
+
*/
|
|
40
|
+
spawn(pi, ctx, type, prompt, options) {
|
|
41
|
+
const id = randomUUID().slice(0, 17);
|
|
42
|
+
const abortController = new AbortController();
|
|
43
|
+
const record = {
|
|
44
|
+
id,
|
|
45
|
+
type,
|
|
46
|
+
description: options.description,
|
|
47
|
+
status: options.isBackground ? "queued" : "running",
|
|
48
|
+
toolUses: 0,
|
|
49
|
+
startedAt: Date.now(),
|
|
50
|
+
abortController,
|
|
51
|
+
};
|
|
52
|
+
this.agents.set(id, record);
|
|
53
|
+
const args = { pi, ctx, type, prompt, options };
|
|
54
|
+
if (options.isBackground && this.runningBackground >= this.maxConcurrent) {
|
|
55
|
+
// Queue it — will be started when a running agent completes
|
|
56
|
+
this.queue.push({ id, args });
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
this.startAgent(id, record, args);
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
/** Actually start an agent (called immediately or from queue drain). */
|
|
63
|
+
startAgent(id, record, { pi, ctx, type, prompt, options }) {
|
|
64
|
+
record.status = "running";
|
|
65
|
+
record.startedAt = Date.now();
|
|
66
|
+
if (options.isBackground)
|
|
67
|
+
this.runningBackground++;
|
|
68
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
69
|
+
pi,
|
|
70
|
+
model: options.model,
|
|
71
|
+
maxTurns: options.maxTurns,
|
|
72
|
+
isolated: options.isolated,
|
|
73
|
+
inheritContext: options.inheritContext,
|
|
74
|
+
thinkingLevel: options.thinkingLevel,
|
|
75
|
+
signal: record.abortController.signal,
|
|
76
|
+
onToolActivity: (activity) => {
|
|
77
|
+
if (activity.type === "end")
|
|
78
|
+
record.toolUses++;
|
|
79
|
+
options.onToolActivity?.(activity);
|
|
80
|
+
},
|
|
81
|
+
onTextDelta: options.onTextDelta,
|
|
82
|
+
onSessionCreated: (session) => {
|
|
83
|
+
record.session = session;
|
|
84
|
+
options.onSessionCreated?.(session);
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.then(({ responseText, session, aborted, steered }) => {
|
|
88
|
+
// Don't overwrite status if externally stopped via abort()
|
|
89
|
+
if (record.status !== "stopped") {
|
|
90
|
+
record.status = aborted ? "aborted" : steered ? "steered" : "completed";
|
|
91
|
+
}
|
|
92
|
+
record.result = responseText;
|
|
93
|
+
record.session = session;
|
|
94
|
+
record.completedAt ??= Date.now();
|
|
95
|
+
if (options.isBackground) {
|
|
96
|
+
this.runningBackground--;
|
|
97
|
+
this.onComplete?.(record);
|
|
98
|
+
this.drainQueue();
|
|
99
|
+
}
|
|
100
|
+
return responseText;
|
|
101
|
+
})
|
|
102
|
+
.catch((err) => {
|
|
103
|
+
// Don't overwrite status if externally stopped via abort()
|
|
104
|
+
if (record.status !== "stopped") {
|
|
105
|
+
record.status = "error";
|
|
106
|
+
}
|
|
107
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
108
|
+
record.completedAt ??= Date.now();
|
|
109
|
+
if (options.isBackground) {
|
|
110
|
+
this.runningBackground--;
|
|
111
|
+
this.onComplete?.(record);
|
|
112
|
+
this.drainQueue();
|
|
113
|
+
}
|
|
114
|
+
return "";
|
|
115
|
+
});
|
|
116
|
+
record.promise = promise;
|
|
117
|
+
}
|
|
118
|
+
/** Start queued agents up to the concurrency limit. */
|
|
119
|
+
drainQueue() {
|
|
120
|
+
while (this.queue.length > 0 && this.runningBackground < this.maxConcurrent) {
|
|
121
|
+
const next = this.queue.shift();
|
|
122
|
+
const record = this.agents.get(next.id);
|
|
123
|
+
if (!record || record.status !== "queued")
|
|
124
|
+
continue;
|
|
125
|
+
this.startAgent(next.id, record, next.args);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Spawn an agent and wait for completion (foreground use).
|
|
130
|
+
* Foreground agents bypass the concurrency queue.
|
|
131
|
+
*/
|
|
132
|
+
async spawnAndWait(pi, ctx, type, prompt, options) {
|
|
133
|
+
const id = this.spawn(pi, ctx, type, prompt, { ...options, isBackground: false });
|
|
134
|
+
const record = this.agents.get(id);
|
|
135
|
+
await record.promise;
|
|
136
|
+
return record;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Resume an existing agent session with a new prompt.
|
|
140
|
+
*/
|
|
141
|
+
async resume(id, prompt, signal) {
|
|
142
|
+
const record = this.agents.get(id);
|
|
143
|
+
if (!record?.session)
|
|
144
|
+
return undefined;
|
|
145
|
+
record.status = "running";
|
|
146
|
+
record.startedAt = Date.now();
|
|
147
|
+
record.completedAt = undefined;
|
|
148
|
+
record.result = undefined;
|
|
149
|
+
record.error = undefined;
|
|
150
|
+
try {
|
|
151
|
+
const responseText = await resumeAgent(record.session, prompt, {
|
|
152
|
+
onToolActivity: (activity) => {
|
|
153
|
+
if (activity.type === "end")
|
|
154
|
+
record.toolUses++;
|
|
155
|
+
},
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
record.status = "completed";
|
|
159
|
+
record.result = responseText;
|
|
160
|
+
record.completedAt = Date.now();
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
record.status = "error";
|
|
164
|
+
record.error = err instanceof Error ? err.message : String(err);
|
|
165
|
+
record.completedAt = Date.now();
|
|
166
|
+
}
|
|
167
|
+
return record;
|
|
168
|
+
}
|
|
169
|
+
getRecord(id) {
|
|
170
|
+
return this.agents.get(id);
|
|
171
|
+
}
|
|
172
|
+
listAgents() {
|
|
173
|
+
return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
174
|
+
}
|
|
175
|
+
abort(id) {
|
|
176
|
+
const record = this.agents.get(id);
|
|
177
|
+
if (!record)
|
|
178
|
+
return false;
|
|
179
|
+
// Remove from queue if queued
|
|
180
|
+
if (record.status === "queued") {
|
|
181
|
+
this.queue = this.queue.filter(q => q.id !== id);
|
|
182
|
+
record.status = "stopped";
|
|
183
|
+
record.completedAt = Date.now();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
if (record.status !== "running")
|
|
187
|
+
return false;
|
|
188
|
+
record.abortController?.abort();
|
|
189
|
+
record.status = "stopped";
|
|
190
|
+
record.completedAt = Date.now();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
cleanup() {
|
|
194
|
+
const cutoff = Date.now() - 10 * 60_000;
|
|
195
|
+
for (const [id, record] of this.agents) {
|
|
196
|
+
if (record.status === "running" || record.status === "queued")
|
|
197
|
+
continue;
|
|
198
|
+
if ((record.completedAt ?? 0) >= cutoff)
|
|
199
|
+
continue;
|
|
200
|
+
// Dispose and clear session so memory can be reclaimed
|
|
201
|
+
if (record.session) {
|
|
202
|
+
record.session.dispose();
|
|
203
|
+
record.session = undefined;
|
|
204
|
+
}
|
|
205
|
+
this.agents.delete(id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** Whether any agents are still running or queued. */
|
|
209
|
+
hasRunning() {
|
|
210
|
+
return [...this.agents.values()].some(r => r.status === "running" || r.status === "queued");
|
|
211
|
+
}
|
|
212
|
+
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
213
|
+
async waitForAll() {
|
|
214
|
+
// Loop because drainQueue respects the concurrency limit — as running
|
|
215
|
+
// agents finish they start queued ones, which need awaiting too.
|
|
216
|
+
while (true) {
|
|
217
|
+
this.drainQueue();
|
|
218
|
+
const pending = [...this.agents.values()]
|
|
219
|
+
.filter(r => r.status === "running" || r.status === "queued")
|
|
220
|
+
.map(r => r.promise)
|
|
221
|
+
.filter(Boolean);
|
|
222
|
+
if (pending.length === 0)
|
|
223
|
+
break;
|
|
224
|
+
await Promise.allSettled(pending);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
dispose() {
|
|
228
|
+
clearInterval(this.cleanupInterval);
|
|
229
|
+
// Clear queue
|
|
230
|
+
this.queue = [];
|
|
231
|
+
for (const record of this.agents.values()) {
|
|
232
|
+
record.session?.dispose();
|
|
233
|
+
}
|
|
234
|
+
this.agents.clear();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
|
+
*/
|
|
4
|
+
import { type AgentSession, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
7
|
+
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
8
|
+
/** Get the default max turns value. */
|
|
9
|
+
export declare function getDefaultMaxTurns(): number;
|
|
10
|
+
/** Set the default max turns value (minimum 1). */
|
|
11
|
+
export declare function setDefaultMaxTurns(n: number): void;
|
|
12
|
+
/** Get the grace turns value. */
|
|
13
|
+
export declare function getGraceTurns(): number;
|
|
14
|
+
/** Set the grace turns value (minimum 1). */
|
|
15
|
+
export declare function setGraceTurns(n: number): void;
|
|
16
|
+
/** Info about a tool event in the subagent. */
|
|
17
|
+
export interface ToolActivity {
|
|
18
|
+
type: "start" | "end";
|
|
19
|
+
toolName: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RunOptions {
|
|
22
|
+
/** ExtensionAPI instance — used for pi.exec() instead of execSync. */
|
|
23
|
+
pi: ExtensionAPI;
|
|
24
|
+
model?: Model<any>;
|
|
25
|
+
maxTurns?: number;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
isolated?: boolean;
|
|
28
|
+
inheritContext?: boolean;
|
|
29
|
+
thinkingLevel?: ThinkingLevel;
|
|
30
|
+
/** Called on tool start/end with activity info. */
|
|
31
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
32
|
+
/** Called on streaming text deltas from the assistant response. */
|
|
33
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
34
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface RunResult {
|
|
37
|
+
responseText: string;
|
|
38
|
+
session: AgentSession;
|
|
39
|
+
/** True if the agent was hard-aborted (max_turns + grace exceeded). */
|
|
40
|
+
aborted: boolean;
|
|
41
|
+
/** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
|
|
42
|
+
steered: boolean;
|
|
43
|
+
}
|
|
44
|
+
export declare function runAgent(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Send a new prompt to an existing session (resume).
|
|
47
|
+
*/
|
|
48
|
+
export declare function resumeAgent(session: AgentSession, prompt: string, options?: {
|
|
49
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Send a steering message to a running subagent.
|
|
54
|
+
* The message will interrupt the agent after its current tool execution.
|
|
55
|
+
*/
|
|
56
|
+
export declare function steerAgent(session: AgentSession, message: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Get the subagent's conversation messages as formatted text.
|
|
59
|
+
*/
|
|
60
|
+
export declare function getAgentConversation(session: AgentSession): string;
|