@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/dist/agent-manager.d.ts +8 -0
  4. package/dist/agent-manager.js +87 -19
  5. package/dist/agent-runner.js +85 -66
  6. package/dist/agent-types.js +45 -27
  7. package/dist/context.js +6 -2
  8. package/dist/cross-extension-rpc.js +9 -5
  9. package/dist/custom-agents.js +18 -15
  10. package/dist/default-agents.js +4 -1
  11. package/dist/enabled-models.js +16 -11
  12. package/dist/env.js +4 -1
  13. package/dist/group-join.js +5 -1
  14. package/dist/index.js +280 -221
  15. package/dist/invocation-config.js +6 -2
  16. package/dist/memory.js +34 -24
  17. package/dist/model-resolver.js +4 -1
  18. package/dist/nico-overrides.js +20 -14
  19. package/dist/output-file.js +21 -15
  20. package/dist/prompts.js +4 -1
  21. package/dist/schedule-store.js +21 -16
  22. package/dist/schedule.js +12 -8
  23. package/dist/settings.js +23 -15
  24. package/dist/skill-loader.js +23 -20
  25. package/dist/status-note.js +4 -1
  26. package/dist/types.d.ts +1 -0
  27. package/dist/types.js +4 -1
  28. package/dist/ui/agent-widget.js +37 -23
  29. package/dist/ui/conversation-viewer.js +43 -39
  30. package/dist/ui/fleet-list.js +30 -24
  31. package/dist/ui/markdown-result.d.ts +3 -0
  32. package/dist/ui/markdown-result.js +53 -0
  33. package/dist/ui/schedule-menu.js +4 -1
  34. package/dist/ui/viewer-keys.js +10 -7
  35. package/dist/usage.js +10 -4
  36. package/dist/worktree.js +31 -26
  37. package/package.json +1 -1
  38. package/src/agent-manager.ts +72 -0
  39. package/src/agent-runner.ts +11 -3
  40. package/src/index.ts +39 -16
  41. package/src/types.ts +1 -0
  42. package/src/ui/markdown-result.ts +56 -0
  43. package/test/agent-manager-history.test.ts +84 -0
  44. 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. Expandable to show full output. Group completions render each agent individually
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 full agent output inline.
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
 
@@ -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. */
@@ -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
- import { randomUUID } from "node:crypto";
9
- import { statSync } from "node:fs";
10
- import { isAbsolute } from "node:path";
11
- import { resumeAgent, runAgent } from "./agent-runner.js";
12
- import { addUsage } from "./usage.js";
13
- import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
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
- export class AgentManager {
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;
@@ -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
- import { readFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
- import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
- import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
9
- import { buildParentContext, extractText } from "./context.js";
10
- import { DEFAULT_AGENTS } from "./default-agents.js";
11
- import { detectEnv } from "./env.js";
12
- import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
13
- import { buildAgentPrompt } from "./prompts.js";
14
- import { preloadSkills } from "./skill-loader.js";
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
- export const SUBAGENT_TOOL_NAMES = {
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
- export function extensionCanonicalName(extPath) {
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
- export function extensionCanonicalNames(extPath) {
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
- export function parseExtensionsSpec(entries, cwd) {
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
- export function parseExtSelectors(entries) {
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
- export function installExtensionToolScope(session, ctx) {
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
- export function normalizeMaxTurns(n) {
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
- export function getDefaultMaxTurns() { return defaultMaxTurns; }
286
+ function getDefaultMaxTurns() { return defaultMaxTurns; }
270
287
  /** Set the default max turns value. undefined or 0 = unlimited, otherwise minimum 1. */
271
- export function setDefaultMaxTurns(n) { defaultMaxTurns = normalizeMaxTurns(n); }
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
- export function getGraceTurns() { return graceTurns; }
292
+ function getGraceTurns() { return graceTurns; }
276
293
  /** Set the grace turns value (minimum 1). */
277
- export function setGraceTurns(n) { graceTurns = Math.max(1, n); }
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
- export async function runAgent(ctx, type, prompt, options) {
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 !== undefined && { modelRuntime: 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
- export async function resumeAgent(session, prompt, options = {}) {
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
- export async function steerAgent(session, message) {
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
- export function getAgentConversation(session) {
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
  }