@esso0428/pi-subagents 0.15.0 → 0.15.2
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 +10 -0
- package/README.md +2 -2
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +87 -19
- package/dist/agent-runner.js +85 -66
- package/dist/agent-types.js +45 -27
- package/dist/context.js +6 -2
- package/dist/cross-extension-rpc.js +9 -5
- package/dist/custom-agents.js +18 -15
- package/dist/default-agents.js +4 -1
- package/dist/enabled-models.js +16 -11
- package/dist/env.js +4 -1
- package/dist/group-join.js +5 -1
- package/dist/index.js +280 -221
- package/dist/invocation-config.js +6 -2
- package/dist/memory.js +34 -24
- package/dist/model-resolver.js +4 -1
- package/dist/nico-overrides.js +20 -14
- package/dist/output-file.js +21 -15
- package/dist/prompts.js +4 -1
- package/dist/schedule-store.js +21 -16
- package/dist/schedule.js +12 -8
- package/dist/settings.js +23 -15
- package/dist/skill-loader.js +23 -20
- package/dist/status-note.js +4 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js +4 -1
- package/dist/ui/agent-widget.js +37 -23
- package/dist/ui/conversation-viewer.js +43 -39
- package/dist/ui/fleet-list.js +30 -24
- package/dist/ui/markdown-result.d.ts +3 -0
- package/dist/ui/markdown-result.js +53 -0
- package/dist/ui/schedule-menu.js +4 -1
- package/dist/ui/viewer-keys.js +10 -7
- package/dist/usage.js +10 -4
- package/dist/worktree.js +31 -26
- package/package.json +1 -1
- package/src/agent-manager.ts +72 -0
- package/src/agent-runner.ts +11 -3
- package/src/index.ts +39 -16
- package/src/types.ts +1 -0
- package/src/ui/markdown-result.ts +56 -0
- package/test/agent-manager-history.test.ts +84 -0
- package/test/ui/markdown-result.test.ts +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.2] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Completed subagent records survive session reloads.** On `session_start`, the extension now reconstructs validated terminal records persisted as `subagents:record` entries. Restored records retain their result and status for `get_subagent_result`, but never pretend to have a live child session; running and queued records are ignored, and duplicate IDs use the newest branch entry.
|
|
14
|
+
|
|
15
|
+
## [0.15.1] - 2026-09-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Markdown-rendered subagent results.** Expanded foreground results and background completion notifications now render headings, lists, inline code, and fenced code through pi's Markdown renderer. Display output is capped to keep terminal notifications bounded; use `get_subagent_result` for the complete result.
|
|
19
|
+
|
|
10
20
|
## [0.14.3] - 2026-07-23
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ https://github.com/user-attachments/assets/8685261b-9338-4fea-8dfe-1c590d5df543
|
|
|
28
28
|
- **Git worktree isolation** — run agents in isolated repo copies; changes auto-committed to branches on completion
|
|
29
29
|
- **Skill preloading** — inject named skills into agent system prompts, discovered from `.pi/skills/`, `.agents/skills/`, and global locations (Pi-standard `<name>/SKILL.md` directory layout supported)
|
|
30
30
|
- **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
|
|
31
|
-
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML.
|
|
31
|
+
- **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expanded results render headings, lists, inline code, and fenced code as Markdown, with a bounded display to keep the TUI responsive. Group completions render each agent individually
|
|
32
32
|
- **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
|
|
33
33
|
- **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 session start
|
|
34
34
|
- **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
|
|
@@ -138,7 +138,7 @@ Individual agent results render Claude Code-style in the conversation:
|
|
|
138
138
|
| **Error** | `✗ ↻3 · 3 tool uses · 12.4k token (8%)` / `⎿ Error: timeout` |
|
|
139
139
|
| **Aborted** | `✗ ↻55≤50 · 55 tool uses · 102.3k token (95% · ⇊3)` / `⎿ Aborted (max turns exceeded)` |
|
|
140
140
|
|
|
141
|
-
Completed results can be expanded (ctrl+o in pi) to show the
|
|
141
|
+
Completed results can be expanded (ctrl+o in pi) to show the agent output inline with Markdown formatting. Expanded output is capped at 50 rendered lines (background notification results at 30 lines) and ends with a marker when more output is available; use `get_subagent_result` for the complete result.
|
|
142
142
|
|
|
143
143
|
By default, foreground and background agents each stream their full conversation to a per-subagent transcript — a JSON-lines file at `<os-tmpdir>/pi-subagents-<uid>/<cwd>/<session>/tasks/<agent-id>.output` (owner-only `0700`, cleared on reboot). Set `output_transcript: false` on a custom agent to write no transcript path or file for it, or set `outputTranscript: false` in `subagents.json` to make transcripts opt-in for the whole project (frontmatter overrides the project default). This governs **only** the transcript: it is independent of `persist_session` (the pi session on disk), and it does not affect `isolation: worktree` (which commits the agent's work to a git branch) or `memory:` (durable files) — set those accordingly if the goal is to keep a run off disk entirely. Background agent completion notifications render as styled boxes:
|
|
144
144
|
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -121,6 +121,14 @@ export declare class AgentManager {
|
|
|
121
121
|
*/
|
|
122
122
|
steer(id: string, message: string): boolean;
|
|
123
123
|
getRecord(id: string): AgentRecord | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Restore terminal records persisted in a parent session.
|
|
126
|
+
*
|
|
127
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
128
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
129
|
+
* extension data and may have been written by an older version.
|
|
130
|
+
*/
|
|
131
|
+
restoreCompleted(records: readonly unknown[]): void;
|
|
124
132
|
listAgents(): AgentRecord[];
|
|
125
133
|
abort(id: string): boolean;
|
|
126
134
|
/** Dispose a record's session and remove it from the map. */
|
package/dist/agent-manager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
3
4
|
*
|
|
@@ -5,14 +6,23 @@
|
|
|
5
6
|
* Excess agents are queued and auto-started as running agents complete.
|
|
6
7
|
* Foreground agents bypass the queue (they block the parent anyway).
|
|
7
8
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AgentManager = void 0;
|
|
11
|
+
const node_crypto_1 = require("node:crypto");
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
const agent_runner_js_1 = require("./agent-runner.js");
|
|
15
|
+
const usage_js_1 = require("./usage.js");
|
|
16
|
+
const worktree_js_1 = require("./worktree.js");
|
|
14
17
|
/** Default max concurrent background agents. */
|
|
15
18
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
19
|
+
const TERMINAL_STATUSES = new Set([
|
|
20
|
+
"completed",
|
|
21
|
+
"steered",
|
|
22
|
+
"aborted",
|
|
23
|
+
"stopped",
|
|
24
|
+
"error",
|
|
25
|
+
]);
|
|
16
26
|
/**
|
|
17
27
|
* Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
|
|
18
28
|
* (parent cwd). Anything else must be an absolute path to an existing
|
|
@@ -22,12 +32,12 @@ const DEFAULT_MAX_CONCURRENT = 4;
|
|
|
22
32
|
function assertValidSpawnCwd(cwd) {
|
|
23
33
|
if (cwd == null)
|
|
24
34
|
return;
|
|
25
|
-
if (typeof cwd !== "string" || !isAbsolute(cwd)) {
|
|
35
|
+
if (typeof cwd !== "string" || !(0, node_path_1.isAbsolute)(cwd)) {
|
|
26
36
|
throw new Error(`SpawnOptions.cwd must be an absolute path: "${String(cwd)}"`);
|
|
27
37
|
}
|
|
28
38
|
let isDirectory = false;
|
|
29
39
|
try {
|
|
30
|
-
isDirectory = statSync(cwd).isDirectory();
|
|
40
|
+
isDirectory = (0, node_fs_1.statSync)(cwd).isDirectory();
|
|
31
41
|
}
|
|
32
42
|
catch {
|
|
33
43
|
throw new Error(`SpawnOptions.cwd does not exist: "${cwd}"`);
|
|
@@ -36,7 +46,30 @@ function assertValidSpawnCwd(cwd) {
|
|
|
36
46
|
throw new Error(`SpawnOptions.cwd is not a directory: "${cwd}"`);
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
|
-
|
|
49
|
+
const RESTORABLE_STATUSES = new Set([
|
|
50
|
+
"completed",
|
|
51
|
+
"steered",
|
|
52
|
+
"aborted",
|
|
53
|
+
"stopped",
|
|
54
|
+
"error",
|
|
55
|
+
]);
|
|
56
|
+
function isRestorableRecord(value) {
|
|
57
|
+
if (!value || typeof value !== "object")
|
|
58
|
+
return false;
|
|
59
|
+
const record = value;
|
|
60
|
+
return (typeof record.id === "string" &&
|
|
61
|
+
record.id.length > 0 &&
|
|
62
|
+
typeof record.type === "string" &&
|
|
63
|
+
typeof record.description === "string" &&
|
|
64
|
+
RESTORABLE_STATUSES.has(record.status) &&
|
|
65
|
+
typeof record.startedAt === "number" &&
|
|
66
|
+
Number.isFinite(record.startedAt) &&
|
|
67
|
+
typeof record.completedAt === "number" &&
|
|
68
|
+
Number.isFinite(record.completedAt) &&
|
|
69
|
+
(record.result === undefined || typeof record.result === "string") &&
|
|
70
|
+
(record.error === undefined || typeof record.error === "string"));
|
|
71
|
+
}
|
|
72
|
+
class AgentManager {
|
|
40
73
|
agents = new Map();
|
|
41
74
|
cleanupInterval;
|
|
42
75
|
onComplete;
|
|
@@ -77,7 +110,7 @@ export class AgentManager {
|
|
|
77
110
|
// call, not minutes later at drain. Throw (not warn): programmatic callers
|
|
78
111
|
// can fix and retry; the RPC layer converts throws into error envelopes.
|
|
79
112
|
assertValidSpawnCwd(options.cwd);
|
|
80
|
-
const id = randomUUID().slice(0, 17);
|
|
113
|
+
const id = (0, node_crypto_1.randomUUID)().slice(0, 17);
|
|
81
114
|
const abortController = new AbortController();
|
|
82
115
|
const record = {
|
|
83
116
|
id,
|
|
@@ -130,7 +163,7 @@ export class AgentManager {
|
|
|
130
163
|
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
131
164
|
let worktreeCwd;
|
|
132
165
|
if (options.isolation === "worktree") {
|
|
133
|
-
const wt = createWorktree(baseCwd, id);
|
|
166
|
+
const wt = (0, worktree_js_1.createWorktree)(baseCwd, id);
|
|
134
167
|
if (!wt) {
|
|
135
168
|
throw new Error('Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
136
169
|
'Initialize git and commit at least once, or omit `isolation`.');
|
|
@@ -158,7 +191,7 @@ export class AgentManager {
|
|
|
158
191
|
detachParentSignal = () => options.signal.removeEventListener("abort", onParentAbort);
|
|
159
192
|
}
|
|
160
193
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
161
|
-
const promise = runAgent(ctx, type, prompt, {
|
|
194
|
+
const promise = (0, agent_runner_js_1.runAgent)(ctx, type, prompt, {
|
|
162
195
|
pi,
|
|
163
196
|
agentId: id,
|
|
164
197
|
model: options.model,
|
|
@@ -182,7 +215,7 @@ export class AgentManager {
|
|
|
182
215
|
onTurnEnd: options.onTurnEnd,
|
|
183
216
|
onTextDelta: options.onTextDelta,
|
|
184
217
|
onAssistantUsage: (usage) => {
|
|
185
|
-
addUsage(record.lifetimeUsage, usage);
|
|
218
|
+
(0, usage_js_1.addUsage)(record.lifetimeUsage, usage);
|
|
186
219
|
options.onAssistantUsage?.(usage);
|
|
187
220
|
},
|
|
188
221
|
onCompaction: (info) => {
|
|
@@ -233,7 +266,7 @@ export class AgentManager {
|
|
|
233
266
|
}
|
|
234
267
|
// Clean up worktree if used
|
|
235
268
|
if (record.worktree) {
|
|
236
|
-
const wtResult = cleanupWorktree(baseCwd, record.worktree, options.description);
|
|
269
|
+
const wtResult = (0, worktree_js_1.cleanupWorktree)(baseCwd, record.worktree, options.description);
|
|
237
270
|
record.worktreeResult = wtResult;
|
|
238
271
|
if (wtResult.hasChanges && wtResult.branch) {
|
|
239
272
|
// With a caller-supplied cwd the branch lives in THAT repo, not the
|
|
@@ -281,7 +314,7 @@ export class AgentManager {
|
|
|
281
314
|
// Best-effort worktree cleanup on error
|
|
282
315
|
if (record.worktree) {
|
|
283
316
|
try {
|
|
284
|
-
const wtResult = cleanupWorktree(baseCwd, record.worktree, options.description);
|
|
317
|
+
const wtResult = (0, worktree_js_1.cleanupWorktree)(baseCwd, record.worktree, options.description);
|
|
285
318
|
record.worktreeResult = wtResult;
|
|
286
319
|
}
|
|
287
320
|
catch { /* ignore cleanup errors */ }
|
|
@@ -366,13 +399,13 @@ export class AgentManager {
|
|
|
366
399
|
record.result = undefined;
|
|
367
400
|
record.error = undefined;
|
|
368
401
|
try {
|
|
369
|
-
const { text, failure } = await resumeAgent(record.session, prompt, {
|
|
402
|
+
const { text, failure } = await (0, agent_runner_js_1.resumeAgent)(record.session, prompt, {
|
|
370
403
|
onToolActivity: (activity) => {
|
|
371
404
|
if (activity.type === "end")
|
|
372
405
|
record.toolUses++;
|
|
373
406
|
},
|
|
374
407
|
onAssistantUsage: (usage) => {
|
|
375
|
-
addUsage(record.lifetimeUsage, usage);
|
|
408
|
+
(0, usage_js_1.addUsage)(record.lifetimeUsage, usage);
|
|
376
409
|
},
|
|
377
410
|
onCompaction: (info) => {
|
|
378
411
|
record.compactionCount++;
|
|
@@ -422,6 +455,40 @@ export class AgentManager {
|
|
|
422
455
|
getRecord(id) {
|
|
423
456
|
return this.agents.get(id);
|
|
424
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Restore terminal records persisted in a parent session.
|
|
460
|
+
*
|
|
461
|
+
* Restored records deliberately have no live session, promise, or abort
|
|
462
|
+
* controller. Invalid data is ignored because session entries are persisted
|
|
463
|
+
* extension data and may have been written by an older version.
|
|
464
|
+
*/
|
|
465
|
+
restoreCompleted(records) {
|
|
466
|
+
const restoredIds = new Set();
|
|
467
|
+
// getBranch() is chronological; newest persisted state wins on duplicate IDs.
|
|
468
|
+
for (const value of [...records].reverse()) {
|
|
469
|
+
if (!isRestorableRecord(value))
|
|
470
|
+
continue;
|
|
471
|
+
if (restoredIds.has(value.id) || this.agents.has(value.id))
|
|
472
|
+
continue;
|
|
473
|
+
restoredIds.add(value.id);
|
|
474
|
+
this.agents.set(value.id, {
|
|
475
|
+
id: value.id,
|
|
476
|
+
type: value.type,
|
|
477
|
+
description: value.description,
|
|
478
|
+
status: value.status,
|
|
479
|
+
result: value.result,
|
|
480
|
+
error: value.error,
|
|
481
|
+
toolUses: 0,
|
|
482
|
+
startedAt: value.startedAt,
|
|
483
|
+
completedAt: value.completedAt,
|
|
484
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
485
|
+
compactionCount: 0,
|
|
486
|
+
// Historical records have no inline tool surface and should remain
|
|
487
|
+
// visible in the background widget.
|
|
488
|
+
isBackground: true,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
425
492
|
listAgents() {
|
|
426
493
|
return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
427
494
|
}
|
|
@@ -527,16 +594,17 @@ export class AgentManager {
|
|
|
527
594
|
this.agents.clear();
|
|
528
595
|
// Prune any orphaned git worktrees (crash recovery)
|
|
529
596
|
try {
|
|
530
|
-
pruneWorktrees(process.cwd());
|
|
597
|
+
(0, worktree_js_1.pruneWorktrees)(process.cwd());
|
|
531
598
|
}
|
|
532
599
|
catch { /* ignore */ }
|
|
533
600
|
// Also prune repos that caller-supplied cwds created worktrees in — a clean
|
|
534
601
|
// exit with in-flight agents would otherwise leave stale registrations there.
|
|
535
602
|
for (const repo of this.worktreeRepos) {
|
|
536
603
|
try {
|
|
537
|
-
pruneWorktrees(repo);
|
|
604
|
+
(0, worktree_js_1.pruneWorktrees)(repo);
|
|
538
605
|
}
|
|
539
606
|
catch { /* ignore */ }
|
|
540
607
|
}
|
|
541
608
|
}
|
|
542
609
|
}
|
|
610
|
+
exports.AgentManager = AgentManager;
|
package/dist/agent-runner.js
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
1
2
|
/**
|
|
2
3
|
* agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SUBAGENT_TOOL_NAMES = void 0;
|
|
7
|
+
exports.extensionCanonicalName = extensionCanonicalName;
|
|
8
|
+
exports.extensionCanonicalNames = extensionCanonicalNames;
|
|
9
|
+
exports.parseExtensionsSpec = parseExtensionsSpec;
|
|
10
|
+
exports.parseExtSelectors = parseExtSelectors;
|
|
11
|
+
exports.installExtensionToolScope = installExtensionToolScope;
|
|
12
|
+
exports.normalizeMaxTurns = normalizeMaxTurns;
|
|
13
|
+
exports.getDefaultMaxTurns = getDefaultMaxTurns;
|
|
14
|
+
exports.setDefaultMaxTurns = setDefaultMaxTurns;
|
|
15
|
+
exports.getGraceTurns = getGraceTurns;
|
|
16
|
+
exports.setGraceTurns = setGraceTurns;
|
|
17
|
+
exports.runAgent = runAgent;
|
|
18
|
+
exports.resumeAgent = resumeAgent;
|
|
19
|
+
exports.steerAgent = steerAgent;
|
|
20
|
+
exports.getAgentConversation = getAgentConversation;
|
|
21
|
+
const node_fs_1 = require("node:fs");
|
|
22
|
+
const node_os_1 = require("node:os");
|
|
23
|
+
const node_path_1 = require("node:path");
|
|
24
|
+
const pi_coding_agent_1 = require("@earendil-works/pi-coding-agent");
|
|
25
|
+
const agent_types_js_1 = require("./agent-types.js");
|
|
26
|
+
const context_js_1 = require("./context.js");
|
|
27
|
+
const default_agents_js_1 = require("./default-agents.js");
|
|
28
|
+
const env_js_1 = require("./env.js");
|
|
29
|
+
const memory_js_1 = require("./memory.js");
|
|
30
|
+
const prompts_js_1 = require("./prompts.js");
|
|
31
|
+
const skill_loader_js_1 = require("./skill-loader.js");
|
|
15
32
|
/**
|
|
16
33
|
* Tool names registered by THIS extension. Single source of truth so the
|
|
17
34
|
* registration sites (index.ts) and the subagent exclusion list below can't
|
|
18
35
|
* drift apart. These are our own tools, not pi built-ins, so they can't be
|
|
19
36
|
* derived from pi — but they only need defining once.
|
|
20
37
|
*/
|
|
21
|
-
|
|
38
|
+
exports.SUBAGENT_TOOL_NAMES = {
|
|
22
39
|
AGENT: "Agent",
|
|
23
40
|
GET_RESULT: "get_subagent_result",
|
|
24
41
|
STEER: "steer_subagent",
|
|
25
42
|
};
|
|
26
43
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
27
|
-
const EXCLUDED_TOOL_NAMES = Object.values(SUBAGENT_TOOL_NAMES);
|
|
44
|
+
const EXCLUDED_TOOL_NAMES = Object.values(exports.SUBAGENT_TOOL_NAMES);
|
|
28
45
|
/**
|
|
29
46
|
* Canonical name of an extension for `extensions: [...]` allowlist matching.
|
|
30
47
|
* Lowercased — extension names match case-insensitively so `extensions: [Mcp]`
|
|
@@ -32,10 +49,10 @@ const EXCLUDED_TOOL_NAMES = Object.values(SUBAGENT_TOOL_NAMES);
|
|
|
32
49
|
* Directory extensions (`foo/index.ts`) resolve to the parent directory name;
|
|
33
50
|
* single-file extensions to the basename minus `.ts`/`.js`.
|
|
34
51
|
*/
|
|
35
|
-
|
|
36
|
-
const base = basename(extPath);
|
|
52
|
+
function extensionCanonicalName(extPath) {
|
|
53
|
+
const base = (0, node_path_1.basename)(extPath);
|
|
37
54
|
const name = base === "index.ts" || base === "index.js"
|
|
38
|
-
? basename(dirname(extPath))
|
|
55
|
+
? (0, node_path_1.basename)((0, node_path_1.dirname)(extPath))
|
|
39
56
|
: base.replace(/\.(ts|js)$/, "");
|
|
40
57
|
return name.toLowerCase();
|
|
41
58
|
}
|
|
@@ -59,18 +76,18 @@ export function extensionCanonicalName(extPath) {
|
|
|
59
76
|
* co-located file to `pi-subagents`.
|
|
60
77
|
*/
|
|
61
78
|
function extensionPackageName(extPath) {
|
|
62
|
-
const entry = resolve(extPath);
|
|
63
|
-
let dir = dirname(extPath);
|
|
79
|
+
const entry = (0, node_path_1.resolve)(extPath);
|
|
80
|
+
let dir = (0, node_path_1.dirname)(extPath);
|
|
64
81
|
for (;;) {
|
|
65
82
|
// Climbing into node_modules means we've left the owning package's tree.
|
|
66
|
-
if (basename(dir) === "node_modules")
|
|
83
|
+
if ((0, node_path_1.basename)(dir) === "node_modules")
|
|
67
84
|
return undefined;
|
|
68
85
|
let pkg;
|
|
69
86
|
try {
|
|
70
|
-
pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
|
|
87
|
+
pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, "package.json"), "utf-8"));
|
|
71
88
|
}
|
|
72
89
|
catch {
|
|
73
|
-
const parent = dirname(dir);
|
|
90
|
+
const parent = (0, node_path_1.dirname)(dir);
|
|
74
91
|
if (parent === dir)
|
|
75
92
|
return undefined; // walked to the filesystem root
|
|
76
93
|
dir = parent;
|
|
@@ -80,7 +97,7 @@ function extensionPackageName(extPath) {
|
|
|
80
97
|
const entries = pkg.pi?.extensions;
|
|
81
98
|
if (typeof pkg.name === "string" &&
|
|
82
99
|
Array.isArray(entries) &&
|
|
83
|
-
entries.some((e) => typeof e === "string" && resolve(dir, e) === entry)) {
|
|
100
|
+
entries.some((e) => typeof e === "string" && (0, node_path_1.resolve)(dir, e) === entry)) {
|
|
84
101
|
const short = pkg.name.startsWith("@") ? pkg.name.slice(pkg.name.indexOf("/") + 1) : pkg.name;
|
|
85
102
|
return short.toLowerCase();
|
|
86
103
|
}
|
|
@@ -95,7 +112,7 @@ function extensionPackageName(extPath) {
|
|
|
95
112
|
* otherwise only ever match as `src` (the source directory), never by its
|
|
96
113
|
* package name. The path-derived name is preserved, so it keeps matching too.
|
|
97
114
|
*/
|
|
98
|
-
|
|
115
|
+
function extensionCanonicalNames(extPath) {
|
|
99
116
|
const canonical = extensionCanonicalName(extPath);
|
|
100
117
|
const pkg = extensionPackageName(extPath);
|
|
101
118
|
return pkg && pkg !== canonical ? [canonical, pkg] : [canonical];
|
|
@@ -111,7 +128,7 @@ export function extensionCanonicalNames(extPath) {
|
|
|
111
128
|
* everything by canonical name, so path-loaded extensions are matched via their name
|
|
112
129
|
* rather than their post-staging `Extension.path`.
|
|
113
130
|
*/
|
|
114
|
-
|
|
131
|
+
function parseExtensionsSpec(entries, cwd) {
|
|
115
132
|
const names = new Set();
|
|
116
133
|
const paths = [];
|
|
117
134
|
let wildcard = false;
|
|
@@ -129,9 +146,9 @@ export function parseExtensionsSpec(entries, cwd) {
|
|
|
129
146
|
}
|
|
130
147
|
let p = entry;
|
|
131
148
|
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
|
|
132
|
-
p = homedir() + p.slice(1);
|
|
149
|
+
p = (0, node_os_1.homedir)() + p.slice(1);
|
|
133
150
|
}
|
|
134
|
-
const abs = isAbsolute(p) ? p : resolve(cwd, p);
|
|
151
|
+
const abs = (0, node_path_1.isAbsolute)(p) ? p : (0, node_path_1.resolve)(cwd, p);
|
|
135
152
|
paths.push(abs);
|
|
136
153
|
names.add(extensionCanonicalName(abs));
|
|
137
154
|
}
|
|
@@ -147,7 +164,7 @@ export function parseExtensionsSpec(entries, cwd) {
|
|
|
147
164
|
* `ext:foo` alongside `ext:foo/bar` leaves narrowing in effect (narrowing wins).
|
|
148
165
|
* The split is on the first `/`; extension canonical names never contain `/`.
|
|
149
166
|
*/
|
|
150
|
-
|
|
167
|
+
function parseExtSelectors(entries) {
|
|
151
168
|
const extNames = new Set();
|
|
152
169
|
const narrowing = new Map();
|
|
153
170
|
for (const raw of entries) {
|
|
@@ -201,7 +218,7 @@ export function parseExtSelectors(entries) {
|
|
|
201
218
|
* Only meaningful when extensions are loaded — under `noExtensions`/`isolated` the
|
|
202
219
|
* static `allowedToolNames` allowlist already gates the registry itself.
|
|
203
220
|
*/
|
|
204
|
-
|
|
221
|
+
function installExtensionToolScope(session, ctx) {
|
|
205
222
|
const { loader, toolNames, disallowedSet, extNames, narrowing } = ctx;
|
|
206
223
|
// The names allowed right now. Mirrors the `ext:` opt-in flip: when any `ext:`
|
|
207
224
|
// selector is present, extension tools become an explicit allowlist — a loaded
|
|
@@ -260,21 +277,21 @@ export function installExtensionToolScope(session, ctx) {
|
|
|
260
277
|
/** Default max turns. undefined = unlimited (no turn limit). */
|
|
261
278
|
let defaultMaxTurns;
|
|
262
279
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
263
|
-
|
|
280
|
+
function normalizeMaxTurns(n) {
|
|
264
281
|
if (n == null || n === 0)
|
|
265
282
|
return undefined;
|
|
266
283
|
return Math.max(1, n);
|
|
267
284
|
}
|
|
268
285
|
/** Get the default max turns value. undefined = unlimited. */
|
|
269
|
-
|
|
286
|
+
function getDefaultMaxTurns() { return defaultMaxTurns; }
|
|
270
287
|
/** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
271
|
-
|
|
288
|
+
function setDefaultMaxTurns(n) { defaultMaxTurns = normalizeMaxTurns(n); }
|
|
272
289
|
/** Additional turns allowed after the soft limit steer message. */
|
|
273
290
|
let graceTurns = 5;
|
|
274
291
|
/** Get the grace turns value. */
|
|
275
|
-
|
|
292
|
+
function getGraceTurns() { return graceTurns; }
|
|
276
293
|
/** Set the grace turns value (minimum 1). */
|
|
277
|
-
|
|
294
|
+
function setGraceTurns(n) { graceTurns = Math.max(1, n); }
|
|
278
295
|
/**
|
|
279
296
|
* Try to find the right model for an agent type.
|
|
280
297
|
* Priority: explicit option > config.model > parent model.
|
|
@@ -329,7 +346,7 @@ function getLastAssistantText(session, startIndex = 0) {
|
|
|
329
346
|
const msg = session.messages[i];
|
|
330
347
|
if (msg.role !== "assistant")
|
|
331
348
|
continue;
|
|
332
|
-
const text = extractText(msg.content).trim();
|
|
349
|
+
const text = (0, context_js_1.extractText)(msg.content).trim();
|
|
333
350
|
if (text)
|
|
334
351
|
return text;
|
|
335
352
|
}
|
|
@@ -357,7 +374,7 @@ function finalTurnError(session, startIndex = 0) {
|
|
|
357
374
|
if (msg.stopReason === "error") {
|
|
358
375
|
return msg.errorMessage?.trim() || "provider error with no output";
|
|
359
376
|
}
|
|
360
|
-
if (msg.stopReason === "length" && !extractText(msg.content).trim()) {
|
|
377
|
+
if (msg.stopReason === "length" && !(0, context_js_1.extractText)(msg.content).trim()) {
|
|
361
378
|
return "run hit the output token limit before producing any text";
|
|
362
379
|
}
|
|
363
380
|
return undefined;
|
|
@@ -379,20 +396,20 @@ function resolveConfiguredSessionDir(sessionDir, cwd) {
|
|
|
379
396
|
if (!sessionDir)
|
|
380
397
|
return undefined;
|
|
381
398
|
if (sessionDir === "~" || sessionDir.startsWith("~/"))
|
|
382
|
-
return resolve(homedir(), sessionDir.slice(2));
|
|
383
|
-
if (isAbsolute(sessionDir))
|
|
399
|
+
return (0, node_path_1.resolve)((0, node_os_1.homedir)(), sessionDir.slice(2));
|
|
400
|
+
if ((0, node_path_1.isAbsolute)(sessionDir))
|
|
384
401
|
return sessionDir;
|
|
385
|
-
return resolve(cwd, sessionDir);
|
|
402
|
+
return (0, node_path_1.resolve)(cwd, sessionDir);
|
|
386
403
|
}
|
|
387
|
-
|
|
388
|
-
const config = getConfig(type);
|
|
389
|
-
const agentConfig = getAgentConfig(type);
|
|
404
|
+
async function runAgent(ctx, type, prompt, options) {
|
|
405
|
+
const config = (0, agent_types_js_1.getConfig)(type);
|
|
406
|
+
const agentConfig = (0, agent_types_js_1.getAgentConfig)(type);
|
|
390
407
|
// Resolve working directory: worktree override > parent cwd
|
|
391
408
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
392
409
|
// Filesystem work happens in effectiveCwd; config discovery in configCwd.
|
|
393
410
|
// They differ only for SpawnOptions.cwd spawns (config stays with the parent).
|
|
394
411
|
const configCwd = options.configCwd ?? effectiveCwd;
|
|
395
|
-
const env = await detectEnv(options.pi, effectiveCwd);
|
|
412
|
+
const env = await (0, env_js_1.detectEnv)(options.pi, effectiveCwd);
|
|
396
413
|
// Get parent system prompt for append-mode agents
|
|
397
414
|
const parentSystemPrompt = ctx.getSystemPrompt();
|
|
398
415
|
// Build prompt extras (memory, skill preloading)
|
|
@@ -405,12 +422,12 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
405
422
|
const skills = options.isolated ? false : config.skills;
|
|
406
423
|
// Skill preloading: when skills is string[], preload their content into prompt
|
|
407
424
|
if (Array.isArray(skills)) {
|
|
408
|
-
const loaded = preloadSkills(skills, configCwd);
|
|
425
|
+
const loaded = (0, skill_loader_js_1.preloadSkills)(skills, configCwd);
|
|
409
426
|
if (loaded.length > 0) {
|
|
410
427
|
extras.skillBlocks = loaded;
|
|
411
428
|
}
|
|
412
429
|
}
|
|
413
|
-
let toolNames = getToolNamesForType(type);
|
|
430
|
+
let toolNames = (0, agent_types_js_1.getToolNamesForType)(type);
|
|
414
431
|
// Persistent memory: detect write capability and branch accordingly.
|
|
415
432
|
// Account for disallowedTools — a tool in the base set but on the denylist is not truly available.
|
|
416
433
|
if (agentConfig?.memory) {
|
|
@@ -420,36 +437,36 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
420
437
|
const hasWriteTools = effectivelyHas("write") || effectivelyHas("edit");
|
|
421
438
|
if (hasWriteTools) {
|
|
422
439
|
// Read-write memory: add any missing memory tool names (read/write/edit)
|
|
423
|
-
const extraNames = getMemoryToolNames(existingNames);
|
|
440
|
+
const extraNames = (0, agent_types_js_1.getMemoryToolNames)(existingNames);
|
|
424
441
|
if (extraNames.length > 0)
|
|
425
442
|
toolNames = [...toolNames, ...extraNames];
|
|
426
|
-
extras.memoryBlock = buildMemoryBlock(agentConfig.name, agentConfig.memory, configCwd);
|
|
443
|
+
extras.memoryBlock = (0, memory_js_1.buildMemoryBlock)(agentConfig.name, agentConfig.memory, configCwd);
|
|
427
444
|
}
|
|
428
445
|
else {
|
|
429
446
|
// Read-only memory: only add read tool name, use read-only prompt
|
|
430
|
-
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
447
|
+
const extraNames = (0, agent_types_js_1.getReadOnlyMemoryToolNames)(existingNames);
|
|
431
448
|
if (extraNames.length > 0)
|
|
432
449
|
toolNames = [...toolNames, ...extraNames];
|
|
433
|
-
extras.memoryBlock = buildReadOnlyMemoryBlock(agentConfig.name, agentConfig.memory, configCwd);
|
|
450
|
+
extras.memoryBlock = (0, memory_js_1.buildReadOnlyMemoryBlock)(agentConfig.name, agentConfig.memory, configCwd);
|
|
434
451
|
}
|
|
435
452
|
}
|
|
436
453
|
// Build system prompt from agent config
|
|
437
454
|
let systemPrompt;
|
|
438
455
|
if (agentConfig) {
|
|
439
|
-
systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, parentSystemPrompt, extras);
|
|
456
|
+
systemPrompt = (0, prompts_js_1.buildAgentPrompt)(agentConfig, effectiveCwd, env, parentSystemPrompt, extras);
|
|
440
457
|
}
|
|
441
458
|
else {
|
|
442
459
|
// Unknown type fallback: spread the canonical general-purpose config (defensive —
|
|
443
460
|
// unreachable in practice since index.ts resolves unknown types before calling runAgent).
|
|
444
|
-
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
461
|
+
const fallback = default_agents_js_1.DEFAULT_AGENTS.get("general-purpose");
|
|
445
462
|
if (!fallback)
|
|
446
463
|
throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
447
|
-
systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, parentSystemPrompt, extras);
|
|
464
|
+
systemPrompt = (0, prompts_js_1.buildAgentPrompt)({ ...fallback, name: type }, effectiveCwd, env, parentSystemPrompt, extras);
|
|
448
465
|
}
|
|
449
466
|
// When skills is string[], we've already preloaded them into the prompt.
|
|
450
467
|
// Still pass noSkills: true since we don't need the skill loader to load them again.
|
|
451
468
|
const noSkills = skills === false || Array.isArray(skills);
|
|
452
|
-
const agentDir = getAgentDir();
|
|
469
|
+
const agentDir = (0, pi_coding_agent_1.getAgentDir)();
|
|
453
470
|
// Extension loading:
|
|
454
471
|
// - true → all default-discovered extensions
|
|
455
472
|
// - false → none (noExtensions)
|
|
@@ -502,7 +519,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
502
519
|
}),
|
|
503
520
|
};
|
|
504
521
|
};
|
|
505
|
-
const loader = new DefaultResourceLoader({
|
|
522
|
+
const loader = new pi_coding_agent_1.DefaultResourceLoader({
|
|
506
523
|
cwd: configCwd,
|
|
507
524
|
agentDir,
|
|
508
525
|
noExtensions,
|
|
@@ -521,7 +538,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
521
538
|
// this produced a silently broken agent (#75) — pi-mono accepted the bogus name
|
|
522
539
|
// into the allowlist, then dropped it at registration with no signal back.
|
|
523
540
|
if (agentConfig?.builtinToolNames?.length) {
|
|
524
|
-
const knownBuiltins = new Set(BUILTIN_TOOL_NAMES);
|
|
541
|
+
const knownBuiltins = new Set(agent_types_js_1.BUILTIN_TOOL_NAMES);
|
|
525
542
|
for (const name of agentConfig.builtinToolNames) {
|
|
526
543
|
if (!knownBuiltins.has(name)) {
|
|
527
544
|
options.onToolActivity?.({
|
|
@@ -624,7 +641,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
624
641
|
else {
|
|
625
642
|
const denyTools = new Set(EXCLUDED_TOOL_NAMES);
|
|
626
643
|
// Keep only the built-ins the agent asked for — deny the rest.
|
|
627
|
-
for (const name of BUILTIN_TOOL_NAMES) {
|
|
644
|
+
for (const name of agent_types_js_1.BUILTIN_TOOL_NAMES) {
|
|
628
645
|
if (!builtinToolNameSet.has(name))
|
|
629
646
|
denyTools.add(name);
|
|
630
647
|
}
|
|
@@ -634,12 +651,12 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
634
651
|
}
|
|
635
652
|
sessionExcludeTools = [...denyTools];
|
|
636
653
|
}
|
|
637
|
-
const settingsManager = SettingsManager.create(configCwd, agentDir);
|
|
654
|
+
const settingsManager = pi_coding_agent_1.SettingsManager.create(configCwd, agentDir);
|
|
638
655
|
const configuredSessionDir = resolveConfiguredSessionDir(agentConfig?.sessionDir, effectiveCwd);
|
|
639
656
|
const defaultSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR ?? settingsManager.getSessionDir?.();
|
|
640
657
|
const sessionManager = agentConfig?.persistSession
|
|
641
|
-
? SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir)
|
|
642
|
-
: SessionManager.inMemory(effectiveCwd);
|
|
658
|
+
? pi_coding_agent_1.SessionManager.create(effectiveCwd, configuredSessionDir ?? defaultSessionDir)
|
|
659
|
+
: pi_coding_agent_1.SessionManager.inMemory(effectiveCwd);
|
|
643
660
|
// Pi 0.80.8 replaced createAgentSession's modelRegistry option with
|
|
644
661
|
// modelRuntime, but ExtensionContext still exposes only the registry facade.
|
|
645
662
|
// Pass both so the full supported Pi range retains the parent's providers.
|
|
@@ -650,7 +667,9 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
650
667
|
sessionManager,
|
|
651
668
|
settingsManager,
|
|
652
669
|
modelRegistry: ctx.modelRegistry,
|
|
653
|
-
...(parentModelRuntime
|
|
670
|
+
...(parentModelRuntime != null && {
|
|
671
|
+
modelRuntime: parentModelRuntime,
|
|
672
|
+
}),
|
|
654
673
|
model,
|
|
655
674
|
tools: sessionTools,
|
|
656
675
|
resourceLoader: loader,
|
|
@@ -661,7 +680,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
661
680
|
if (thinkingLevel) {
|
|
662
681
|
sessionOpts.thinkingLevel = thinkingLevel;
|
|
663
682
|
}
|
|
664
|
-
const { session } = await createAgentSession(sessionOpts);
|
|
683
|
+
const { session } = await (0, pi_coding_agent_1.createAgentSession)(sessionOpts);
|
|
665
684
|
const baseSessionName = agentConfig?.name ?? type;
|
|
666
685
|
session.setSessionName(options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName);
|
|
667
686
|
// Bind extensions so that session_start fires and extensions can initialize
|
|
@@ -744,7 +763,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
744
763
|
// Build the effective prompt: optionally prepend parent context
|
|
745
764
|
let effectivePrompt = prompt;
|
|
746
765
|
if (options.inheritContext) {
|
|
747
|
-
const parentContext = buildParentContext(ctx);
|
|
766
|
+
const parentContext = (0, context_js_1.buildParentContext)(ctx);
|
|
748
767
|
if (parentContext) {
|
|
749
768
|
effectivePrompt = parentContext + prompt;
|
|
750
769
|
}
|
|
@@ -766,7 +785,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
766
785
|
/**
|
|
767
786
|
* Send a new prompt to an existing session (resume).
|
|
768
787
|
*/
|
|
769
|
-
|
|
788
|
+
async function resumeAgent(session, prompt, options = {}) {
|
|
770
789
|
// Boundary for the history fallback: the session already holds prior turns,
|
|
771
790
|
// so only assistant text produced by THIS resume prompt counts as its output
|
|
772
791
|
// — a failed resume must not surface the previous turn's answer (#144).
|
|
@@ -810,19 +829,19 @@ export async function resumeAgent(session, prompt, options = {}) {
|
|
|
810
829
|
* Send a steering message to a running subagent.
|
|
811
830
|
* The message will interrupt the agent after its current tool execution.
|
|
812
831
|
*/
|
|
813
|
-
|
|
832
|
+
async function steerAgent(session, message) {
|
|
814
833
|
await session.steer(message);
|
|
815
834
|
}
|
|
816
835
|
/**
|
|
817
836
|
* Get the subagent's conversation messages as formatted text.
|
|
818
837
|
*/
|
|
819
|
-
|
|
838
|
+
function getAgentConversation(session) {
|
|
820
839
|
const parts = [];
|
|
821
840
|
for (const msg of session.messages) {
|
|
822
841
|
if (msg.role === "user") {
|
|
823
842
|
const text = typeof msg.content === "string"
|
|
824
843
|
? msg.content
|
|
825
|
-
: extractText(msg.content);
|
|
844
|
+
: (0, context_js_1.extractText)(msg.content);
|
|
826
845
|
if (text.trim())
|
|
827
846
|
parts.push(`[User]: ${text.trim()}`);
|
|
828
847
|
}
|
|
@@ -841,7 +860,7 @@ export function getAgentConversation(session) {
|
|
|
841
860
|
parts.push(`[Tool Calls]:\n${toolCalls.join("\n")}`);
|
|
842
861
|
}
|
|
843
862
|
else if (msg.role === "toolResult") {
|
|
844
|
-
const text = extractText(msg.content);
|
|
863
|
+
const text = (0, context_js_1.extractText)(msg.content);
|
|
845
864
|
const truncated = text.length > 200 ? text.slice(0, 200) + "..." : text;
|
|
846
865
|
parts.push(`[Tool Result (${msg.toolName})]: ${truncated}`);
|
|
847
866
|
}
|