@esso0428/pi-subagents 0.15.4 → 0.15.6
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 +4 -17
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +18 -0
- package/dist/agent-history-list.js +68 -0
- package/dist/agent-history.d.ts +1 -1
- package/dist/agent-history.js +31 -34
- package/dist/agent-manager.d.ts +22 -8
- package/dist/agent-manager.js +100 -93
- package/dist/agent-runner.js +67 -86
- package/dist/agent-types.js +27 -45
- package/dist/context.js +2 -6
- package/dist/cross-extension-rpc.js +5 -9
- package/dist/custom-agents.js +15 -18
- package/dist/default-agents.js +1 -4
- package/dist/enabled-models.js +11 -16
- package/dist/env.js +1 -4
- package/dist/group-join.js +1 -5
- package/dist/index.js +273 -284
- package/dist/invocation-config.js +2 -6
- package/dist/memory.js +24 -34
- package/dist/model-resolver.js +1 -4
- package/dist/nico-overrides.js +14 -20
- package/dist/output-file.js +16 -22
- package/dist/prompts.js +1 -4
- package/dist/schedule-store.js +16 -21
- package/dist/schedule.js +8 -12
- package/dist/settings.js +15 -23
- package/dist/skill-loader.js +20 -23
- package/dist/status-note.js +1 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.js +1 -4
- package/dist/ui/agent-widget.d.ts +1 -5
- package/dist/ui/agent-widget.js +25 -49
- package/dist/ui/conversation-blocks.d.ts +18 -0
- package/dist/ui/conversation-blocks.js +214 -0
- package/dist/ui/conversation-nvim.d.ts +7 -0
- package/dist/ui/conversation-nvim.js +116 -0
- package/dist/ui/conversation-search.d.ts +38 -0
- package/dist/ui/conversation-search.js +123 -0
- package/dist/ui/conversation-viewer.d.ts +55 -19
- package/dist/ui/conversation-viewer.js +398 -215
- package/dist/ui/fleet-list.d.ts +23 -11
- package/dist/ui/fleet-list.js +85 -78
- package/dist/ui/schedule-menu.js +1 -4
- package/dist/ui/viewer-keys.js +7 -10
- package/dist/usage.js +4 -10
- package/dist/worktree.js +26 -31
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +108 -81
- package/src/agent-runner.ts +5 -13
- package/src/index.ts +84 -85
- package/src/output-file.ts +1 -1
- package/src/types.ts +0 -1
- package/src/ui/agent-widget.ts +1 -8
- package/src/ui/conversation-blocks.ts +270 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-viewer.ts +376 -205
- package/src/ui/fleet-list.ts +73 -52
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/ui/conversation-blocks.test.ts +72 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +132 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -7,25 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.15.
|
|
10
|
+
## [0.15.6] - 2026-09-11
|
|
11
11
|
|
|
12
12
|
### Fixed
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### Added
|
|
18
|
-
- **Reload-safe transcript history.** Agents now keep a project-local copy under `.pi-subagents/agent-transcripts/`; terminal records persist a relative locator and `/agents`/FleetView can open a read-only history viewer after reload. The shared `.pi-subagents/.gitignore` is created with `*` to keep private transcripts out of git.
|
|
19
|
-
|
|
20
|
-
## [0.15.2] - 2026-09-10
|
|
21
|
-
|
|
22
|
-
### Added
|
|
23
|
-
- **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.
|
|
24
|
-
|
|
25
|
-
## [0.15.1] - 2026-09-10
|
|
26
|
-
|
|
27
|
-
### Added
|
|
28
|
-
- **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.
|
|
13
|
+
- **FleetView stays within a six-row budget** while preserving full-roster keyboard navigation, selected-row visibility, overflow indicators, and prompt-area separation.
|
|
14
|
+
- **Agent history and live FleetView are routed separately**, keeping terminal records in the read-only history viewer instead of treating them as active sessions.
|
|
15
|
+
- **Pi 0.85.1 model-runtime type compatibility** is restored without changing the legacy fallback path.
|
|
29
16
|
|
|
30
17
|
## [0.14.3] - 2026-07-23
|
|
31
18
|
|
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. Expandable to show full output. 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,9 +138,9 @@ 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 agent output inline
|
|
141
|
+
Completed results can be expanded (ctrl+o in pi) to show the full agent output inline.
|
|
142
142
|
|
|
143
|
-
By default, foreground and background agents stream their full conversation to
|
|
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
|
|
|
145
145
|
```
|
|
146
146
|
✓ Find auth files completed
|
|
@@ -222,7 +222,7 @@ All fields are optional — sensible defaults for everything.
|
|
|
222
222
|
| `thinking` | inherit | off, minimal, low, medium, high, xhigh, max — actual availability depends on your pi version and model; pi clamps unsupported levels down |
|
|
223
223
|
| `max_turns` | unlimited | Max agentic turns before graceful shutdown. `0` or omit for unlimited |
|
|
224
224
|
| `persist_session` | `false` | Persist this subagent as a normal pi session instead of keeping the session in memory only. The subagent's `.output` transcript is still written either way unless `output_transcript: false` |
|
|
225
|
-
| `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write
|
|
225
|
+
| `output_transcript` | `true` (or `subagents.json` `outputTranscript`) | Write this subagent's `.output` transcript; when set, overrides the `subagents.json` `outputTranscript` default. Set `false` to write no transcript file or path. Governs only the transcript — independent of `persist_session`, `isolation: worktree`, and `memory:` |
|
|
226
226
|
| `session_dir` | pi default | Optional session directory when `persist_session: true`; omitted uses pi's normal session location, and relative paths resolve from the agent cwd |
|
|
227
227
|
| `prompt_mode` | `replace` | `replace`: body is the full system prompt (no AGENTS.md / CLAUDE.md inheritance). `append`: body appended to parent's prompt (agent acts as a "parent twin" — inherits parent's AGENTS.md / CLAUDE.md) |
|
|
228
228
|
| `inherit_context` | `false` | Fork parent conversation into agent |
|
|
@@ -508,7 +508,7 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, default m
|
|
|
508
508
|
|
|
509
509
|
**Disable defaults** (`disableDefaultAgents`, default `false`): when on, the three built-in agents (general-purpose, Explore, Plan) are not registered — only your project/global custom agents are advertised and spawnable. User-defined agents are unaffected, including ones that override a default by name. The Agent tool's type list updates on the next pi session (the tool schema is registered at startup).
|
|
510
510
|
|
|
511
|
-
**Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's
|
|
511
|
+
**Output transcript** (`outputTranscript`, default `true`): the project/global default for writing each subagent's `.output` transcript. Toggle via `/agents → Settings → Output transcript`, or set `false` in `subagents.json` to make transcripts opt-in project-wide — useful when run transcripts shouldn't sit on disk for backup or DLP tooling to pick up. A custom agent's `output_transcript` frontmatter overrides this per agent. Applied live at spawn time. Governs only the transcript, not `persist_session`, worktree commits, or memory files.
|
|
512
512
|
|
|
513
513
|
**Tool description** (`toolDescriptionMode`, default `"full"`): which Agent tool description the LLM sees. `"full"` is the rich Claude Code-style prompt (~1,400 tokens with the default agents); `"compact"` is ~75% smaller — one-line agent type list, terse usage notes — for small/local models where tool-spec tokens are expensive. Per-option details stay in the parameter descriptions in every mode (the parameter schema is never customizable). Applies on the next pi session.
|
|
514
514
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Helpers for separating openable active agents from terminal history. */
|
|
2
|
+
import type { AgentRecord } from "./types.js";
|
|
3
|
+
export type AgentHistoryStatus = "completed" | "steered" | "stopped" | "aborted" | "error";
|
|
4
|
+
/** Return whether a record status represents a terminal agent run. */
|
|
5
|
+
export declare function isTerminalAgentStatus(status: string): status is AgentHistoryStatus;
|
|
6
|
+
/** Return whether an active record has a live session that can be opened. */
|
|
7
|
+
export declare function canOpenActiveAgent(record: AgentRecord): boolean;
|
|
8
|
+
/** Return whether a terminal record has an in-memory or durable conversation. */
|
|
9
|
+
export declare function canOpenAgentHistory(record: AgentRecord, cwd: string | undefined): boolean;
|
|
10
|
+
/** Split records into openable active and terminal-history buckets. */
|
|
11
|
+
export declare function splitAgentRecords(records: readonly AgentRecord[], cwd: string | undefined): {
|
|
12
|
+
active: AgentRecord[];
|
|
13
|
+
history: AgentRecord[];
|
|
14
|
+
};
|
|
15
|
+
/** Format a bounded, single-line history menu option. */
|
|
16
|
+
export declare function formatAgentHistoryOption(record: AgentRecord, now: number): string;
|
|
17
|
+
/** Build the visible status-section labels for the agents menu. */
|
|
18
|
+
export declare function buildAgentStatusMenuEntries(records: readonly AgentRecord[], cwd: string | undefined): string[];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Helpers for separating openable active agents from terminal history. */
|
|
2
|
+
import { hasAgentHistory } from "./agent-history.js";
|
|
3
|
+
import { getAgentConfig } from "./agent-types.js";
|
|
4
|
+
import { formatDuration, getDisplayName } from "./ui/agent-widget.js";
|
|
5
|
+
const TERMINAL_STATUSES = new Set([
|
|
6
|
+
"completed",
|
|
7
|
+
"steered",
|
|
8
|
+
"stopped",
|
|
9
|
+
"aborted",
|
|
10
|
+
"error",
|
|
11
|
+
]);
|
|
12
|
+
/** Return whether a record status represents a terminal agent run. */
|
|
13
|
+
export function isTerminalAgentStatus(status) {
|
|
14
|
+
return TERMINAL_STATUSES.has(status);
|
|
15
|
+
}
|
|
16
|
+
/** Return whether an active record has a live session that can be opened. */
|
|
17
|
+
export function canOpenActiveAgent(record) {
|
|
18
|
+
return (record.status === "running" || record.status === "queued")
|
|
19
|
+
&& record.session !== undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Return whether a terminal record has an in-memory or durable conversation. */
|
|
22
|
+
export function canOpenAgentHistory(record, cwd) {
|
|
23
|
+
if (!isTerminalAgentStatus(record.status))
|
|
24
|
+
return false;
|
|
25
|
+
return record.session !== undefined
|
|
26
|
+
|| (cwd !== undefined && hasAgentHistory(cwd, record.transcriptPath));
|
|
27
|
+
}
|
|
28
|
+
/** Split records into openable active and terminal-history buckets. */
|
|
29
|
+
export function splitAgentRecords(records, cwd) {
|
|
30
|
+
const active = [];
|
|
31
|
+
const history = [];
|
|
32
|
+
for (const record of records) {
|
|
33
|
+
if (canOpenActiveAgent(record)) {
|
|
34
|
+
active.push(record);
|
|
35
|
+
}
|
|
36
|
+
else if (canOpenAgentHistory(record, cwd)) {
|
|
37
|
+
history.push(record);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { active, history };
|
|
41
|
+
}
|
|
42
|
+
const DESCRIPTION_LIMIT = 72;
|
|
43
|
+
const LABEL_LIMIT = 140;
|
|
44
|
+
function boundedDescription(description) {
|
|
45
|
+
const line = description.split("\n").find((part) => part.trim())?.trim() ?? "";
|
|
46
|
+
if (line.length <= DESCRIPTION_LIMIT)
|
|
47
|
+
return line;
|
|
48
|
+
return `${line.slice(0, DESCRIPTION_LIMIT - 1)}…`;
|
|
49
|
+
}
|
|
50
|
+
/** Format a bounded, single-line history menu option. */
|
|
51
|
+
export function formatAgentHistoryOption(record, now) {
|
|
52
|
+
const duration = formatDuration(record.startedAt, record.completedAt ?? now);
|
|
53
|
+
// Unit consumers may format persisted records before the registry has been
|
|
54
|
+
// initialized; preserve the persisted type instead of falling back to
|
|
55
|
+
// general-purpose's generic display name in that case.
|
|
56
|
+
const displayName = getAgentConfig(record.type)?.displayName
|
|
57
|
+
?? (record.type === "general-purpose" ? getDisplayName(record.type) : record.type);
|
|
58
|
+
const option = `[${record.status}] ${displayName} · ${boundedDescription(record.description)} · ${duration}`;
|
|
59
|
+
return option.length <= LABEL_LIMIT ? option : `${option.slice(0, LABEL_LIMIT - 1)}…`;
|
|
60
|
+
}
|
|
61
|
+
/** Build the visible status-section labels for the agents menu. */
|
|
62
|
+
export function buildAgentStatusMenuEntries(records, cwd) {
|
|
63
|
+
const { active, history } = splitAgentRecords(records, cwd);
|
|
64
|
+
return [
|
|
65
|
+
...(active.length > 0 ? [`Running agents (${active.length})`] : []),
|
|
66
|
+
...(history.length > 0 ? [`Agent history (${history.length})`] : []),
|
|
67
|
+
];
|
|
68
|
+
}
|
package/dist/agent-history.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function ensureSubagentsGitignore(cwd: string): string;
|
|
11
11
|
/** Return the durable transcript path for an agent. */
|
|
12
|
-
export declare function createAgentHistoryPath(cwd: string, agentId: string
|
|
12
|
+
export declare function createAgentHistoryPath(cwd: string, agentId: string): string;
|
|
13
13
|
/** Return the project-relative path stored in the parent session record. */
|
|
14
14
|
export declare function agentHistoryLocator(cwd: string, historyPath: string): string;
|
|
15
15
|
/** Resolve only paths in this package's project-local transcript namespace. */
|
package/dist/agent-history.js
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/** Durable, project-local transcript storage for subagents. */
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.createAgentHistoryPath = createAgentHistoryPath;
|
|
6
|
-
exports.agentHistoryLocator = agentHistoryLocator;
|
|
7
|
-
exports.resolveAgentHistoryPath = resolveAgentHistoryPath;
|
|
8
|
-
exports.hasAgentHistory = hasAgentHistory;
|
|
9
|
-
exports.readAgentHistory = readAgentHistory;
|
|
10
|
-
const node_fs_1 = require("node:fs");
|
|
11
|
-
const node_path_1 = require("node:path");
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
4
|
const SUBAGENTS_DIR = ".pi-subagents";
|
|
13
5
|
const TRANSCRIPTS_DIR = "agent-transcripts";
|
|
14
6
|
const MAX_HISTORY_BYTES = 20 * 1024 * 1024;
|
|
@@ -19,65 +11,70 @@ const MAX_HISTORY_BYTES = 20 * 1024 * 1024;
|
|
|
19
11
|
* existing user-owned rules and makes concurrent callers harmless (duplicate
|
|
20
12
|
* `*` rules are semantically equivalent).
|
|
21
13
|
*/
|
|
22
|
-
function ensureSubagentsGitignore(cwd) {
|
|
23
|
-
const directory =
|
|
24
|
-
|
|
25
|
-
const path =
|
|
14
|
+
export function ensureSubagentsGitignore(cwd) {
|
|
15
|
+
const directory = join(cwd, SUBAGENTS_DIR);
|
|
16
|
+
mkdirSync(directory, { recursive: true });
|
|
17
|
+
const path = join(directory, ".gitignore");
|
|
26
18
|
let content = "";
|
|
27
19
|
try {
|
|
28
|
-
content =
|
|
20
|
+
content = readFileSync(path, "utf8");
|
|
29
21
|
}
|
|
30
22
|
catch {
|
|
31
23
|
// The append below also creates a missing file.
|
|
32
24
|
}
|
|
33
25
|
if (!content.split(/\r?\n/).some((line) => line.trim() === "*")) {
|
|
34
26
|
const separator = content.length > 0 && !/[\r\n]$/.test(content) ? "\n" : "";
|
|
35
|
-
|
|
27
|
+
appendFileSync(path, `${separator}*\n`, "utf8");
|
|
36
28
|
}
|
|
37
29
|
return path;
|
|
38
30
|
}
|
|
39
31
|
/** Return the durable transcript path for an agent. */
|
|
40
|
-
function createAgentHistoryPath(cwd, agentId
|
|
32
|
+
export function createAgentHistoryPath(cwd, agentId) {
|
|
41
33
|
ensureSubagentsGitignore(cwd);
|
|
42
|
-
const directory =
|
|
43
|
-
|
|
34
|
+
const directory = join(cwd, SUBAGENTS_DIR, TRANSCRIPTS_DIR);
|
|
35
|
+
mkdirSync(directory, { recursive: true });
|
|
44
36
|
const safeId = agentId.replace(/[^A-Za-z0-9._-]+/g, "-") || "agent";
|
|
45
|
-
|
|
46
|
-
return (0, node_path_1.join)(directory, `${safeId}_${safeType}_transcript.jsonl`);
|
|
37
|
+
return join(directory, `${safeId}.jsonl`);
|
|
47
38
|
}
|
|
48
39
|
/** Return the project-relative path stored in the parent session record. */
|
|
49
|
-
function agentHistoryLocator(cwd, historyPath) {
|
|
50
|
-
return
|
|
40
|
+
export function agentHistoryLocator(cwd, historyPath) {
|
|
41
|
+
return relative(cwd, historyPath).split(sep).join("/");
|
|
51
42
|
}
|
|
52
43
|
/** Resolve only paths in this package's project-local transcript namespace. */
|
|
53
|
-
function resolveAgentHistoryPath(cwd, locator) {
|
|
54
|
-
|
|
44
|
+
export function resolveAgentHistoryPath(cwd, locator) {
|
|
45
|
+
// Locators are persisted data, not arbitrary paths. Reject traversal syntax
|
|
46
|
+
// before resolving so a path which normalizes back inside the namespace is
|
|
47
|
+
// still not accepted as an unsafe locator.
|
|
48
|
+
if (!locator || isAbsolute(locator) || locator.includes("\0") || locator.includes("\\"))
|
|
55
49
|
return undefined;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
const segments = locator.split("/");
|
|
51
|
+
if (segments.some((segment) => segment === ".." || segment === ""))
|
|
52
|
+
return undefined;
|
|
53
|
+
const root = resolve(cwd, SUBAGENTS_DIR, TRANSCRIPTS_DIR);
|
|
54
|
+
const candidate = resolve(cwd, locator);
|
|
55
|
+
if (candidate !== root && !candidate.startsWith(`${root}${sep}`))
|
|
59
56
|
return undefined;
|
|
60
57
|
return candidate;
|
|
61
58
|
}
|
|
62
59
|
/** Return true only when a valid project-local transcript file exists. */
|
|
63
|
-
function hasAgentHistory(cwd, locator) {
|
|
60
|
+
export function hasAgentHistory(cwd, locator) {
|
|
64
61
|
if (!locator)
|
|
65
62
|
return false;
|
|
66
63
|
const path = resolveAgentHistoryPath(cwd, locator);
|
|
67
|
-
return path !== undefined &&
|
|
64
|
+
return path !== undefined && existsSync(path);
|
|
68
65
|
}
|
|
69
66
|
/**
|
|
70
67
|
* Read persisted transcript entries into the message shape used by the live
|
|
71
68
|
* conversation viewer. Malformed lines and unknown records are skipped so one
|
|
72
69
|
* damaged entry cannot hide the rest of a history.
|
|
73
70
|
*/
|
|
74
|
-
function readAgentHistory(cwd, locator) {
|
|
71
|
+
export function readAgentHistory(cwd, locator) {
|
|
75
72
|
const path = resolveAgentHistoryPath(cwd, locator);
|
|
76
|
-
if (!path)
|
|
73
|
+
if (!path || !existsSync(path))
|
|
77
74
|
return undefined;
|
|
78
75
|
let raw;
|
|
79
76
|
try {
|
|
80
|
-
raw =
|
|
77
|
+
raw = readFileSync(path, "utf8");
|
|
81
78
|
}
|
|
82
79
|
catch {
|
|
83
80
|
return undefined;
|
|
@@ -90,7 +87,7 @@ function readAgentHistory(cwd, locator) {
|
|
|
90
87
|
continue;
|
|
91
88
|
try {
|
|
92
89
|
const entry = JSON.parse(line);
|
|
93
|
-
const message = entry
|
|
90
|
+
const message = entry?.message;
|
|
94
91
|
if (!message || typeof message !== "object")
|
|
95
92
|
continue;
|
|
96
93
|
const role = message.role;
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -16,6 +16,25 @@ export type CompactionInfo = {
|
|
|
16
16
|
reason: "manual" | "threshold" | "overflow";
|
|
17
17
|
tokensBefore: number;
|
|
18
18
|
};
|
|
19
|
+
type RestorableAgentStatus = "completed" | "steered" | "stopped" | "aborted" | "error";
|
|
20
|
+
/** Validate one persisted terminal record without constructing runtime handles. */
|
|
21
|
+
export declare function isRestorableAgentRecord(value: unknown): value is {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
status: RestorableAgentStatus;
|
|
26
|
+
startedAt: number;
|
|
27
|
+
completedAt: number;
|
|
28
|
+
result?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
toolUses?: number;
|
|
31
|
+
lifetimeUsage?: {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheWrite: number;
|
|
35
|
+
};
|
|
36
|
+
transcriptPath?: string;
|
|
37
|
+
};
|
|
19
38
|
interface SpawnOptions {
|
|
20
39
|
description: string;
|
|
21
40
|
model?: Model<any>;
|
|
@@ -121,15 +140,10 @@ export declare class AgentManager {
|
|
|
121
140
|
*/
|
|
122
141
|
steer(id: string, message: string): boolean;
|
|
123
142
|
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;
|
|
132
143
|
listAgents(): AgentRecord[];
|
|
144
|
+
/** Restore terminal records persisted by a parent branch without runtime handles. */
|
|
145
|
+
restoreCompleted(records: readonly unknown[]): void;
|
|
146
|
+
private createRestoredRecord;
|
|
133
147
|
abort(id: string): boolean;
|
|
134
148
|
/** Dispose a record's session and remove it from the map. */
|
|
135
149
|
private removeRecord;
|
package/dist/agent-manager.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* agent-manager.ts — Tracks agents, background execution, resume support.
|
|
4
3
|
*
|
|
@@ -6,23 +5,64 @@
|
|
|
6
5
|
* Excess agents are queued and auto-started as running agents complete.
|
|
7
6
|
* Foreground agents bypass the queue (they block the parent anyway).
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const usage_js_1 = require("./usage.js");
|
|
16
|
-
const worktree_js_1 = require("./worktree.js");
|
|
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";
|
|
17
14
|
/** Default max concurrent background agents. */
|
|
18
15
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
const RESTORABLE_STATUSES = new Set(["completed", "steered", "stopped", "aborted", "error"]);
|
|
17
|
+
/** Narrow persisted strings before putting them into runtime/UI state. */
|
|
18
|
+
function isSafePersistedString(value, maxLength) {
|
|
19
|
+
return typeof value === "string" && value.length > 0 && value.length <= maxLength && !/[\0\r\n]/.test(value);
|
|
20
|
+
}
|
|
21
|
+
function isFiniteTimestamp(value) {
|
|
22
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value) && value >= 0;
|
|
23
|
+
}
|
|
24
|
+
function isValidUsage(value) {
|
|
25
|
+
if (!value || typeof value !== "object")
|
|
26
|
+
return false;
|
|
27
|
+
const usage = value;
|
|
28
|
+
return ["input", "output", "cacheWrite"].every((key) => {
|
|
29
|
+
const n = usage[key];
|
|
30
|
+
return typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function isSafeTranscriptLocator(value) {
|
|
34
|
+
return typeof value === "string"
|
|
35
|
+
&& /^\.pi-subagents\/agent-transcripts\/[^/]+\.jsonl$/.test(value)
|
|
36
|
+
&& !value.includes("..")
|
|
37
|
+
&& !value.includes("\\")
|
|
38
|
+
&& !value.includes("\0");
|
|
39
|
+
}
|
|
40
|
+
/** Validate one persisted terminal record without constructing runtime handles. */
|
|
41
|
+
export function isRestorableAgentRecord(value) {
|
|
42
|
+
if (!value || typeof value !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
const record = value;
|
|
45
|
+
if (!isSafePersistedString(record.id, 256)
|
|
46
|
+
|| !isSafePersistedString(record.type, 256)
|
|
47
|
+
|| !isSafePersistedString(record.description, 4096)
|
|
48
|
+
|| typeof record.status !== "string"
|
|
49
|
+
|| !RESTORABLE_STATUSES.has(record.status)
|
|
50
|
+
|| !isFiniteTimestamp(record.startedAt)
|
|
51
|
+
|| !isFiniteTimestamp(record.completedAt)
|
|
52
|
+
|| record.completedAt < record.startedAt)
|
|
53
|
+
return false;
|
|
54
|
+
if (record.result !== undefined && !isSafePersistedString(record.result, 2_000_000))
|
|
55
|
+
return false;
|
|
56
|
+
if (record.error !== undefined && !isSafePersistedString(record.error, 64_000))
|
|
57
|
+
return false;
|
|
58
|
+
if (record.toolUses !== undefined && (!Number.isInteger(record.toolUses) || record.toolUses < 0))
|
|
59
|
+
return false;
|
|
60
|
+
if (record.lifetimeUsage !== undefined && !isValidUsage(record.lifetimeUsage))
|
|
61
|
+
return false;
|
|
62
|
+
if (record.transcriptPath !== undefined && !isSafeTranscriptLocator(record.transcriptPath))
|
|
63
|
+
return false;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
26
66
|
/**
|
|
27
67
|
* Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
|
|
28
68
|
* (parent cwd). Anything else must be an absolute path to an existing
|
|
@@ -32,12 +72,12 @@ const TERMINAL_STATUSES = new Set([
|
|
|
32
72
|
function assertValidSpawnCwd(cwd) {
|
|
33
73
|
if (cwd == null)
|
|
34
74
|
return;
|
|
35
|
-
if (typeof cwd !== "string" || !
|
|
75
|
+
if (typeof cwd !== "string" || !isAbsolute(cwd)) {
|
|
36
76
|
throw new Error(`SpawnOptions.cwd must be an absolute path: "${String(cwd)}"`);
|
|
37
77
|
}
|
|
38
78
|
let isDirectory = false;
|
|
39
79
|
try {
|
|
40
|
-
isDirectory =
|
|
80
|
+
isDirectory = statSync(cwd).isDirectory();
|
|
41
81
|
}
|
|
42
82
|
catch {
|
|
43
83
|
throw new Error(`SpawnOptions.cwd does not exist: "${cwd}"`);
|
|
@@ -46,37 +86,7 @@ function assertValidSpawnCwd(cwd) {
|
|
|
46
86
|
throw new Error(`SpawnOptions.cwd is not a directory: "${cwd}"`);
|
|
47
87
|
}
|
|
48
88
|
}
|
|
49
|
-
|
|
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
|
-
const transcriptPath = record.transcriptPath;
|
|
61
|
-
const validTranscriptPath = transcriptPath === undefined ||
|
|
62
|
-
(typeof transcriptPath === "string" &&
|
|
63
|
-
transcriptPath.startsWith(".pi-subagents/agent-transcripts/") &&
|
|
64
|
-
!transcriptPath.includes("..") &&
|
|
65
|
-
!transcriptPath.includes("\\"));
|
|
66
|
-
return (typeof record.id === "string" &&
|
|
67
|
-
record.id.length > 0 &&
|
|
68
|
-
typeof record.type === "string" &&
|
|
69
|
-
typeof record.description === "string" &&
|
|
70
|
-
RESTORABLE_STATUSES.has(record.status) &&
|
|
71
|
-
typeof record.startedAt === "number" &&
|
|
72
|
-
Number.isFinite(record.startedAt) &&
|
|
73
|
-
typeof record.completedAt === "number" &&
|
|
74
|
-
Number.isFinite(record.completedAt) &&
|
|
75
|
-
(record.result === undefined || typeof record.result === "string") &&
|
|
76
|
-
(record.error === undefined || typeof record.error === "string") &&
|
|
77
|
-
validTranscriptPath);
|
|
78
|
-
}
|
|
79
|
-
class AgentManager {
|
|
89
|
+
export class AgentManager {
|
|
80
90
|
agents = new Map();
|
|
81
91
|
cleanupInterval;
|
|
82
92
|
onComplete;
|
|
@@ -117,7 +127,7 @@ class AgentManager {
|
|
|
117
127
|
// call, not minutes later at drain. Throw (not warn): programmatic callers
|
|
118
128
|
// can fix and retry; the RPC layer converts throws into error envelopes.
|
|
119
129
|
assertValidSpawnCwd(options.cwd);
|
|
120
|
-
const id =
|
|
130
|
+
const id = randomUUID().slice(0, 17);
|
|
121
131
|
const abortController = new AbortController();
|
|
122
132
|
const record = {
|
|
123
133
|
id,
|
|
@@ -170,7 +180,7 @@ class AgentManager {
|
|
|
170
180
|
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
171
181
|
let worktreeCwd;
|
|
172
182
|
if (options.isolation === "worktree") {
|
|
173
|
-
const wt =
|
|
183
|
+
const wt = createWorktree(baseCwd, id);
|
|
174
184
|
if (!wt) {
|
|
175
185
|
throw new Error('Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
176
186
|
'Initialize git and commit at least once, or omit `isolation`.');
|
|
@@ -198,7 +208,7 @@ class AgentManager {
|
|
|
198
208
|
detachParentSignal = () => options.signal.removeEventListener("abort", onParentAbort);
|
|
199
209
|
}
|
|
200
210
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
201
|
-
const promise =
|
|
211
|
+
const promise = runAgent(ctx, type, prompt, {
|
|
202
212
|
pi,
|
|
203
213
|
agentId: id,
|
|
204
214
|
model: options.model,
|
|
@@ -222,7 +232,7 @@ class AgentManager {
|
|
|
222
232
|
onTurnEnd: options.onTurnEnd,
|
|
223
233
|
onTextDelta: options.onTextDelta,
|
|
224
234
|
onAssistantUsage: (usage) => {
|
|
225
|
-
|
|
235
|
+
addUsage(record.lifetimeUsage, usage);
|
|
226
236
|
options.onAssistantUsage?.(usage);
|
|
227
237
|
},
|
|
228
238
|
onCompaction: (info) => {
|
|
@@ -273,7 +283,7 @@ class AgentManager {
|
|
|
273
283
|
}
|
|
274
284
|
// Clean up worktree if used
|
|
275
285
|
if (record.worktree) {
|
|
276
|
-
const wtResult =
|
|
286
|
+
const wtResult = cleanupWorktree(baseCwd, record.worktree, options.description);
|
|
277
287
|
record.worktreeResult = wtResult;
|
|
278
288
|
if (wtResult.hasChanges && wtResult.branch) {
|
|
279
289
|
// With a caller-supplied cwd the branch lives in THAT repo, not the
|
|
@@ -321,7 +331,7 @@ class AgentManager {
|
|
|
321
331
|
// Best-effort worktree cleanup on error
|
|
322
332
|
if (record.worktree) {
|
|
323
333
|
try {
|
|
324
|
-
const wtResult =
|
|
334
|
+
const wtResult = cleanupWorktree(baseCwd, record.worktree, options.description);
|
|
325
335
|
record.worktreeResult = wtResult;
|
|
326
336
|
}
|
|
327
337
|
catch { /* ignore cleanup errors */ }
|
|
@@ -406,13 +416,13 @@ class AgentManager {
|
|
|
406
416
|
record.result = undefined;
|
|
407
417
|
record.error = undefined;
|
|
408
418
|
try {
|
|
409
|
-
const { text, failure } = await
|
|
419
|
+
const { text, failure } = await resumeAgent(record.session, prompt, {
|
|
410
420
|
onToolActivity: (activity) => {
|
|
411
421
|
if (activity.type === "end")
|
|
412
422
|
record.toolUses++;
|
|
413
423
|
},
|
|
414
424
|
onAssistantUsage: (usage) => {
|
|
415
|
-
|
|
425
|
+
addUsage(record.lifetimeUsage, usage);
|
|
416
426
|
},
|
|
417
427
|
onCompaction: (info) => {
|
|
418
428
|
record.compactionCount++;
|
|
@@ -462,43 +472,41 @@ class AgentManager {
|
|
|
462
472
|
getRecord(id) {
|
|
463
473
|
return this.agents.get(id);
|
|
464
474
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
* controller. Invalid data is ignored because session entries are persisted
|
|
470
|
-
* extension data and may have been written by an older version.
|
|
471
|
-
*/
|
|
475
|
+
listAgents() {
|
|
476
|
+
return [...this.agents.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
477
|
+
}
|
|
478
|
+
/** Restore terminal records persisted by a parent branch without runtime handles. */
|
|
472
479
|
restoreCompleted(records) {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
480
|
+
const latest = new Map();
|
|
481
|
+
for (const value of records) {
|
|
482
|
+
if (isRestorableAgentRecord(value)) {
|
|
483
|
+
latest.set(value.id, this.createRestoredRecord(value));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
for (const [id, restored] of latest) {
|
|
487
|
+
const existing = this.agents.get(id);
|
|
488
|
+
if (existing?.status === "running" || existing?.status === "queued")
|
|
479
489
|
continue;
|
|
480
|
-
|
|
481
|
-
this.agents.set(value.id, {
|
|
482
|
-
id: value.id,
|
|
483
|
-
type: value.type,
|
|
484
|
-
description: value.description,
|
|
485
|
-
status: value.status,
|
|
486
|
-
result: value.result,
|
|
487
|
-
error: value.error,
|
|
488
|
-
transcriptPath: value.transcriptPath,
|
|
489
|
-
toolUses: 0,
|
|
490
|
-
startedAt: value.startedAt,
|
|
491
|
-
completedAt: value.completedAt,
|
|
492
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
493
|
-
compactionCount: 0,
|
|
494
|
-
// Historical records have no inline tool surface and should remain
|
|
495
|
-
// visible in the background widget.
|
|
496
|
-
isBackground: true,
|
|
497
|
-
});
|
|
490
|
+
this.agents.set(id, restored);
|
|
498
491
|
}
|
|
499
492
|
}
|
|
500
|
-
|
|
501
|
-
return
|
|
493
|
+
createRestoredRecord(record) {
|
|
494
|
+
return {
|
|
495
|
+
id: record.id,
|
|
496
|
+
type: record.type,
|
|
497
|
+
description: record.description,
|
|
498
|
+
status: record.status,
|
|
499
|
+
result: record.result,
|
|
500
|
+
error: record.error,
|
|
501
|
+
toolUses: record.toolUses ?? 0,
|
|
502
|
+
startedAt: record.startedAt,
|
|
503
|
+
completedAt: record.completedAt,
|
|
504
|
+
transcriptPath: record.transcriptPath,
|
|
505
|
+
lifetimeUsage: record.lifetimeUsage
|
|
506
|
+
? { ...record.lifetimeUsage }
|
|
507
|
+
: { input: 0, output: 0, cacheWrite: 0 },
|
|
508
|
+
compactionCount: 0,
|
|
509
|
+
};
|
|
502
510
|
}
|
|
503
511
|
abort(id) {
|
|
504
512
|
const record = this.agents.get(id);
|
|
@@ -602,17 +610,16 @@ class AgentManager {
|
|
|
602
610
|
this.agents.clear();
|
|
603
611
|
// Prune any orphaned git worktrees (crash recovery)
|
|
604
612
|
try {
|
|
605
|
-
|
|
613
|
+
pruneWorktrees(process.cwd());
|
|
606
614
|
}
|
|
607
615
|
catch { /* ignore */ }
|
|
608
616
|
// Also prune repos that caller-supplied cwds created worktrees in — a clean
|
|
609
617
|
// exit with in-flight agents would otherwise leave stale registrations there.
|
|
610
618
|
for (const repo of this.worktreeRepos) {
|
|
611
619
|
try {
|
|
612
|
-
|
|
620
|
+
pruneWorktrees(repo);
|
|
613
621
|
}
|
|
614
622
|
catch { /* ignore */ }
|
|
615
623
|
}
|
|
616
624
|
}
|
|
617
625
|
}
|
|
618
|
-
exports.AgentManager = AgentManager;
|