@tintinweb/pi-subagents 0.4.10 → 0.5.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/CHANGELOG.md +14 -4
- package/README.md +23 -8
- package/dist/agent-manager.d.ts +18 -4
- package/dist/agent-manager.js +111 -9
- package/dist/agent-runner.d.ts +10 -6
- package/dist/agent-runner.js +81 -27
- package/dist/agent-types.d.ts +10 -0
- package/dist/agent-types.js +23 -1
- package/dist/cross-extension-rpc.d.ts +46 -0
- package/dist/cross-extension-rpc.js +54 -0
- package/dist/custom-agents.js +36 -8
- package/dist/index.js +336 -66
- package/dist/memory.d.ts +49 -0
- package/dist/memory.js +151 -0
- package/dist/output-file.d.ts +17 -0
- package/dist/output-file.js +66 -0
- package/dist/prompts.d.ts +12 -1
- package/dist/prompts.js +15 -3
- package/dist/skill-loader.d.ts +19 -0
- package/dist/skill-loader.js +67 -0
- package/dist/types.d.ts +45 -1
- package/dist/ui/agent-widget.d.ts +21 -0
- package/dist/ui/agent-widget.js +205 -127
- package/dist/ui/conversation-viewer.d.ts +2 -2
- package/dist/ui/conversation-viewer.js +3 -3
- package/dist/ui/conversation-viewer.test.d.ts +1 -0
- package/dist/ui/conversation-viewer.test.js +254 -0
- package/dist/worktree.d.ts +36 -0
- package/dist/worktree.js +139 -0
- package/package.json +8 -6
- package/src/agent-runner.ts +1 -1
- package/src/cross-extension-rpc.ts +57 -23
- package/src/index.ts +4 -3
- package/src/ui/conversation-viewer.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,17 +5,26 @@ 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
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
- **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
|
|
12
|
-
- **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress.
|
|
10
|
+
## [0.5.0] - 2026-03-22
|
|
13
11
|
|
|
14
12
|
### Added
|
|
13
|
+
- **RPC stop handler** — new `subagents:rpc:stop` event bus RPC allows other extensions to stop running subagents by agent ID. Returns structured error ("Agent not found") on failure.
|
|
14
|
+
- **`abort` in `SpawnCapable` interface** — cross-extension RPC consumers can now stop agents, not just spawn them.
|
|
15
|
+
- **Live turn counter** — all agents now show a live turn count in the widget, inline result, and completion notification. With a turn limit: `⟳5≤30` (5 of 30 turns). Without: `⟳5`. Updates in real time as turns progress via `onTurnEnd` callback.
|
|
15
16
|
- **Biome linting** — added [Biome](https://biomejs.dev/) for correctness linting (unused imports, suspicious patterns). Style rules disabled. Run `npm run lint` to check, `npm run lint:fix` to auto-fix.
|
|
16
17
|
- **CI workflow** — GitHub Actions runs lint, typecheck, and tests on push to master and PRs.
|
|
18
|
+
- **Auto-trigger parent turn on background completion** — background agent completion notifications now use `triggerTurn: true`, automatically prompting the parent agent to process results instead of waiting for user input.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **Standardized RPC envelope** — cross-extension RPC handlers (`ping`, `spawn`, `stop`) now use a `handleRpc` wrapper that emits structured envelopes (`{ success: true, data }` / `{ success: false, error }`), matching pi-mono's `RpcResponse` convention.
|
|
22
|
+
- **Protocol versioning via ping** — ping reply now includes `{ version: PROTOCOL_VERSION }` (currently v2). Callers can detect version mismatches and warn users to update.
|
|
23
|
+
- **Default max turns is now unlimited** — subagents no longer have a 50-turn default cap. The default is unlimited (no turn limit), matching Claude Code's main loop behavior. Users can still set explicit limits per-agent via `max_turns` frontmatter or the Agent tool parameter, or globally via `/agents` → Settings (`0` = unlimited).
|
|
24
|
+
- **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
|
|
17
25
|
|
|
18
26
|
### Fixed
|
|
27
|
+
- **Tool name display** — `getAgentConversation` now reads `ToolCall.name` (the correct property) instead of `toolName`, resolving `[Tool: unknown]` in conversation viewer and verbose output.
|
|
19
28
|
- **Env test CI failure** — `detectEnv` test assumed a branch name exists, but CI checks out detached HEAD. Split into separate tests for repo detection and branch detection with a controlled temp repo.
|
|
20
29
|
|
|
21
30
|
## [0.4.9] - 2026-03-18
|
|
@@ -320,6 +329,7 @@ Initial release.
|
|
|
320
329
|
- **Thinking level** — per-agent extended thinking control
|
|
321
330
|
- **`/agent` and `/agents` commands**
|
|
322
331
|
|
|
332
|
+
[0.5.0]: https://github.com/tintinweb/pi-subagents/compare/v0.4.9...v0.5.0
|
|
323
333
|
[0.4.9]: https://github.com/tintinweb/pi-subagents/compare/v0.4.8...v0.4.9
|
|
324
334
|
[0.4.8]: https://github.com/tintinweb/pi-subagents/compare/v0.4.7...v0.4.8
|
|
325
335
|
[0.4.7]: https://github.com/tintinweb/pi-subagents/compare/v0.4.6...v0.4.7
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
29
29
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
30
30
|
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
|
|
31
31
|
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
32
|
-
- **Cross-extension RPC** — other pi extensions can spawn subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`). Emits `subagents:ready` on load
|
|
32
|
+
- **Cross-extension RPC** — other pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Standardized reply envelopes with protocol versioning. Emits `subagents:ready` on load
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
@@ -289,7 +289,9 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
289
289
|
|
|
290
290
|
## Cross-Extension RPC
|
|
291
291
|
|
|
292
|
-
Other pi extensions can spawn subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
292
|
+
Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
293
|
+
|
|
294
|
+
All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
|
|
293
295
|
|
|
294
296
|
### Discovery
|
|
295
297
|
|
|
@@ -297,19 +299,19 @@ Listen for `subagents:ready` to know when RPC handlers are available:
|
|
|
297
299
|
|
|
298
300
|
```typescript
|
|
299
301
|
pi.events.on("subagents:ready", () => {
|
|
300
|
-
// RPC handlers are registered — safe to call ping/spawn
|
|
302
|
+
// RPC handlers are registered — safe to call ping/spawn/stop
|
|
301
303
|
});
|
|
302
304
|
```
|
|
303
305
|
|
|
304
306
|
### Ping
|
|
305
307
|
|
|
306
|
-
Check if the subagents extension is loaded:
|
|
308
|
+
Check if the subagents extension is loaded and get the protocol version:
|
|
307
309
|
|
|
308
310
|
```typescript
|
|
309
311
|
const requestId = crypto.randomUUID();
|
|
310
|
-
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, () => {
|
|
312
|
+
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
|
|
311
313
|
unsub();
|
|
312
|
-
|
|
314
|
+
if (reply.success) console.log("Protocol version:", reply.data.version);
|
|
313
315
|
});
|
|
314
316
|
pi.events.emit("subagents:rpc:ping", { requestId });
|
|
315
317
|
```
|
|
@@ -322,10 +324,10 @@ Spawn a subagent and receive its ID:
|
|
|
322
324
|
const requestId = crypto.randomUUID();
|
|
323
325
|
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
324
326
|
unsub();
|
|
325
|
-
if (reply.
|
|
327
|
+
if (!reply.success) {
|
|
326
328
|
console.error("Spawn failed:", reply.error);
|
|
327
329
|
} else {
|
|
328
|
-
console.log("Agent ID:", reply.id);
|
|
330
|
+
console.log("Agent ID:", reply.data.id);
|
|
329
331
|
}
|
|
330
332
|
});
|
|
331
333
|
pi.events.emit("subagents:rpc:spawn", {
|
|
@@ -336,6 +338,19 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
336
338
|
});
|
|
337
339
|
```
|
|
338
340
|
|
|
341
|
+
### Stop
|
|
342
|
+
|
|
343
|
+
Stop a running agent by ID:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
const requestId = crypto.randomUUID();
|
|
347
|
+
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
|
|
348
|
+
unsub();
|
|
349
|
+
if (!reply.success) console.error("Stop failed:", reply.error);
|
|
350
|
+
});
|
|
351
|
+
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
352
|
+
```
|
|
353
|
+
|
|
339
354
|
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
340
355
|
|
|
341
356
|
## Persistent Agent Memory
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* Excess agents are queued and auto-started as running agents complete.
|
|
6
6
|
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
7
|
*/
|
|
8
|
-
import type { ExtensionContext, ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
9
8
|
import type { Model } from "@mariozechner/pi-ai";
|
|
10
|
-
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
11
10
|
import { type ToolActivity } from "./agent-runner.js";
|
|
12
|
-
import type {
|
|
11
|
+
import type { AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
|
|
13
12
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
13
|
+
export type OnAgentStart = (record: AgentRecord) => void;
|
|
14
14
|
interface SpawnOptions {
|
|
15
15
|
description: string;
|
|
16
16
|
model?: Model<any>;
|
|
@@ -19,23 +19,28 @@ interface SpawnOptions {
|
|
|
19
19
|
inheritContext?: boolean;
|
|
20
20
|
thinkingLevel?: ThinkingLevel;
|
|
21
21
|
isBackground?: boolean;
|
|
22
|
+
/** Isolation mode — "worktree" creates a temp git worktree for the agent. */
|
|
23
|
+
isolation?: IsolationMode;
|
|
22
24
|
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
23
25
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
24
26
|
/** Called on streaming text deltas from the assistant response. */
|
|
25
27
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
26
28
|
/** Called when the agent session is created (for accessing session stats). */
|
|
27
29
|
onSessionCreated?: (session: AgentSession) => void;
|
|
30
|
+
/** Called at the end of each agentic turn with the cumulative count. */
|
|
31
|
+
onTurnEnd?: (turnCount: number) => void;
|
|
28
32
|
}
|
|
29
33
|
export declare class AgentManager {
|
|
30
34
|
private agents;
|
|
31
35
|
private cleanupInterval;
|
|
32
36
|
private onComplete?;
|
|
37
|
+
private onStart?;
|
|
33
38
|
private maxConcurrent;
|
|
34
39
|
/** Queue of background agents waiting to start. */
|
|
35
40
|
private queue;
|
|
36
41
|
/** Number of currently running background agents. */
|
|
37
42
|
private runningBackground;
|
|
38
|
-
constructor(onComplete?: OnAgentComplete, maxConcurrent?: number);
|
|
43
|
+
constructor(onComplete?: OnAgentComplete, maxConcurrent?: number, onStart?: OnAgentStart);
|
|
39
44
|
/** Update the max concurrent background agents limit. */
|
|
40
45
|
setMaxConcurrent(n: number): void;
|
|
41
46
|
getMaxConcurrent(): number;
|
|
@@ -60,9 +65,18 @@ export declare class AgentManager {
|
|
|
60
65
|
getRecord(id: string): AgentRecord | undefined;
|
|
61
66
|
listAgents(): AgentRecord[];
|
|
62
67
|
abort(id: string): boolean;
|
|
68
|
+
/** Dispose a record's session and remove it from the map. */
|
|
69
|
+
private removeRecord;
|
|
63
70
|
private cleanup;
|
|
71
|
+
/**
|
|
72
|
+
* Remove all completed/stopped/errored records immediately.
|
|
73
|
+
* Called on session start/switch so tasks from a prior session don't persist.
|
|
74
|
+
*/
|
|
75
|
+
clearCompleted(): void;
|
|
64
76
|
/** Whether any agents are still running or queued. */
|
|
65
77
|
hasRunning(): boolean;
|
|
78
|
+
/** Abort all running and queued agents immediately. */
|
|
79
|
+
abortAll(): number;
|
|
66
80
|
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
67
81
|
waitForAll(): Promise<void>;
|
|
68
82
|
dispose(): void;
|
package/dist/agent-manager.js
CHANGED
|
@@ -6,20 +6,23 @@
|
|
|
6
6
|
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
-
import {
|
|
9
|
+
import { resumeAgent, runAgent } from "./agent-runner.js";
|
|
10
|
+
import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
|
|
10
11
|
/** Default max concurrent background agents. */
|
|
11
12
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
12
13
|
export class AgentManager {
|
|
13
14
|
agents = new Map();
|
|
14
15
|
cleanupInterval;
|
|
15
16
|
onComplete;
|
|
17
|
+
onStart;
|
|
16
18
|
maxConcurrent;
|
|
17
19
|
/** Queue of background agents waiting to start. */
|
|
18
20
|
queue = [];
|
|
19
21
|
/** Number of currently running background agents. */
|
|
20
22
|
runningBackground = 0;
|
|
21
|
-
constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT) {
|
|
23
|
+
constructor(onComplete, maxConcurrent = DEFAULT_MAX_CONCURRENT, onStart) {
|
|
22
24
|
this.onComplete = onComplete;
|
|
25
|
+
this.onStart = onStart;
|
|
23
26
|
this.maxConcurrent = maxConcurrent;
|
|
24
27
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
25
28
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
@@ -65,22 +68,47 @@ export class AgentManager {
|
|
|
65
68
|
record.startedAt = Date.now();
|
|
66
69
|
if (options.isBackground)
|
|
67
70
|
this.runningBackground++;
|
|
68
|
-
|
|
71
|
+
this.onStart?.(record);
|
|
72
|
+
// Worktree isolation: create a temporary git worktree if requested
|
|
73
|
+
let worktreeCwd;
|
|
74
|
+
let worktreeWarning = "";
|
|
75
|
+
if (options.isolation === "worktree") {
|
|
76
|
+
const wt = createWorktree(ctx.cwd, id);
|
|
77
|
+
if (wt) {
|
|
78
|
+
record.worktree = wt;
|
|
79
|
+
worktreeCwd = wt.path;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
worktreeWarning = "\n\n[WARNING: Worktree isolation was requested but failed (not a git repo, or no commits yet). Running in the main working directory instead.]";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Prepend worktree warning to prompt if isolation failed
|
|
86
|
+
const effectivePrompt = worktreeWarning ? worktreeWarning + "\n\n" + prompt : prompt;
|
|
87
|
+
const promise = runAgent(ctx, type, effectivePrompt, {
|
|
69
88
|
pi,
|
|
70
89
|
model: options.model,
|
|
71
90
|
maxTurns: options.maxTurns,
|
|
72
91
|
isolated: options.isolated,
|
|
73
92
|
inheritContext: options.inheritContext,
|
|
74
93
|
thinkingLevel: options.thinkingLevel,
|
|
94
|
+
cwd: worktreeCwd,
|
|
75
95
|
signal: record.abortController.signal,
|
|
76
96
|
onToolActivity: (activity) => {
|
|
77
97
|
if (activity.type === "end")
|
|
78
98
|
record.toolUses++;
|
|
79
99
|
options.onToolActivity?.(activity);
|
|
80
100
|
},
|
|
101
|
+
onTurnEnd: options.onTurnEnd,
|
|
81
102
|
onTextDelta: options.onTextDelta,
|
|
82
103
|
onSessionCreated: (session) => {
|
|
83
104
|
record.session = session;
|
|
105
|
+
// Flush any steers that arrived before the session was ready
|
|
106
|
+
if (record.pendingSteers?.length) {
|
|
107
|
+
for (const msg of record.pendingSteers) {
|
|
108
|
+
session.steer(msg).catch(() => { });
|
|
109
|
+
}
|
|
110
|
+
record.pendingSteers = undefined;
|
|
111
|
+
}
|
|
84
112
|
options.onSessionCreated?.(session);
|
|
85
113
|
},
|
|
86
114
|
})
|
|
@@ -92,6 +120,23 @@ export class AgentManager {
|
|
|
92
120
|
record.result = responseText;
|
|
93
121
|
record.session = session;
|
|
94
122
|
record.completedAt ??= Date.now();
|
|
123
|
+
// Final flush of streaming output file
|
|
124
|
+
if (record.outputCleanup) {
|
|
125
|
+
try {
|
|
126
|
+
record.outputCleanup();
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore */ }
|
|
129
|
+
record.outputCleanup = undefined;
|
|
130
|
+
}
|
|
131
|
+
// Clean up worktree if used
|
|
132
|
+
if (record.worktree) {
|
|
133
|
+
const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
|
|
134
|
+
record.worktreeResult = wtResult;
|
|
135
|
+
if (wtResult.hasChanges && wtResult.branch) {
|
|
136
|
+
record.result = (record.result ?? "") +
|
|
137
|
+
`\n\n---\nChanges saved to branch \`${wtResult.branch}\`. Merge with: \`git merge ${wtResult.branch}\``;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
95
140
|
if (options.isBackground) {
|
|
96
141
|
this.runningBackground--;
|
|
97
142
|
this.onComplete?.(record);
|
|
@@ -106,6 +151,22 @@ export class AgentManager {
|
|
|
106
151
|
}
|
|
107
152
|
record.error = err instanceof Error ? err.message : String(err);
|
|
108
153
|
record.completedAt ??= Date.now();
|
|
154
|
+
// Final flush of streaming output file on error
|
|
155
|
+
if (record.outputCleanup) {
|
|
156
|
+
try {
|
|
157
|
+
record.outputCleanup();
|
|
158
|
+
}
|
|
159
|
+
catch { /* ignore */ }
|
|
160
|
+
record.outputCleanup = undefined;
|
|
161
|
+
}
|
|
162
|
+
// Best-effort worktree cleanup on error
|
|
163
|
+
if (record.worktree) {
|
|
164
|
+
try {
|
|
165
|
+
const wtResult = cleanupWorktree(ctx.cwd, record.worktree, options.description);
|
|
166
|
+
record.worktreeResult = wtResult;
|
|
167
|
+
}
|
|
168
|
+
catch { /* ignore cleanup errors */ }
|
|
169
|
+
}
|
|
109
170
|
if (options.isBackground) {
|
|
110
171
|
this.runningBackground--;
|
|
111
172
|
this.onComplete?.(record);
|
|
@@ -190,6 +251,12 @@ export class AgentManager {
|
|
|
190
251
|
record.completedAt = Date.now();
|
|
191
252
|
return true;
|
|
192
253
|
}
|
|
254
|
+
/** Dispose a record's session and remove it from the map. */
|
|
255
|
+
removeRecord(id, record) {
|
|
256
|
+
record.session?.dispose?.();
|
|
257
|
+
record.session = undefined;
|
|
258
|
+
this.agents.delete(id);
|
|
259
|
+
}
|
|
193
260
|
cleanup() {
|
|
194
261
|
const cutoff = Date.now() - 10 * 60_000;
|
|
195
262
|
for (const [id, record] of this.agents) {
|
|
@@ -197,18 +264,48 @@ export class AgentManager {
|
|
|
197
264
|
continue;
|
|
198
265
|
if ((record.completedAt ?? 0) >= cutoff)
|
|
199
266
|
continue;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
267
|
+
this.removeRecord(id, record);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Remove all completed/stopped/errored records immediately.
|
|
272
|
+
* Called on session start/switch so tasks from a prior session don't persist.
|
|
273
|
+
*/
|
|
274
|
+
clearCompleted() {
|
|
275
|
+
for (const [id, record] of this.agents) {
|
|
276
|
+
if (record.status === "running" || record.status === "queued")
|
|
277
|
+
continue;
|
|
278
|
+
this.removeRecord(id, record);
|
|
206
279
|
}
|
|
207
280
|
}
|
|
208
281
|
/** Whether any agents are still running or queued. */
|
|
209
282
|
hasRunning() {
|
|
210
283
|
return [...this.agents.values()].some(r => r.status === "running" || r.status === "queued");
|
|
211
284
|
}
|
|
285
|
+
/** Abort all running and queued agents immediately. */
|
|
286
|
+
abortAll() {
|
|
287
|
+
let count = 0;
|
|
288
|
+
// Clear queued agents first
|
|
289
|
+
for (const queued of this.queue) {
|
|
290
|
+
const record = this.agents.get(queued.id);
|
|
291
|
+
if (record) {
|
|
292
|
+
record.status = "stopped";
|
|
293
|
+
record.completedAt = Date.now();
|
|
294
|
+
count++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.queue = [];
|
|
298
|
+
// Abort running agents
|
|
299
|
+
for (const record of this.agents.values()) {
|
|
300
|
+
if (record.status === "running") {
|
|
301
|
+
record.abortController?.abort();
|
|
302
|
+
record.status = "stopped";
|
|
303
|
+
record.completedAt = Date.now();
|
|
304
|
+
count++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return count;
|
|
308
|
+
}
|
|
212
309
|
/** Wait for all running and queued agents to complete (including queued ones). */
|
|
213
310
|
async waitForAll() {
|
|
214
311
|
// Loop because drainQueue respects the concurrency limit — as running
|
|
@@ -232,5 +329,10 @@ export class AgentManager {
|
|
|
232
329
|
record.session?.dispose();
|
|
233
330
|
}
|
|
234
331
|
this.agents.clear();
|
|
332
|
+
// Prune any orphaned git worktrees (crash recovery)
|
|
333
|
+
try {
|
|
334
|
+
pruneWorktrees(process.cwd());
|
|
335
|
+
}
|
|
336
|
+
catch { /* ignore */ }
|
|
235
337
|
}
|
|
236
338
|
}
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
|
-
import { type AgentSession, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
4
|
import type { Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { type AgentSession, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
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
|
|
11
|
-
export declare function setDefaultMaxTurns(n: number): void;
|
|
8
|
+
/** Get the default max turns value. undefined = unlimited. */
|
|
9
|
+
export declare function getDefaultMaxTurns(): number | undefined;
|
|
10
|
+
/** Set the default max turns value. undefined = unlimited, otherwise minimum 1. */
|
|
11
|
+
export declare function setDefaultMaxTurns(n: number | undefined): void;
|
|
12
12
|
/** Get the grace turns value. */
|
|
13
13
|
export declare function getGraceTurns(): number;
|
|
14
14
|
/** Set the grace turns value (minimum 1). */
|
|
@@ -27,11 +27,15 @@ export interface RunOptions {
|
|
|
27
27
|
isolated?: boolean;
|
|
28
28
|
inheritContext?: boolean;
|
|
29
29
|
thinkingLevel?: ThinkingLevel;
|
|
30
|
+
/** Override working directory (e.g. for worktree isolation). */
|
|
31
|
+
cwd?: string;
|
|
30
32
|
/** Called on tool start/end with activity info. */
|
|
31
33
|
onToolActivity?: (activity: ToolActivity) => void;
|
|
32
34
|
/** Called on streaming text deltas from the assistant response. */
|
|
33
35
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
34
36
|
onSessionCreated?: (session: AgentSession) => void;
|
|
37
|
+
/** Called at the end of each agentic turn with the cumulative count. */
|
|
38
|
+
onTurnEnd?: (turnCount: number) => void;
|
|
35
39
|
}
|
|
36
40
|
export interface RunResult {
|
|
37
41
|
responseText: string;
|
package/dist/agent-runner.js
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*/
|
|
4
4
|
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
import { buildAgentPrompt } from "./prompts.js";
|
|
5
|
+
import { getAgentConfig, getConfig, getMemoryTools, getReadOnlyMemoryTools, getToolsForType } from "./agent-types.js";
|
|
7
6
|
import { buildParentContext, extractText } from "./context.js";
|
|
8
7
|
import { detectEnv } from "./env.js";
|
|
8
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
9
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
10
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
9
11
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
10
12
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
11
|
-
/** Default max turns
|
|
12
|
-
let defaultMaxTurns
|
|
13
|
-
/** Get the default max turns value. */
|
|
13
|
+
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
14
|
+
let defaultMaxTurns;
|
|
15
|
+
/** Get the default max turns value. undefined = unlimited. */
|
|
14
16
|
export function getDefaultMaxTurns() { return defaultMaxTurns; }
|
|
15
|
-
/** Set the default max turns value
|
|
16
|
-
export function setDefaultMaxTurns(n) { defaultMaxTurns = Math.max(1, n); }
|
|
17
|
+
/** Set the default max turns value. undefined = unlimited, otherwise minimum 1. */
|
|
18
|
+
export function setDefaultMaxTurns(n) { defaultMaxTurns = n != null ? Math.max(1, n) : undefined; }
|
|
17
19
|
/** Additional turns allowed after the soft limit steer message. */
|
|
18
20
|
let graceTurns = 5;
|
|
19
21
|
/** Get the grace turns value. */
|
|
@@ -73,13 +75,52 @@ function forwardAbortSignal(session, signal) {
|
|
|
73
75
|
export async function runAgent(ctx, type, prompt, options) {
|
|
74
76
|
const config = getConfig(type);
|
|
75
77
|
const agentConfig = getAgentConfig(type);
|
|
76
|
-
|
|
78
|
+
// Resolve working directory: worktree override > parent cwd
|
|
79
|
+
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
80
|
+
const env = await detectEnv(options.pi, effectiveCwd);
|
|
77
81
|
// Get parent system prompt for append-mode agents
|
|
78
82
|
const parentSystemPrompt = ctx.getSystemPrompt();
|
|
83
|
+
// Build prompt extras (memory, skill preloading)
|
|
84
|
+
const extras = {};
|
|
85
|
+
// Resolve extensions/skills: isolated overrides to false
|
|
86
|
+
const extensions = options.isolated ? false : config.extensions;
|
|
87
|
+
const skills = options.isolated ? false : config.skills;
|
|
88
|
+
// Skill preloading: when skills is string[], preload their content into prompt
|
|
89
|
+
if (Array.isArray(skills)) {
|
|
90
|
+
const loaded = preloadSkills(skills, effectiveCwd);
|
|
91
|
+
if (loaded.length > 0) {
|
|
92
|
+
extras.skillBlocks = loaded;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let tools = getToolsForType(type, effectiveCwd);
|
|
96
|
+
// Persistent memory: detect write capability and branch accordingly.
|
|
97
|
+
// Account for disallowedTools — a tool in the base set but on the denylist is not truly available.
|
|
98
|
+
if (agentConfig?.memory) {
|
|
99
|
+
const existingNames = new Set(tools.map(t => t.name));
|
|
100
|
+
const denied = agentConfig.disallowedTools ? new Set(agentConfig.disallowedTools) : undefined;
|
|
101
|
+
const effectivelyHas = (name) => existingNames.has(name) && !denied?.has(name);
|
|
102
|
+
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
103
|
+
if (hasWriteTools) {
|
|
104
|
+
// Read-write memory: add any missing memory tools (read/write/edit)
|
|
105
|
+
const memTools = getMemoryTools(effectiveCwd, existingNames);
|
|
106
|
+
if (memTools.length > 0)
|
|
107
|
+
tools = [...tools, ...memTools];
|
|
108
|
+
extras.memoryBlock = buildMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Read-only memory: only add read tool, use read-only prompt
|
|
112
|
+
if (!existingNames.has("read")) {
|
|
113
|
+
const readTools = getReadOnlyMemoryTools(effectiveCwd, existingNames);
|
|
114
|
+
if (readTools.length > 0)
|
|
115
|
+
tools = [...tools, ...readTools];
|
|
116
|
+
}
|
|
117
|
+
extras.memoryBlock = buildReadOnlyMemoryBlock(agentConfig.name, agentConfig.memory, effectiveCwd);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
79
120
|
// Build system prompt from agent config
|
|
80
121
|
let systemPrompt;
|
|
81
122
|
if (agentConfig) {
|
|
82
|
-
systemPrompt = buildAgentPrompt(agentConfig,
|
|
123
|
+
systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, parentSystemPrompt, extras);
|
|
83
124
|
}
|
|
84
125
|
else {
|
|
85
126
|
// Unknown type fallback: general-purpose (defensive — unreachable in practice
|
|
@@ -94,17 +135,16 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
94
135
|
inheritContext: false,
|
|
95
136
|
runInBackground: false,
|
|
96
137
|
isolated: false,
|
|
97
|
-
},
|
|
138
|
+
}, effectiveCwd, env, parentSystemPrompt, extras);
|
|
98
139
|
}
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
const
|
|
102
|
-
const skills = options.isolated ? false : config.skills;
|
|
140
|
+
// When skills is string[], we've already preloaded them into the prompt.
|
|
141
|
+
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
142
|
+
const noSkills = skills === false || Array.isArray(skills);
|
|
103
143
|
// Load extensions/skills: true or string[] → load; false → don't
|
|
104
144
|
const loader = new DefaultResourceLoader({
|
|
105
|
-
cwd:
|
|
145
|
+
cwd: effectiveCwd,
|
|
106
146
|
noExtensions: extensions === false,
|
|
107
|
-
noSkills
|
|
147
|
+
noSkills,
|
|
108
148
|
noPromptTemplates: true,
|
|
109
149
|
noThemes: true,
|
|
110
150
|
systemPromptOverride: () => systemPrompt,
|
|
@@ -115,8 +155,8 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
115
155
|
// Resolve thinking level: explicit option > agent config > undefined (inherit)
|
|
116
156
|
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
|
|
117
157
|
const sessionOpts = {
|
|
118
|
-
cwd:
|
|
119
|
-
sessionManager: SessionManager.inMemory(
|
|
158
|
+
cwd: effectiveCwd,
|
|
159
|
+
sessionManager: SessionManager.inMemory(effectiveCwd),
|
|
120
160
|
settingsManager: SettingsManager.create(),
|
|
121
161
|
modelRegistry: ctx.modelRegistry,
|
|
122
162
|
model,
|
|
@@ -128,13 +168,19 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
128
168
|
}
|
|
129
169
|
// createAgentSession's type signature may not include thinkingLevel yet
|
|
130
170
|
const { session } = await createAgentSession(sessionOpts);
|
|
171
|
+
// Build disallowed tools set from agent config
|
|
172
|
+
const disallowedSet = agentConfig?.disallowedTools
|
|
173
|
+
? new Set(agentConfig.disallowedTools)
|
|
174
|
+
: undefined;
|
|
131
175
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
132
|
-
//
|
|
176
|
+
// apply extension allowlist if specified, and apply disallowedTools denylist
|
|
133
177
|
if (extensions !== false) {
|
|
134
178
|
const builtinToolNames = new Set(tools.map(t => t.name));
|
|
135
179
|
const activeTools = session.getActiveToolNames().filter((t) => {
|
|
136
180
|
if (EXCLUDED_TOOL_NAMES.includes(t))
|
|
137
181
|
return false;
|
|
182
|
+
if (disallowedSet?.has(t))
|
|
183
|
+
return false;
|
|
138
184
|
if (builtinToolNames.has(t))
|
|
139
185
|
return true;
|
|
140
186
|
if (Array.isArray(extensions)) {
|
|
@@ -144,6 +190,11 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
144
190
|
});
|
|
145
191
|
session.setActiveToolsByName(activeTools);
|
|
146
192
|
}
|
|
193
|
+
else if (disallowedSet) {
|
|
194
|
+
// Even with extensions disabled, apply denylist to built-in tools
|
|
195
|
+
const activeTools = session.getActiveToolNames().filter(t => !disallowedSet.has(t));
|
|
196
|
+
session.setActiveToolsByName(activeTools);
|
|
197
|
+
}
|
|
147
198
|
options.onSessionCreated?.(session);
|
|
148
199
|
// Track turns for graceful max_turns enforcement
|
|
149
200
|
let turnCount = 0;
|
|
@@ -154,13 +205,16 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
154
205
|
const unsubTurns = session.subscribe((event) => {
|
|
155
206
|
if (event.type === "turn_end") {
|
|
156
207
|
turnCount++;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
208
|
+
options.onTurnEnd?.(turnCount);
|
|
209
|
+
if (maxTurns != null) {
|
|
210
|
+
if (!softLimitReached && turnCount >= maxTurns) {
|
|
211
|
+
softLimitReached = true;
|
|
212
|
+
session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
|
|
213
|
+
}
|
|
214
|
+
else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
|
|
215
|
+
aborted = true;
|
|
216
|
+
session.abort();
|
|
217
|
+
}
|
|
164
218
|
}
|
|
165
219
|
}
|
|
166
220
|
if (event.type === "message_start") {
|
|
@@ -248,7 +302,7 @@ export function getAgentConversation(session) {
|
|
|
248
302
|
if (c.type === "text" && c.text)
|
|
249
303
|
textParts.push(c.text);
|
|
250
304
|
else if (c.type === "toolCall")
|
|
251
|
-
toolCalls.push(` Tool: ${c.toolName ?? "unknown"}`);
|
|
305
|
+
toolCalls.push(` Tool: ${c.name ?? c.toolName ?? "unknown"}`);
|
|
252
306
|
}
|
|
253
307
|
if (textParts.length > 0)
|
|
254
308
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
package/dist/agent-types.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ export declare function getDefaultAgentNames(): string[];
|
|
|
28
28
|
export declare function getUserAgentNames(): string[];
|
|
29
29
|
/** Check if a type is valid and enabled (case-insensitive). */
|
|
30
30
|
export declare function isValidType(type: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Get the tools needed for memory management (read, write, edit).
|
|
33
|
+
* Only returns tools that are NOT already in the provided set.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getMemoryTools(cwd: string, existingToolNames: Set<string>): AgentTool<any>[];
|
|
36
|
+
/**
|
|
37
|
+
* Get only the read tool for read-only memory access.
|
|
38
|
+
* Only returns tools that are NOT already in the provided set.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getReadOnlyMemoryTools(cwd: string, existingToolNames: Set<string>): AgentTool<any>[];
|
|
31
41
|
/** Get built-in tools for a type (case-insensitive). */
|
|
32
42
|
export declare function getToolsForType(type: string, cwd: string): AgentTool<any>[];
|
|
33
43
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
package/dist/agent-types.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Merges embedded default agents with user-defined agents from .pi/agents/*.md.
|
|
5
5
|
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { createBashTool, createEditTool, createFindTool, createGrepTool, createLsTool, createReadTool, createWriteTool, } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
9
9
|
const TOOL_FACTORIES = {
|
|
10
10
|
read: (cwd) => createReadTool(cwd),
|
|
@@ -84,6 +84,28 @@ export function isValidType(type) {
|
|
|
84
84
|
return false;
|
|
85
85
|
return agents.get(key)?.enabled !== false;
|
|
86
86
|
}
|
|
87
|
+
/** Tool names required for memory management. */
|
|
88
|
+
const MEMORY_TOOL_NAMES = ["read", "write", "edit"];
|
|
89
|
+
/**
|
|
90
|
+
* Get the tools needed for memory management (read, write, edit).
|
|
91
|
+
* Only returns tools that are NOT already in the provided set.
|
|
92
|
+
*/
|
|
93
|
+
export function getMemoryTools(cwd, existingToolNames) {
|
|
94
|
+
return MEMORY_TOOL_NAMES
|
|
95
|
+
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
96
|
+
.map(n => TOOL_FACTORIES[n](cwd));
|
|
97
|
+
}
|
|
98
|
+
/** Tool names needed for read-only memory access. */
|
|
99
|
+
const READONLY_MEMORY_TOOL_NAMES = ["read"];
|
|
100
|
+
/**
|
|
101
|
+
* Get only the read tool for read-only memory access.
|
|
102
|
+
* Only returns tools that are NOT already in the provided set.
|
|
103
|
+
*/
|
|
104
|
+
export function getReadOnlyMemoryTools(cwd, existingToolNames) {
|
|
105
|
+
return READONLY_MEMORY_TOOL_NAMES
|
|
106
|
+
.filter(n => !existingToolNames.has(n) && n in TOOL_FACTORIES)
|
|
107
|
+
.map(n => TOOL_FACTORIES[n](cwd));
|
|
108
|
+
}
|
|
87
109
|
/** Get built-in tools for a type (case-insensitive). */
|
|
88
110
|
export function getToolsForType(type, cwd) {
|
|
89
111
|
const key = resolveKey(type);
|