@tintinweb/pi-subagents 0.4.11 → 0.5.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 +23 -7
- package/README.md +26 -13
- package/dist/agent-runner.d.ts +3 -1
- package/dist/agent-runner.js +25 -6
- package/dist/cross-extension-rpc.d.ts +19 -3
- package/dist/cross-extension-rpc.js +40 -19
- package/dist/custom-agents.js +7 -7
- package/dist/index.js +28 -33
- package/dist/invocation-config.d.ts +22 -0
- package/dist/invocation-config.js +15 -0
- package/dist/types.d.ts +6 -6
- package/dist/ui/conversation-viewer.js +1 -1
- package/dist/ui/conversation-viewer.test.js +1 -1
- package/package.json +6 -6
- package/src/agent-runner.ts +24 -6
- package/src/cross-extension-rpc.ts +57 -23
- package/src/custom-agents.ts +7 -7
- package/src/index.ts +29 -36
- package/src/invocation-config.ts +40 -0
- package/src/types.ts +6 -6
- package/src/ui/conversation-viewer.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,22 +5,37 @@ 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
|
-
- **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
|
|
12
|
-
|
|
13
|
-
## [0.4.10] - 2026-03-18
|
|
10
|
+
## [0.5.1] - 2026-03-24
|
|
14
11
|
|
|
15
12
|
### Changed
|
|
16
|
-
- **
|
|
17
|
-
-
|
|
13
|
+
- **Agent config is authoritative** — frontmatter values for `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, and `isolation` now take precedence over `Agent` tool-call parameters. Tool-call params only fill fields the agent config leaves unspecified.
|
|
14
|
+
- **`join_mode` is now a global setting only** — removed the per-call `join_mode` parameter from the `Agent` tool. Join behavior is configured via `/agents` → Settings → Join mode.
|
|
15
|
+
- **`max_turns: 0` means unlimited** — agent files can now explicitly set `max_turns: 0` to lock unlimited turns. Previously `0` was silently clamped to `1`.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Final subagent text preserved from non-streaming providers** — agents using providers that return the final message without streaming `text_delta` events no longer return empty results. Falls back to extracting text from the completed session history.
|
|
19
|
+
- **`effectiveMaxTurns` passed to spawn calls** — previously `params.max_turns` was passed raw to both foreground and background spawn, bypassing the agent config entirely.
|
|
20
|
+
|
|
21
|
+
## [0.5.0] - 2026-03-22
|
|
18
22
|
|
|
19
23
|
### Added
|
|
24
|
+
- **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.
|
|
25
|
+
- **`abort` in `SpawnCapable` interface** — cross-extension RPC consumers can now stop agents, not just spawn them.
|
|
26
|
+
- **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.
|
|
20
27
|
- **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.
|
|
21
28
|
- **CI workflow** — GitHub Actions runs lint, typecheck, and tests on push to master and PRs.
|
|
29
|
+
- **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.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- **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.
|
|
33
|
+
- **Protocol versioning via ping** — ping reply now includes `{ version: PROTOCOL_VERSION }` (currently v2). Callers can detect version mismatches and warn users to update.
|
|
34
|
+
- **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).
|
|
35
|
+
- **Stale dist in published package** — added `prepublishOnly` hook to build fresh `dist/` on every `npm publish`.
|
|
22
36
|
|
|
23
37
|
### Fixed
|
|
38
|
+
- **Tool name display** — `getAgentConversation` now reads `ToolCall.name` (the correct property) instead of `toolName`, resolving `[Tool: unknown]` in conversation viewer and verbose output.
|
|
24
39
|
- **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.
|
|
25
40
|
|
|
26
41
|
## [0.4.9] - 2026-03-18
|
|
@@ -325,6 +340,7 @@ Initial release.
|
|
|
325
340
|
- **Thinking level** — per-agent extended thinking control
|
|
326
341
|
- **`/agent` and `/agents` commands**
|
|
327
342
|
|
|
343
|
+
[0.5.0]: https://github.com/tintinweb/pi-subagents/compare/v0.4.9...v0.5.0
|
|
328
344
|
[0.4.9]: https://github.com/tintinweb/pi-subagents/compare/v0.4.8...v0.4.9
|
|
329
345
|
[0.4.8]: https://github.com/tintinweb/pi-subagents/compare/v0.4.7...v0.4.8
|
|
330
346
|
[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
|
|
|
@@ -170,7 +170,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
170
170
|
| `isolated` | `false` | No extension/MCP tools, only built-in |
|
|
171
171
|
| `enabled` | `true` | Set to `false` to disable an agent (useful for hiding a default agent per-project) |
|
|
172
172
|
|
|
173
|
-
Frontmatter
|
|
173
|
+
Frontmatter is authoritative. If an agent file sets `model`, `thinking`, `max_turns`, `inherit_context`, `run_in_background`, `isolated`, or `isolation`, those values are locked for that agent. `Agent` tool parameters only fill fields the agent config leaves unspecified.
|
|
174
174
|
|
|
175
175
|
## Tools
|
|
176
176
|
|
|
@@ -191,7 +191,6 @@ Launch a sub-agent.
|
|
|
191
191
|
| `isolated` | boolean | no | No extension/MCP tools |
|
|
192
192
|
| `isolation` | `"worktree"` | no | Run in an isolated git worktree |
|
|
193
193
|
| `inherit_context` | boolean | no | Fork parent conversation into agent |
|
|
194
|
-
| `join_mode` | `"async"` \| `"group"` | no | Override join strategy for background completion notifications (default: smart) |
|
|
195
194
|
|
|
196
195
|
### `get_subagent_result`
|
|
197
196
|
|
|
@@ -260,7 +259,7 @@ Foreground agents bypass the queue — they block the parent anyway.
|
|
|
260
259
|
|
|
261
260
|
## Join Strategies
|
|
262
261
|
|
|
263
|
-
When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered
|
|
262
|
+
When background agents complete, they notify the main agent. The **join mode** controls how these notifications are delivered. It applies only to background agents.
|
|
264
263
|
|
|
265
264
|
| Mode | Behavior |
|
|
266
265
|
|------|----------|
|
|
@@ -271,8 +270,7 @@ When background agents complete, they notify the main agent. The **join mode** c
|
|
|
271
270
|
**Timeout behavior:** When agents are grouped, a 30-second timeout starts after the first agent completes. If not all agents finish in time, a partial notification is sent with completed results and remaining agents continue with a shorter 15-second re-batch window for stragglers.
|
|
272
271
|
|
|
273
272
|
**Configuration:**
|
|
274
|
-
-
|
|
275
|
-
- Global default: `/agents` → Settings → Join mode
|
|
273
|
+
- Configure join mode in `/agents` → Settings → Join mode
|
|
276
274
|
|
|
277
275
|
## Events
|
|
278
276
|
|
|
@@ -289,7 +287,9 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
|
|
|
289
287
|
|
|
290
288
|
## Cross-Extension RPC
|
|
291
289
|
|
|
292
|
-
Other pi extensions can spawn subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
290
|
+
Other pi extensions can spawn and stop subagents programmatically via the `pi.events` event bus, without importing this package directly.
|
|
291
|
+
|
|
292
|
+
All RPC replies use a standardized envelope: `{ success: true, data?: T }` on success, `{ success: false, error: string }` on failure.
|
|
293
293
|
|
|
294
294
|
### Discovery
|
|
295
295
|
|
|
@@ -297,19 +297,19 @@ Listen for `subagents:ready` to know when RPC handlers are available:
|
|
|
297
297
|
|
|
298
298
|
```typescript
|
|
299
299
|
pi.events.on("subagents:ready", () => {
|
|
300
|
-
// RPC handlers are registered — safe to call ping/spawn
|
|
300
|
+
// RPC handlers are registered — safe to call ping/spawn/stop
|
|
301
301
|
});
|
|
302
302
|
```
|
|
303
303
|
|
|
304
304
|
### Ping
|
|
305
305
|
|
|
306
|
-
Check if the subagents extension is loaded:
|
|
306
|
+
Check if the subagents extension is loaded and get the protocol version:
|
|
307
307
|
|
|
308
308
|
```typescript
|
|
309
309
|
const requestId = crypto.randomUUID();
|
|
310
|
-
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, () => {
|
|
310
|
+
const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
|
|
311
311
|
unsub();
|
|
312
|
-
|
|
312
|
+
if (reply.success) console.log("Protocol version:", reply.data.version);
|
|
313
313
|
});
|
|
314
314
|
pi.events.emit("subagents:rpc:ping", { requestId });
|
|
315
315
|
```
|
|
@@ -322,10 +322,10 @@ Spawn a subagent and receive its ID:
|
|
|
322
322
|
const requestId = crypto.randomUUID();
|
|
323
323
|
const unsub = pi.events.on(`subagents:rpc:spawn:reply:${requestId}`, (reply) => {
|
|
324
324
|
unsub();
|
|
325
|
-
if (reply.
|
|
325
|
+
if (!reply.success) {
|
|
326
326
|
console.error("Spawn failed:", reply.error);
|
|
327
327
|
} else {
|
|
328
|
-
console.log("Agent ID:", reply.id);
|
|
328
|
+
console.log("Agent ID:", reply.data.id);
|
|
329
329
|
}
|
|
330
330
|
});
|
|
331
331
|
pi.events.emit("subagents:rpc:spawn", {
|
|
@@ -336,6 +336,19 @@ pi.events.emit("subagents:rpc:spawn", {
|
|
|
336
336
|
});
|
|
337
337
|
```
|
|
338
338
|
|
|
339
|
+
### Stop
|
|
340
|
+
|
|
341
|
+
Stop a running agent by ID:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
const requestId = crypto.randomUUID();
|
|
345
|
+
const unsub = pi.events.on(`subagents:rpc:stop:reply:${requestId}`, (reply) => {
|
|
346
|
+
unsub();
|
|
347
|
+
if (!reply.success) console.error("Stop failed:", reply.error);
|
|
348
|
+
});
|
|
349
|
+
pi.events.emit("subagents:rpc:stop", { requestId, agentId: "agent-id-here" });
|
|
350
|
+
```
|
|
351
|
+
|
|
339
352
|
Reply channels are scoped per `requestId`, so concurrent requests don't interfere.
|
|
340
353
|
|
|
341
354
|
## Persistent Agent Memory
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -5,9 +5,11 @@ import type { Model } from "@mariozechner/pi-ai";
|
|
|
5
5
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { type AgentSession, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
7
7
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
8
|
+
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
9
|
+
export declare function normalizeMaxTurns(n: number | undefined): number | undefined;
|
|
8
10
|
/** Get the default max turns value. undefined = unlimited. */
|
|
9
11
|
export declare function getDefaultMaxTurns(): number | undefined;
|
|
10
|
-
/** Set the default max turns value. undefined = unlimited, otherwise minimum 1. */
|
|
12
|
+
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
11
13
|
export declare function setDefaultMaxTurns(n: number | undefined): void;
|
|
12
14
|
/** Get the grace turns value. */
|
|
13
15
|
export declare function getGraceTurns(): number;
|
package/dist/agent-runner.js
CHANGED
|
@@ -12,10 +12,16 @@ import { preloadSkills } from "./skill-loader.js";
|
|
|
12
12
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
13
13
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
14
14
|
let defaultMaxTurns;
|
|
15
|
+
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
16
|
+
export function normalizeMaxTurns(n) {
|
|
17
|
+
if (n == null || n === 0)
|
|
18
|
+
return undefined;
|
|
19
|
+
return Math.max(1, n);
|
|
20
|
+
}
|
|
15
21
|
/** Get the default max turns value. undefined = unlimited. */
|
|
16
22
|
export function getDefaultMaxTurns() { return defaultMaxTurns; }
|
|
17
|
-
/** Set the default max turns value. undefined = unlimited, otherwise minimum 1. */
|
|
18
|
-
export function setDefaultMaxTurns(n) { defaultMaxTurns =
|
|
23
|
+
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
24
|
+
export function setDefaultMaxTurns(n) { defaultMaxTurns = normalizeMaxTurns(n); }
|
|
19
25
|
/** Additional turns allowed after the soft limit steer message. */
|
|
20
26
|
let graceTurns = 5;
|
|
21
27
|
/** Get the grace turns value. */
|
|
@@ -61,6 +67,18 @@ function collectResponseText(session) {
|
|
|
61
67
|
});
|
|
62
68
|
return { getText: () => text, unsubscribe };
|
|
63
69
|
}
|
|
70
|
+
/** Get the last assistant text from the completed session history. */
|
|
71
|
+
function getLastAssistantText(session) {
|
|
72
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
73
|
+
const msg = session.messages[i];
|
|
74
|
+
if (msg.role !== "assistant")
|
|
75
|
+
continue;
|
|
76
|
+
const text = extractText(msg.content).trim();
|
|
77
|
+
if (text)
|
|
78
|
+
return text;
|
|
79
|
+
}
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
64
82
|
/**
|
|
65
83
|
* Wire an AbortSignal to abort a session.
|
|
66
84
|
* Returns a cleanup function to remove the listener.
|
|
@@ -198,7 +216,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
198
216
|
options.onSessionCreated?.(session);
|
|
199
217
|
// Track turns for graceful max_turns enforcement
|
|
200
218
|
let turnCount = 0;
|
|
201
|
-
const maxTurns = options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns;
|
|
219
|
+
const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns);
|
|
202
220
|
let softLimitReached = false;
|
|
203
221
|
let aborted = false;
|
|
204
222
|
let currentMessageText = "";
|
|
@@ -249,7 +267,8 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
249
267
|
collector.unsubscribe();
|
|
250
268
|
cleanupAbort();
|
|
251
269
|
}
|
|
252
|
-
|
|
270
|
+
const responseText = collector.getText().trim() || getLastAssistantText(session);
|
|
271
|
+
return { responseText, session, aborted, steered: softLimitReached };
|
|
253
272
|
}
|
|
254
273
|
/**
|
|
255
274
|
* Send a new prompt to an existing session (resume).
|
|
@@ -273,7 +292,7 @@ export async function resumeAgent(session, prompt, options = {}) {
|
|
|
273
292
|
unsubToolUse();
|
|
274
293
|
cleanupAbort();
|
|
275
294
|
}
|
|
276
|
-
return collector.getText();
|
|
295
|
+
return collector.getText().trim() || getLastAssistantText(session);
|
|
277
296
|
}
|
|
278
297
|
/**
|
|
279
298
|
* Send a steering message to a running subagent.
|
|
@@ -302,7 +321,7 @@ export function getAgentConversation(session) {
|
|
|
302
321
|
if (c.type === "text" && c.text)
|
|
303
322
|
textParts.push(c.text);
|
|
304
323
|
else if (c.type === "toolCall")
|
|
305
|
-
toolCalls.push(` Tool: ${c.toolName ?? "unknown"}`);
|
|
324
|
+
toolCalls.push(` Tool: ${c.name ?? c.toolName ?? "unknown"}`);
|
|
306
325
|
}
|
|
307
326
|
if (textParts.length > 0)
|
|
308
327
|
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
7
11
|
/** Minimal event bus interface needed by the RPC handlers. */
|
|
8
12
|
export interface EventBus {
|
|
9
13
|
on(event: string, handler: (data: unknown) => void): () => void;
|
|
10
14
|
emit(event: string, data: unknown): void;
|
|
11
15
|
}
|
|
12
|
-
/**
|
|
16
|
+
/** RPC reply envelope — matches pi-mono's RpcResponse shape. */
|
|
17
|
+
export type RpcReply<T = void> = {
|
|
18
|
+
success: true;
|
|
19
|
+
data?: T;
|
|
20
|
+
} | {
|
|
21
|
+
success: false;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
25
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
26
|
+
/** Minimal AgentManager interface needed by the spawn/stop RPCs. */
|
|
13
27
|
export interface SpawnCapable {
|
|
14
28
|
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
|
|
29
|
+
abort(id: string): boolean;
|
|
15
30
|
}
|
|
16
31
|
export interface RpcDeps {
|
|
17
32
|
events: EventBus;
|
|
@@ -22,9 +37,10 @@ export interface RpcDeps {
|
|
|
22
37
|
export interface RpcHandle {
|
|
23
38
|
unsubPing: () => void;
|
|
24
39
|
unsubSpawn: () => void;
|
|
40
|
+
unsubStop: () => void;
|
|
25
41
|
}
|
|
26
42
|
/**
|
|
27
|
-
* Register ping and
|
|
43
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
28
44
|
* Returns unsub functions for cleanup.
|
|
29
45
|
*/
|
|
30
46
|
export declare function registerRpcHandlers(deps: RpcDeps): RpcHandle;
|
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
11
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
12
|
+
export const PROTOCOL_VERSION = 2;
|
|
7
13
|
/**
|
|
8
|
-
*
|
|
14
|
+
* Wire a single RPC handler: listen on `channel`, run `fn(params)`,
|
|
15
|
+
* emit the reply envelope on `channel:reply:${requestId}`.
|
|
16
|
+
*/
|
|
17
|
+
function handleRpc(events, channel, fn) {
|
|
18
|
+
return events.on(channel, async (raw) => {
|
|
19
|
+
const params = raw;
|
|
20
|
+
try {
|
|
21
|
+
const data = await fn(params);
|
|
22
|
+
const reply = { success: true };
|
|
23
|
+
if (data !== undefined)
|
|
24
|
+
reply.data = data;
|
|
25
|
+
events.emit(`${channel}:reply:${params.requestId}`, reply);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
events.emit(`${channel}:reply:${params.requestId}`, {
|
|
29
|
+
success: false, error: err?.message ?? String(err),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
9
36
|
* Returns unsub functions for cleanup.
|
|
10
37
|
*/
|
|
11
38
|
export function registerRpcHandlers(deps) {
|
|
12
39
|
const { events, pi, getCtx, manager } = deps;
|
|
13
|
-
const unsubPing = events
|
|
14
|
-
|
|
15
|
-
events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
|
|
40
|
+
const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
|
|
41
|
+
return { version: PROTOCOL_VERSION };
|
|
16
42
|
});
|
|
17
|
-
const unsubSpawn = events
|
|
18
|
-
const { requestId, type, prompt, options } = raw;
|
|
43
|
+
const unsubSpawn = handleRpc(events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
19
44
|
const ctx = getCtx();
|
|
20
|
-
if (!ctx)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
catch (err) {
|
|
29
|
-
events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
|
|
30
|
-
}
|
|
45
|
+
if (!ctx)
|
|
46
|
+
throw new Error("No active session");
|
|
47
|
+
return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
|
|
48
|
+
});
|
|
49
|
+
const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
|
|
50
|
+
if (!manager.abort(agentId))
|
|
51
|
+
throw new Error("Agent not found");
|
|
31
52
|
});
|
|
32
|
-
return { unsubPing, unsubSpawn };
|
|
53
|
+
return { unsubPing, unsubSpawn, unsubStop };
|
|
33
54
|
}
|
package/dist/custom-agents.js
CHANGED
|
@@ -54,12 +54,12 @@ function loadFromDir(dir, agents, source) {
|
|
|
54
54
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
55
55
|
model: str(fm.model),
|
|
56
56
|
thinking: str(fm.thinking),
|
|
57
|
-
maxTurns:
|
|
57
|
+
maxTurns: nonNegativeInt(fm.max_turns),
|
|
58
58
|
systemPrompt: body.trim(),
|
|
59
59
|
promptMode: fm.prompt_mode === "append" ? "append" : "replace",
|
|
60
|
-
inheritContext: fm.inherit_context === true,
|
|
61
|
-
runInBackground: fm.run_in_background === true,
|
|
62
|
-
isolated: fm.isolated === true,
|
|
60
|
+
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
61
|
+
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
62
|
+
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
63
63
|
memory: parseMemory(fm.memory),
|
|
64
64
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
65
65
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
@@ -73,9 +73,9 @@ function loadFromDir(dir, agents, source) {
|
|
|
73
73
|
function str(val) {
|
|
74
74
|
return typeof val === "string" ? val : undefined;
|
|
75
75
|
}
|
|
76
|
-
/** Extract a
|
|
77
|
-
function
|
|
78
|
-
return typeof val === "number" && val >=
|
|
76
|
+
/** Extract a non-negative integer or undefined. 0 means unlimited for max_turns. */
|
|
77
|
+
function nonNegativeInt(val) {
|
|
78
|
+
return typeof val === "number" && val >= 0 ? val : undefined;
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* Parse a raw CSV field value into items, or undefined if absent/empty/"none".
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,12 @@ import { join } from "node:path";
|
|
|
15
15
|
import { Text } from "@mariozechner/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { AgentManager } from "./agent-manager.js";
|
|
18
|
-
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
18
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
19
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
20
20
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
21
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
22
|
import { GroupJoinManager } from "./group-join.js";
|
|
23
|
+
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
23
24
|
import { resolveModel } from "./model-resolver.js";
|
|
24
25
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
25
26
|
import { AgentWidget, describeActivity, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
@@ -254,7 +255,7 @@ export default function (pi) {
|
|
|
254
255
|
content: notification + footer,
|
|
255
256
|
display: true,
|
|
256
257
|
details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
|
|
257
|
-
}, { deliverAs: "followUp" });
|
|
258
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
258
259
|
}
|
|
259
260
|
function sendIndividualNudge(record) {
|
|
260
261
|
agentActivity.delete(record.id);
|
|
@@ -290,7 +291,7 @@ export default function (pi) {
|
|
|
290
291
|
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
291
292
|
display: true,
|
|
292
293
|
details,
|
|
293
|
-
}, { deliverAs: "followUp" });
|
|
294
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
294
295
|
});
|
|
295
296
|
widget.update();
|
|
296
297
|
}, 30_000);
|
|
@@ -383,7 +384,7 @@ export default function (pi) {
|
|
|
383
384
|
manager.clearCompleted(); // preserve existing behavior
|
|
384
385
|
});
|
|
385
386
|
pi.on("session_switch", () => { manager.clearCompleted(); });
|
|
386
|
-
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
|
|
387
|
+
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
387
388
|
events: pi.events,
|
|
388
389
|
pi,
|
|
389
390
|
getCtx: () => currentCtx,
|
|
@@ -395,6 +396,7 @@ export default function (pi) {
|
|
|
395
396
|
// If the session is going down, there's nothing left to consume agent results.
|
|
396
397
|
pi.on("session_shutdown", async () => {
|
|
397
398
|
unsubSpawnRpc();
|
|
399
|
+
unsubStopRpc();
|
|
398
400
|
unsubPingRpc();
|
|
399
401
|
currentCtx = undefined;
|
|
400
402
|
delete globalThis[MANAGER_KEY];
|
|
@@ -510,8 +512,7 @@ Guidelines:
|
|
|
510
512
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
511
513
|
- Use thinking to control extended thinking level.
|
|
512
514
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
513
|
-
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications)
|
|
514
|
-
- Use join_mode to control how background completion notifications are delivered. By default (smart), 2+ background agents spawned in the same turn are grouped into a single notification. Use "async" for individual notifications or "group" to force grouping.`,
|
|
515
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
|
|
515
516
|
parameters: Type.Object({
|
|
516
517
|
prompt: Type.String({
|
|
517
518
|
description: "The task for the agent to perform.",
|
|
@@ -547,10 +548,6 @@ Guidelines:
|
|
|
547
548
|
isolation: Type.Optional(Type.Literal("worktree", {
|
|
548
549
|
description: 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
549
550
|
})),
|
|
550
|
-
join_mode: Type.Optional(Type.Union([
|
|
551
|
-
Type.Literal("async"),
|
|
552
|
-
Type.Literal("group"),
|
|
553
|
-
], { description: "Override join behavior for background agents. async: individual nudge on completion. group: hold and send one consolidated notification when all agents in the group complete. Default: smart (auto-groups 2+ background agents spawned in the same turn)." })),
|
|
554
551
|
}),
|
|
555
552
|
// ---- Custom rendering: Claude Code style ----
|
|
556
553
|
renderCall(args, theme) {
|
|
@@ -649,27 +646,25 @@ Guidelines:
|
|
|
649
646
|
const displayName = getDisplayName(subagentType);
|
|
650
647
|
// Get agent config (if any)
|
|
651
648
|
const customConfig = getAgentConfig(subagentType);
|
|
652
|
-
|
|
649
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
650
|
+
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
653
651
|
let model = ctx.model;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
const resolved = resolveModel(modelInput, ctx.modelRegistry);
|
|
652
|
+
if (resolvedConfig.modelInput) {
|
|
653
|
+
const resolved = resolveModel(resolvedConfig.modelInput, ctx.modelRegistry);
|
|
657
654
|
if (typeof resolved === "string") {
|
|
658
|
-
if (
|
|
659
|
-
return textResult(resolved);
|
|
655
|
+
if (resolvedConfig.modelFromParams)
|
|
656
|
+
return textResult(resolved);
|
|
660
657
|
// config-specified: silent fallback to parent
|
|
661
658
|
}
|
|
662
659
|
else {
|
|
663
660
|
model = resolved;
|
|
664
661
|
}
|
|
665
662
|
}
|
|
666
|
-
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
const
|
|
670
|
-
const
|
|
671
|
-
const isolated = params.isolated ?? customConfig?.isolated ?? false;
|
|
672
|
-
const isolation = params.isolation ?? customConfig?.isolation;
|
|
663
|
+
const thinking = resolvedConfig.thinking;
|
|
664
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
665
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
666
|
+
const isolated = resolvedConfig.isolated;
|
|
667
|
+
const isolation = resolvedConfig.isolation;
|
|
673
668
|
// Build display tags for non-default config
|
|
674
669
|
const parentModelId = ctx.model?.id;
|
|
675
670
|
const effectiveModelId = model?.id;
|
|
@@ -686,7 +681,7 @@ Guidelines:
|
|
|
686
681
|
agentTags.push("isolated");
|
|
687
682
|
if (isolation === "worktree")
|
|
688
683
|
agentTags.push("worktree");
|
|
689
|
-
const effectiveMaxTurns =
|
|
684
|
+
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
690
685
|
// Shared base fields for all AgentDetails in this call
|
|
691
686
|
const detailBase = {
|
|
692
687
|
displayName,
|
|
@@ -708,7 +703,7 @@ Guidelines:
|
|
|
708
703
|
if (!record) {
|
|
709
704
|
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
710
705
|
}
|
|
711
|
-
return textResult(record.result
|
|
706
|
+
return textResult(record.result?.trim() || record.error?.trim() || "No output.", buildDetails(detailBase, record));
|
|
712
707
|
}
|
|
713
708
|
// Background execution
|
|
714
709
|
if (runInBackground) {
|
|
@@ -728,7 +723,7 @@ Guidelines:
|
|
|
728
723
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
729
724
|
description: params.description,
|
|
730
725
|
model,
|
|
731
|
-
maxTurns:
|
|
726
|
+
maxTurns: effectiveMaxTurns,
|
|
732
727
|
isolated,
|
|
733
728
|
inheritContext,
|
|
734
729
|
thinkingLevel: thinking,
|
|
@@ -738,16 +733,16 @@ Guidelines:
|
|
|
738
733
|
});
|
|
739
734
|
// Set output file + join mode synchronously after spawn, before the
|
|
740
735
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
741
|
-
const joinMode =
|
|
736
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
742
737
|
const record = manager.getRecord(id);
|
|
743
|
-
if (record) {
|
|
738
|
+
if (record && joinMode) {
|
|
744
739
|
record.joinMode = joinMode;
|
|
745
740
|
record.toolCallId = toolCallId;
|
|
746
741
|
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
747
742
|
writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
|
|
748
743
|
}
|
|
749
|
-
if (joinMode === 'async') {
|
|
750
|
-
//
|
|
744
|
+
if (joinMode == null || joinMode === 'async') {
|
|
745
|
+
// Foreground/no join mode or explicit async — not part of any batch
|
|
751
746
|
}
|
|
752
747
|
else {
|
|
753
748
|
// smart or group — add to current batch
|
|
@@ -823,7 +818,7 @@ Guidelines:
|
|
|
823
818
|
const record = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
824
819
|
description: params.description,
|
|
825
820
|
model,
|
|
826
|
-
maxTurns:
|
|
821
|
+
maxTurns: effectiveMaxTurns,
|
|
827
822
|
isolated,
|
|
828
823
|
inheritContext,
|
|
829
824
|
thinkingLevel: thinking,
|
|
@@ -850,7 +845,7 @@ Guidelines:
|
|
|
850
845
|
if (tokenText)
|
|
851
846
|
statsParts.push(tokenText);
|
|
852
847
|
return textResult(`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
853
|
-
(record.result
|
|
848
|
+
(record.result?.trim() || "No output."), details);
|
|
854
849
|
},
|
|
855
850
|
});
|
|
856
851
|
// ---- get_subagent_result tool ----
|
|
@@ -897,7 +892,7 @@ Guidelines:
|
|
|
897
892
|
output += `Error: ${record.error}`;
|
|
898
893
|
}
|
|
899
894
|
else {
|
|
900
|
-
output += record.result
|
|
895
|
+
output += record.result?.trim() || "No output.";
|
|
901
896
|
}
|
|
902
897
|
// Mark result as consumed — suppresses the completion notification
|
|
903
898
|
if (record.status !== "running" && record.status !== "queued") {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentConfig, IsolationMode, JoinMode, ThinkingLevel } from "./types.js";
|
|
2
|
+
interface AgentInvocationParams {
|
|
3
|
+
model?: string;
|
|
4
|
+
thinking?: string;
|
|
5
|
+
max_turns?: number;
|
|
6
|
+
run_in_background?: boolean;
|
|
7
|
+
inherit_context?: boolean;
|
|
8
|
+
isolated?: boolean;
|
|
9
|
+
isolation?: IsolationMode;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveAgentInvocationConfig(agentConfig: AgentConfig | undefined, params: AgentInvocationParams): {
|
|
12
|
+
modelInput?: string;
|
|
13
|
+
modelFromParams: boolean;
|
|
14
|
+
thinking?: ThinkingLevel;
|
|
15
|
+
maxTurns?: number;
|
|
16
|
+
inheritContext: boolean;
|
|
17
|
+
runInBackground: boolean;
|
|
18
|
+
isolated: boolean;
|
|
19
|
+
isolation?: IsolationMode;
|
|
20
|
+
};
|
|
21
|
+
export declare function resolveJoinMode(defaultJoinMode: JoinMode, runInBackground: boolean): JoinMode | undefined;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function resolveAgentInvocationConfig(agentConfig, params) {
|
|
2
|
+
return {
|
|
3
|
+
modelInput: agentConfig?.model ?? params.model,
|
|
4
|
+
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
5
|
+
thinking: (agentConfig?.thinking ?? params.thinking),
|
|
6
|
+
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
7
|
+
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
8
|
+
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
9
|
+
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
10
|
+
isolation: agentConfig?.isolation ?? params.isolation,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function resolveJoinMode(defaultJoinMode, runInBackground) {
|
|
14
|
+
return runInBackground ? defaultJoinMode : undefined;
|
|
15
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -29,12 +29,12 @@ export interface AgentConfig {
|
|
|
29
29
|
maxTurns?: number;
|
|
30
30
|
systemPrompt: string;
|
|
31
31
|
promptMode: "replace" | "append";
|
|
32
|
-
/** Default for spawn: fork parent conversation */
|
|
33
|
-
inheritContext
|
|
34
|
-
/** Default for spawn: run in background */
|
|
35
|
-
runInBackground
|
|
36
|
-
/** Default for spawn: no extension tools */
|
|
37
|
-
isolated
|
|
32
|
+
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
33
|
+
inheritContext?: boolean;
|
|
34
|
+
/** Default for spawn: run in background. undefined = caller decides. */
|
|
35
|
+
runInBackground?: boolean;
|
|
36
|
+
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
37
|
+
isolated?: boolean;
|
|
38
38
|
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
39
39
|
memory?: MemoryScope;
|
|
40
40
|
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
@@ -179,7 +179,7 @@ export class ConversationViewer {
|
|
|
179
179
|
if (c.type === "text" && c.text)
|
|
180
180
|
textParts.push(c.text);
|
|
181
181
|
else if (c.type === "toolCall") {
|
|
182
|
-
toolCalls.push(c.toolName ?? "unknown");
|
|
182
|
+
toolCalls.push(c.name ?? c.toolName ?? "unknown");
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
if (needsSeparator)
|
|
@@ -159,7 +159,7 @@ describe("ConversationViewer", () => {
|
|
|
159
159
|
role: "assistant",
|
|
160
160
|
content: [
|
|
161
161
|
{ type: "text", text: "Let me check that." },
|
|
162
|
-
{ type: "toolCall", toolUseId: "t1",
|
|
162
|
+
{ type: "toolCall", toolUseId: "t1", name: "very_long_tool_name_" + "x".repeat(200), input: {} },
|
|
163
163
|
],
|
|
164
164
|
},
|
|
165
165
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tintinweb/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
|
|
5
5
|
"author": "tintinweb",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"autonomous"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@mariozechner/pi-ai": "^0.
|
|
25
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
26
|
-
"@mariozechner/pi-tui": "^0.
|
|
24
|
+
"@mariozechner/pi-ai": "^0.62.0",
|
|
25
|
+
"@mariozechner/pi-coding-agent": "^0.62.0",
|
|
26
|
+
"@mariozechner/pi-tui": "^0.62.0",
|
|
27
27
|
"@sinclair/typebox": "latest"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"lint:fix": "biome check --fix src/ test/"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^20.0.0",
|
|
40
|
-
"typescript": "^5.0.0",
|
|
41
39
|
"@biomejs/biome": "^2.3.5",
|
|
40
|
+
"@types/node": "^25.5.0",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
42
|
"vitest": "^4.0.18"
|
|
43
43
|
},
|
|
44
44
|
"pi": {
|
package/src/agent-runner.ts
CHANGED
|
@@ -27,10 +27,16 @@ const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
|
|
27
27
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
28
28
|
let defaultMaxTurns: number | undefined;
|
|
29
29
|
|
|
30
|
+
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
31
|
+
export function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
32
|
+
if (n == null || n === 0) return undefined;
|
|
33
|
+
return Math.max(1, n);
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
/** Get the default max turns value. undefined = unlimited. */
|
|
31
37
|
export function getDefaultMaxTurns(): number | undefined { return defaultMaxTurns; }
|
|
32
|
-
/** Set the default max turns value. undefined = unlimited, otherwise minimum 1. */
|
|
33
|
-
export function setDefaultMaxTurns(n: number | undefined): void { defaultMaxTurns =
|
|
38
|
+
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
39
|
+
export function setDefaultMaxTurns(n: number | undefined): void { defaultMaxTurns = normalizeMaxTurns(n); }
|
|
34
40
|
|
|
35
41
|
/** Additional turns allowed after the soft limit steer message. */
|
|
36
42
|
let graceTurns = 5;
|
|
@@ -123,6 +129,17 @@ function collectResponseText(session: AgentSession) {
|
|
|
123
129
|
return { getText: () => text, unsubscribe };
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
/** Get the last assistant text from the completed session history. */
|
|
133
|
+
function getLastAssistantText(session: AgentSession): string {
|
|
134
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
135
|
+
const msg = session.messages[i];
|
|
136
|
+
if (msg.role !== "assistant") continue;
|
|
137
|
+
const text = extractText(msg.content).trim();
|
|
138
|
+
if (text) return text;
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
|
|
126
143
|
/**
|
|
127
144
|
* Wire an AbortSignal to abort a session.
|
|
128
145
|
* Returns a cleanup function to remove the listener.
|
|
@@ -279,7 +296,7 @@ export async function runAgent(
|
|
|
279
296
|
|
|
280
297
|
// Track turns for graceful max_turns enforcement
|
|
281
298
|
let turnCount = 0;
|
|
282
|
-
const maxTurns = options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns;
|
|
299
|
+
const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns);
|
|
283
300
|
let softLimitReached = false;
|
|
284
301
|
let aborted = false;
|
|
285
302
|
|
|
@@ -333,7 +350,8 @@ export async function runAgent(
|
|
|
333
350
|
cleanupAbort();
|
|
334
351
|
}
|
|
335
352
|
|
|
336
|
-
|
|
353
|
+
const responseText = collector.getText().trim() || getLastAssistantText(session);
|
|
354
|
+
return { responseText, session, aborted, steered: softLimitReached };
|
|
337
355
|
}
|
|
338
356
|
|
|
339
357
|
/**
|
|
@@ -362,7 +380,7 @@ export async function resumeAgent(
|
|
|
362
380
|
cleanupAbort();
|
|
363
381
|
}
|
|
364
382
|
|
|
365
|
-
return collector.getText();
|
|
383
|
+
return collector.getText().trim() || getLastAssistantText(session);
|
|
366
384
|
}
|
|
367
385
|
|
|
368
386
|
/**
|
|
@@ -393,7 +411,7 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
393
411
|
const toolCalls: string[] = [];
|
|
394
412
|
for (const c of msg.content) {
|
|
395
413
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
396
|
-
else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).toolName ?? "unknown"}`);
|
|
414
|
+
else if (c.type === "toolCall") toolCalls.push(` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`);
|
|
397
415
|
}
|
|
398
416
|
if (textParts.length > 0) parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
399
417
|
if (toolCalls.length > 0) parts.push(`[Tool Calls]:\n${toolCalls.join("\n")}`);
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension RPC handlers for the subagents extension.
|
|
3
3
|
*
|
|
4
|
-
* Exposes ping and
|
|
4
|
+
* Exposes ping, spawn, and stop RPCs over the pi.events event bus,
|
|
5
5
|
* using per-request scoped reply channels.
|
|
6
|
+
*
|
|
7
|
+
* Reply envelope follows pi-mono convention:
|
|
8
|
+
* success → { success: true, data?: T }
|
|
9
|
+
* error → { success: false, error: string }
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
/** Minimal event bus interface needed by the RPC handlers. */
|
|
@@ -11,9 +15,18 @@ export interface EventBus {
|
|
|
11
15
|
emit(event: string, data: unknown): void;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
/**
|
|
18
|
+
/** RPC reply envelope — matches pi-mono's RpcResponse shape. */
|
|
19
|
+
export type RpcReply<T = void> =
|
|
20
|
+
| { success: true; data?: T }
|
|
21
|
+
| { success: false; error: string };
|
|
22
|
+
|
|
23
|
+
/** RPC protocol version — bumped when the envelope or method contracts change. */
|
|
24
|
+
export const PROTOCOL_VERSION = 2;
|
|
25
|
+
|
|
26
|
+
/** Minimal AgentManager interface needed by the spawn/stop RPCs. */
|
|
15
27
|
export interface SpawnCapable {
|
|
16
28
|
spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string;
|
|
29
|
+
abort(id: string): boolean;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export interface RpcDeps {
|
|
@@ -26,36 +39,57 @@ export interface RpcDeps {
|
|
|
26
39
|
export interface RpcHandle {
|
|
27
40
|
unsubPing: () => void;
|
|
28
41
|
unsubSpawn: () => void;
|
|
42
|
+
unsubStop: () => void;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
/**
|
|
32
|
-
*
|
|
46
|
+
* Wire a single RPC handler: listen on `channel`, run `fn(params)`,
|
|
47
|
+
* emit the reply envelope on `channel:reply:${requestId}`.
|
|
48
|
+
*/
|
|
49
|
+
function handleRpc<P extends { requestId: string }>(
|
|
50
|
+
events: EventBus,
|
|
51
|
+
channel: string,
|
|
52
|
+
fn: (params: P) => unknown | Promise<unknown>,
|
|
53
|
+
): () => void {
|
|
54
|
+
return events.on(channel, async (raw: unknown) => {
|
|
55
|
+
const params = raw as P;
|
|
56
|
+
try {
|
|
57
|
+
const data = await fn(params);
|
|
58
|
+
const reply: { success: true; data?: unknown } = { success: true };
|
|
59
|
+
if (data !== undefined) reply.data = data;
|
|
60
|
+
events.emit(`${channel}:reply:${params.requestId}`, reply);
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
events.emit(`${channel}:reply:${params.requestId}`, {
|
|
63
|
+
success: false, error: err?.message ?? String(err),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register ping, spawn, and stop RPC handlers on the event bus.
|
|
33
71
|
* Returns unsub functions for cleanup.
|
|
34
72
|
*/
|
|
35
73
|
export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
|
|
36
74
|
const { events, pi, getCtx, manager } = deps;
|
|
37
75
|
|
|
38
|
-
const unsubPing = events
|
|
39
|
-
|
|
40
|
-
events.emit(`subagents:rpc:ping:reply:${requestId}`, {});
|
|
76
|
+
const unsubPing = handleRpc(events, "subagents:rpc:ping", () => {
|
|
77
|
+
return { version: PROTOCOL_VERSION };
|
|
41
78
|
});
|
|
42
79
|
|
|
43
|
-
const unsubSpawn =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
events.emit(`subagents:rpc:spawn:reply:${requestId}`, { error: err.message });
|
|
57
|
-
}
|
|
58
|
-
});
|
|
80
|
+
const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>(
|
|
81
|
+
events, "subagents:rpc:spawn", ({ type, prompt, options }) => {
|
|
82
|
+
const ctx = getCtx();
|
|
83
|
+
if (!ctx) throw new Error("No active session");
|
|
84
|
+
return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) };
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const unsubStop = handleRpc<{ requestId: string; agentId: string }>(
|
|
89
|
+
events, "subagents:rpc:stop", ({ agentId }) => {
|
|
90
|
+
if (!manager.abort(agentId)) throw new Error("Agent not found");
|
|
91
|
+
},
|
|
92
|
+
);
|
|
59
93
|
|
|
60
|
-
return { unsubPing, unsubSpawn };
|
|
94
|
+
return { unsubPing, unsubSpawn, unsubStop };
|
|
61
95
|
}
|
package/src/custom-agents.ts
CHANGED
|
@@ -61,12 +61,12 @@ function loadFromDir(dir: string, agents: Map<string, AgentConfig>, source: "pro
|
|
|
61
61
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
62
62
|
model: str(fm.model),
|
|
63
63
|
thinking: str(fm.thinking) as ThinkingLevel | undefined,
|
|
64
|
-
maxTurns:
|
|
64
|
+
maxTurns: nonNegativeInt(fm.max_turns),
|
|
65
65
|
systemPrompt: body.trim(),
|
|
66
66
|
promptMode: fm.prompt_mode === "append" ? "append" : "replace",
|
|
67
|
-
inheritContext: fm.inherit_context === true,
|
|
68
|
-
runInBackground: fm.run_in_background === true,
|
|
69
|
-
isolated: fm.isolated === true,
|
|
67
|
+
inheritContext: fm.inherit_context != null ? fm.inherit_context === true : undefined,
|
|
68
|
+
runInBackground: fm.run_in_background != null ? fm.run_in_background === true : undefined,
|
|
69
|
+
isolated: fm.isolated != null ? fm.isolated === true : undefined,
|
|
70
70
|
memory: parseMemory(fm.memory),
|
|
71
71
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
72
72
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
@@ -83,9 +83,9 @@ function str(val: unknown): string | undefined {
|
|
|
83
83
|
return typeof val === "string" ? val : undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** Extract a
|
|
87
|
-
function
|
|
88
|
-
return typeof val === "number" && val >=
|
|
86
|
+
/** Extract a non-negative integer or undefined. 0 means unlimited for max_turns. */
|
|
87
|
+
function nonNegativeInt(val: unknown): number | undefined {
|
|
88
|
+
return typeof val === "number" && val >= 0 ? val : undefined;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
package/src/index.ts
CHANGED
|
@@ -17,14 +17,15 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@m
|
|
|
17
17
|
import { Text } from "@mariozechner/pi-tui";
|
|
18
18
|
import { Type } from "@sinclair/typebox";
|
|
19
19
|
import { AgentManager } from "./agent-manager.js";
|
|
20
|
-
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
20
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
21
21
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
22
22
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
23
23
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
24
24
|
import { GroupJoinManager } from "./group-join.js";
|
|
25
|
+
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
25
26
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
26
27
|
import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
|
|
27
|
-
import { type AgentConfig, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType
|
|
28
|
+
import { type AgentConfig, type AgentRecord, type JoinMode, type NotificationDetails, type SubagentType } from "./types.js";
|
|
28
29
|
import {
|
|
29
30
|
type AgentActivity,
|
|
30
31
|
type AgentDetails,
|
|
@@ -289,7 +290,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
289
290
|
content: notification + footer,
|
|
290
291
|
display: true,
|
|
291
292
|
details: buildNotificationDetails(record, 500, agentActivity.get(record.id)),
|
|
292
|
-
}, { deliverAs: "followUp" });
|
|
293
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
function sendIndividualNudge(record: AgentRecord) {
|
|
@@ -326,7 +327,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
327
|
content: `Background agent group completed: ${label}\n\n${notifications}\n\nUse get_subagent_result for full output.`,
|
|
327
328
|
display: true,
|
|
328
329
|
details,
|
|
329
|
-
}, { deliverAs: "followUp" });
|
|
330
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
330
331
|
});
|
|
331
332
|
widget.update();
|
|
332
333
|
},
|
|
@@ -431,7 +432,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
431
432
|
|
|
432
433
|
pi.on("session_switch", () => { manager.clearCompleted(); });
|
|
433
434
|
|
|
434
|
-
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc } = registerRpcHandlers({
|
|
435
|
+
const { unsubPing: unsubPingRpc, unsubSpawn: unsubSpawnRpc, unsubStop: unsubStopRpc } = registerRpcHandlers({
|
|
435
436
|
events: pi.events,
|
|
436
437
|
pi,
|
|
437
438
|
getCtx: () => currentCtx,
|
|
@@ -445,6 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
445
446
|
// If the session is going down, there's nothing left to consume agent results.
|
|
446
447
|
pi.on("session_shutdown", async () => {
|
|
447
448
|
unsubSpawnRpc();
|
|
449
|
+
unsubStopRpc();
|
|
448
450
|
unsubPingRpc();
|
|
449
451
|
currentCtx = undefined;
|
|
450
452
|
delete (globalThis as any)[MANAGER_KEY];
|
|
@@ -571,8 +573,7 @@ Guidelines:
|
|
|
571
573
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
572
574
|
- Use thinking to control extended thinking level.
|
|
573
575
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
574
|
-
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications)
|
|
575
|
-
- Use join_mode to control how background completion notifications are delivered. By default (smart), 2+ background agents spawned in the same turn are grouped into a single notification. Use "async" for individual notifications or "group" to force grouping.`,
|
|
576
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).`,
|
|
576
577
|
parameters: Type.Object({
|
|
577
578
|
prompt: Type.String({
|
|
578
579
|
description: "The task for the agent to perform.",
|
|
@@ -625,12 +626,6 @@ Guidelines:
|
|
|
625
626
|
description: 'Set to "worktree" to run the agent in a temporary git worktree (isolated copy of the repo). Changes are saved to a branch on completion.',
|
|
626
627
|
}),
|
|
627
628
|
),
|
|
628
|
-
join_mode: Type.Optional(
|
|
629
|
-
Type.Union([
|
|
630
|
-
Type.Literal("async"),
|
|
631
|
-
Type.Literal("group"),
|
|
632
|
-
], { description: "Override join behavior for background agents. async: individual nudge on completion. group: hold and send one consolidated notification when all agents in the group complete. Default: smart (auto-groups 2+ background agents spawned in the same turn)." }),
|
|
633
|
-
),
|
|
634
629
|
}),
|
|
635
630
|
|
|
636
631
|
// ---- Custom rendering: Claude Code style ----
|
|
@@ -742,27 +737,25 @@ Guidelines:
|
|
|
742
737
|
// Get agent config (if any)
|
|
743
738
|
const customConfig = getAgentConfig(subagentType);
|
|
744
739
|
|
|
745
|
-
|
|
740
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, params);
|
|
741
|
+
|
|
742
|
+
// Resolve model from agent config first; tool-call params only fill gaps.
|
|
746
743
|
let model = ctx.model;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const resolved = resolveModel(modelInput, ctx.modelRegistry);
|
|
744
|
+
if (resolvedConfig.modelInput) {
|
|
745
|
+
const resolved = resolveModel(resolvedConfig.modelInput, ctx.modelRegistry);
|
|
750
746
|
if (typeof resolved === "string") {
|
|
751
|
-
if (
|
|
747
|
+
if (resolvedConfig.modelFromParams) return textResult(resolved);
|
|
752
748
|
// config-specified: silent fallback to parent
|
|
753
749
|
} else {
|
|
754
750
|
model = resolved;
|
|
755
751
|
}
|
|
756
752
|
}
|
|
757
753
|
|
|
758
|
-
|
|
759
|
-
const
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
const runInBackground = params.run_in_background ?? customConfig?.runInBackground ?? false;
|
|
764
|
-
const isolated = params.isolated ?? customConfig?.isolated ?? false;
|
|
765
|
-
const isolation = params.isolation ?? customConfig?.isolation;
|
|
754
|
+
const thinking = resolvedConfig.thinking;
|
|
755
|
+
const inheritContext = resolvedConfig.inheritContext;
|
|
756
|
+
const runInBackground = resolvedConfig.runInBackground;
|
|
757
|
+
const isolated = resolvedConfig.isolated;
|
|
758
|
+
const isolation = resolvedConfig.isolation;
|
|
766
759
|
|
|
767
760
|
// Build display tags for non-default config
|
|
768
761
|
const parentModelId = ctx.model?.id;
|
|
@@ -776,7 +769,7 @@ Guidelines:
|
|
|
776
769
|
if (thinking) agentTags.push(`thinking: ${thinking}`);
|
|
777
770
|
if (isolated) agentTags.push("isolated");
|
|
778
771
|
if (isolation === "worktree") agentTags.push("worktree");
|
|
779
|
-
const effectiveMaxTurns =
|
|
772
|
+
const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
|
|
780
773
|
// Shared base fields for all AgentDetails in this call
|
|
781
774
|
const detailBase = {
|
|
782
775
|
displayName,
|
|
@@ -800,7 +793,7 @@ Guidelines:
|
|
|
800
793
|
return textResult(`Failed to resume agent "${params.resume}".`);
|
|
801
794
|
}
|
|
802
795
|
return textResult(
|
|
803
|
-
record.result
|
|
796
|
+
record.result?.trim() || record.error?.trim() || "No output.",
|
|
804
797
|
buildDetails(detailBase, record),
|
|
805
798
|
);
|
|
806
799
|
}
|
|
@@ -825,7 +818,7 @@ Guidelines:
|
|
|
825
818
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
826
819
|
description: params.description,
|
|
827
820
|
model,
|
|
828
|
-
maxTurns:
|
|
821
|
+
maxTurns: effectiveMaxTurns,
|
|
829
822
|
isolated,
|
|
830
823
|
inheritContext,
|
|
831
824
|
thinkingLevel: thinking,
|
|
@@ -836,17 +829,17 @@ Guidelines:
|
|
|
836
829
|
|
|
837
830
|
// Set output file + join mode synchronously after spawn, before the
|
|
838
831
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
839
|
-
const joinMode
|
|
832
|
+
const joinMode = resolveJoinMode(defaultJoinMode, true);
|
|
840
833
|
const record = manager.getRecord(id);
|
|
841
|
-
if (record) {
|
|
834
|
+
if (record && joinMode) {
|
|
842
835
|
record.joinMode = joinMode;
|
|
843
836
|
record.toolCallId = toolCallId;
|
|
844
837
|
record.outputFile = createOutputFilePath(ctx.cwd, id, ctx.sessionManager.getSessionId());
|
|
845
838
|
writeInitialEntry(record.outputFile, id, params.prompt, ctx.cwd);
|
|
846
839
|
}
|
|
847
840
|
|
|
848
|
-
if (joinMode === 'async') {
|
|
849
|
-
//
|
|
841
|
+
if (joinMode == null || joinMode === 'async') {
|
|
842
|
+
// Foreground/no join mode or explicit async — not part of any batch
|
|
850
843
|
} else {
|
|
851
844
|
// smart or group — add to current batch
|
|
852
845
|
currentBatchAgents.push({ id, joinMode });
|
|
@@ -933,7 +926,7 @@ Guidelines:
|
|
|
933
926
|
const record = await manager.spawnAndWait(pi, ctx, subagentType, params.prompt, {
|
|
934
927
|
description: params.description,
|
|
935
928
|
model,
|
|
936
|
-
maxTurns:
|
|
929
|
+
maxTurns: effectiveMaxTurns,
|
|
937
930
|
isolated,
|
|
938
931
|
inheritContext,
|
|
939
932
|
thinkingLevel: thinking,
|
|
@@ -967,7 +960,7 @@ Guidelines:
|
|
|
967
960
|
if (tokenText) statsParts.push(tokenText);
|
|
968
961
|
return textResult(
|
|
969
962
|
`${fallbackNote}Agent completed in ${formatMs(durationMs)} (${statsParts.join(", ")})${getStatusNote(record.status)}.\n\n` +
|
|
970
|
-
(record.result
|
|
963
|
+
(record.result?.trim() || "No output."),
|
|
971
964
|
details,
|
|
972
965
|
);
|
|
973
966
|
},
|
|
@@ -1026,7 +1019,7 @@ Guidelines:
|
|
|
1026
1019
|
} else if (record.status === "error") {
|
|
1027
1020
|
output += `Error: ${record.error}`;
|
|
1028
1021
|
} else {
|
|
1029
|
-
output += record.result
|
|
1022
|
+
output += record.result?.trim() || "No output.";
|
|
1030
1023
|
}
|
|
1031
1024
|
|
|
1032
1025
|
// Mark result as consumed — suppresses the completion notification
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AgentConfig, IsolationMode, JoinMode, ThinkingLevel } from "./types.js";
|
|
2
|
+
|
|
3
|
+
interface AgentInvocationParams {
|
|
4
|
+
model?: string;
|
|
5
|
+
thinking?: string;
|
|
6
|
+
max_turns?: number;
|
|
7
|
+
run_in_background?: boolean;
|
|
8
|
+
inherit_context?: boolean;
|
|
9
|
+
isolated?: boolean;
|
|
10
|
+
isolation?: IsolationMode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveAgentInvocationConfig(
|
|
14
|
+
agentConfig: AgentConfig | undefined,
|
|
15
|
+
params: AgentInvocationParams,
|
|
16
|
+
): {
|
|
17
|
+
modelInput?: string;
|
|
18
|
+
modelFromParams: boolean;
|
|
19
|
+
thinking?: ThinkingLevel;
|
|
20
|
+
maxTurns?: number;
|
|
21
|
+
inheritContext: boolean;
|
|
22
|
+
runInBackground: boolean;
|
|
23
|
+
isolated: boolean;
|
|
24
|
+
isolation?: IsolationMode;
|
|
25
|
+
} {
|
|
26
|
+
return {
|
|
27
|
+
modelInput: agentConfig?.model ?? params.model,
|
|
28
|
+
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
29
|
+
thinking: (agentConfig?.thinking ?? params.thinking) as ThinkingLevel | undefined,
|
|
30
|
+
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
31
|
+
inheritContext: agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
|
32
|
+
runInBackground: agentConfig?.runInBackground ?? params.run_in_background ?? false,
|
|
33
|
+
isolated: agentConfig?.isolated ?? params.isolated ?? false,
|
|
34
|
+
isolation: agentConfig?.isolation ?? params.isolation,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveJoinMode(defaultJoinMode: JoinMode, runInBackground: boolean): JoinMode | undefined {
|
|
39
|
+
return runInBackground ? defaultJoinMode : undefined;
|
|
40
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -36,12 +36,12 @@ export interface AgentConfig {
|
|
|
36
36
|
maxTurns?: number;
|
|
37
37
|
systemPrompt: string;
|
|
38
38
|
promptMode: "replace" | "append";
|
|
39
|
-
/** Default for spawn: fork parent conversation */
|
|
40
|
-
inheritContext
|
|
41
|
-
/** Default for spawn: run in background */
|
|
42
|
-
runInBackground
|
|
43
|
-
/** Default for spawn: no extension tools */
|
|
44
|
-
isolated
|
|
39
|
+
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
40
|
+
inheritContext?: boolean;
|
|
41
|
+
/** Default for spawn: run in background. undefined = caller decides. */
|
|
42
|
+
runInBackground?: boolean;
|
|
43
|
+
/** Default for spawn: no extension tools. undefined = caller decides. */
|
|
44
|
+
isolated?: boolean;
|
|
45
45
|
/** Persistent memory scope — agents with memory get a persistent directory and MEMORY.md */
|
|
46
46
|
memory?: MemoryScope;
|
|
47
47
|
/** Isolation mode — "worktree" runs the agent in a temporary git worktree */
|
|
@@ -191,7 +191,7 @@ export class ConversationViewer implements Component {
|
|
|
191
191
|
for (const c of msg.content) {
|
|
192
192
|
if (c.type === "text" && c.text) textParts.push(c.text);
|
|
193
193
|
else if (c.type === "toolCall") {
|
|
194
|
-
toolCalls.push((c as any).toolName ?? "unknown");
|
|
194
|
+
toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
if (needsSeparator) lines.push(th.fg("dim", "───"));
|