@gavin-hu/my-pi-agent 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -34
- package/README.zh-CN.md +44 -28
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +126 -37
- package/extensions/file-browse/client.ts +98 -33
- package/extensions/file-browse/config.ts +24 -9
- package/extensions/file-browse/files.ts +183 -48
- package/extensions/file-browse/fuzzy.ts +65 -11
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +97 -171
- package/extensions/file-browse/live-client.ts +158 -0
- package/extensions/file-browse/live.ts +222 -0
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +65 -78
- package/extensions/file-browse/server.ts +75 -59
- package/extensions/file-browse/styles.ts +124 -0
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +32 -79
- package/extensions/plan/web.ts +21 -42
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +192 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +101 -21
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/rewind/snapshot.ts +13 -4
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +15 -9
- package/lib/README.md +23 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +28 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +74 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +109 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/pool.ts +62 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +418 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* A background run is the same child process and result as a foreground one;
|
|
5
|
+
* only the wait moves off the tool call. The session branch is the durable
|
|
6
|
+
* record: a start entry, then a finish entry. The live map keeps a bounded
|
|
7
|
+
* preview and the final output for collection; after a restart the branch can
|
|
8
|
+
* only report a start without a finish as interrupted.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { formatDuration, sanitizeLine, stripControlChars } from "../../lib/format.ts";
|
|
13
|
+
import type { AgentConfig } from "./agents.ts";
|
|
14
|
+
import { formatUsageStats } from "./format.ts";
|
|
15
|
+
import type { RunOptions } from "./run.ts";
|
|
16
|
+
import { createResult, getFinalOutput, isFailedResult } from "./stream.ts";
|
|
17
|
+
import type { DispatchDefaults, SingleResult, UsageStats } from "./types.ts";
|
|
18
|
+
|
|
19
|
+
/** Custom-entry type for a background run start or finish. */
|
|
20
|
+
export const BACKGROUND_ENTRY = "subagent-background";
|
|
21
|
+
|
|
22
|
+
/** Cap on the output stored in the entry and sent by collect. */
|
|
23
|
+
const MAX_OUTPUT_CHARS = 50 * 1024;
|
|
24
|
+
/** Cap on the live preview shown while a run is active. */
|
|
25
|
+
const MAX_PREVIEW_CHARS = 8 * 1024;
|
|
26
|
+
/** Cap on the task stored in an entry. */
|
|
27
|
+
const MAX_TASK_CHARS = 4 * 1024;
|
|
28
|
+
|
|
29
|
+
export type BackgroundStatus = "running" | "finished" | "failed" | "cancelled" | "timedout" | "interrupted";
|
|
30
|
+
|
|
31
|
+
/** One background run as the board and collect see it. */
|
|
32
|
+
export interface BackgroundRun {
|
|
33
|
+
id: string;
|
|
34
|
+
agent: string;
|
|
35
|
+
task: string;
|
|
36
|
+
cwd: string;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
finishedAt?: number;
|
|
39
|
+
status: BackgroundStatus;
|
|
40
|
+
exitCode?: number | null;
|
|
41
|
+
stopReason?: string;
|
|
42
|
+
errorMessage?: string;
|
|
43
|
+
usage?: UsageStats;
|
|
44
|
+
/** Latest bounded output: a live preview while running, final output after. */
|
|
45
|
+
output?: string;
|
|
46
|
+
artifactPath?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface StartEntry {
|
|
50
|
+
phase: "start";
|
|
51
|
+
runId: string;
|
|
52
|
+
agent: string;
|
|
53
|
+
task: string;
|
|
54
|
+
cwd: string;
|
|
55
|
+
startedAt: number;
|
|
56
|
+
agentSource?: string;
|
|
57
|
+
readOnly?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface FinishEntry {
|
|
61
|
+
phase: "finish";
|
|
62
|
+
runId: string;
|
|
63
|
+
finishedAt: number;
|
|
64
|
+
status: Exclude<BackgroundStatus, "running" | "interrupted">;
|
|
65
|
+
exitCode: number | null;
|
|
66
|
+
stopReason?: string;
|
|
67
|
+
errorMessage?: string;
|
|
68
|
+
usage: UsageStats;
|
|
69
|
+
output: string;
|
|
70
|
+
artifactPath?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type BackgroundEntry = StartEntry | FinishEntry;
|
|
74
|
+
|
|
75
|
+
export interface BackgroundStartInput {
|
|
76
|
+
agent: string;
|
|
77
|
+
task: string;
|
|
78
|
+
cwd?: string;
|
|
79
|
+
readOnly?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type BackgroundStartResult = { ok: true; run: BackgroundRun } | { ok: false; error: string };
|
|
83
|
+
|
|
84
|
+
export interface BackgroundRuntime {
|
|
85
|
+
start(input: BackgroundStartInput, branch: readonly unknown[], run: BackgroundRunDeps): BackgroundStartResult;
|
|
86
|
+
list(branch: readonly unknown[]): BackgroundRun[];
|
|
87
|
+
/** The text a collect sends, or the reason it cannot be collected. */
|
|
88
|
+
collect(runId: string, branch: readonly unknown[]): { ok: true; text: string } | { ok: false; reason: string };
|
|
89
|
+
/** Abort one running child. */
|
|
90
|
+
cancel(runId: string): boolean;
|
|
91
|
+
/** Abort every child and stop accepting late writes. */
|
|
92
|
+
shutdown(): void;
|
|
93
|
+
/** Clear the disposed flag and live rows for a new session. */
|
|
94
|
+
reset(): void;
|
|
95
|
+
/** Repaint hook; returns an unsubscribe. */
|
|
96
|
+
subscribe(listener: () => void): () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface BackgroundDeps {
|
|
100
|
+
appendEntry(customType: string, data: unknown): void;
|
|
101
|
+
/** Called once when a run reaches a terminal state. */
|
|
102
|
+
onFinish?(run: BackgroundRun): void;
|
|
103
|
+
now?: () => number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Everything one start needs from the tool call's resolved context. */
|
|
107
|
+
export interface BackgroundRunDeps {
|
|
108
|
+
run(options: RunOptions): Promise<SingleResult>;
|
|
109
|
+
agents: readonly AgentConfig[];
|
|
110
|
+
defaults: DispatchDefaults;
|
|
111
|
+
defaultCwd: string;
|
|
112
|
+
timeoutMs: number;
|
|
113
|
+
transcripts?: RunOptions["transcripts"];
|
|
114
|
+
backgroundMax: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** A finite, non-negative number or undefined. */
|
|
118
|
+
function count(value: unknown): number | undefined {
|
|
119
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read one custom entry's data if it is a background entry. */
|
|
123
|
+
function readEntry(raw: unknown): BackgroundEntry | undefined {
|
|
124
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
125
|
+
const entry = raw as { type?: unknown; customType?: unknown; data?: unknown };
|
|
126
|
+
if (
|
|
127
|
+
entry.type !== "custom" ||
|
|
128
|
+
entry.customType !== BACKGROUND_ENTRY ||
|
|
129
|
+
!entry.data ||
|
|
130
|
+
typeof entry.data !== "object"
|
|
131
|
+
) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const data = entry.data as Record<string, unknown>;
|
|
135
|
+
if (typeof data.runId !== "string" || data.runId === "") return undefined;
|
|
136
|
+
if (data.phase === "start") {
|
|
137
|
+
if (typeof data.agent !== "string" || typeof data.task !== "string" || typeof data.cwd !== "string") {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const startedAt = count(data.startedAt);
|
|
141
|
+
if (startedAt === undefined) return undefined;
|
|
142
|
+
return {
|
|
143
|
+
phase: "start",
|
|
144
|
+
runId: data.runId,
|
|
145
|
+
agent: data.agent,
|
|
146
|
+
task: data.task,
|
|
147
|
+
cwd: data.cwd,
|
|
148
|
+
startedAt,
|
|
149
|
+
...(typeof data.agentSource === "string" ? { agentSource: data.agentSource } : {}),
|
|
150
|
+
...(typeof data.readOnly === "boolean" ? { readOnly: data.readOnly } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (data.phase === "finish") {
|
|
154
|
+
const finishedAt = count(data.finishedAt);
|
|
155
|
+
const usage = data.usage as UsageStats | undefined;
|
|
156
|
+
if (finishedAt === undefined || !usage || typeof usage !== "object") return undefined;
|
|
157
|
+
if (!["finished", "failed", "cancelled", "timedout"].includes(String(data.status))) return undefined;
|
|
158
|
+
return {
|
|
159
|
+
phase: "finish",
|
|
160
|
+
runId: data.runId,
|
|
161
|
+
finishedAt,
|
|
162
|
+
status: data.status as FinishEntry["status"],
|
|
163
|
+
exitCode: typeof data.exitCode === "number" ? data.exitCode : null,
|
|
164
|
+
...(typeof data.stopReason === "string" ? { stopReason: data.stopReason } : {}),
|
|
165
|
+
...(typeof data.errorMessage === "string" ? { errorMessage: data.errorMessage } : {}),
|
|
166
|
+
usage,
|
|
167
|
+
output: typeof data.output === "string" ? data.output : "",
|
|
168
|
+
...(typeof data.artifactPath === "string" ? { artifactPath: data.artifactPath } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The status a finished result represents. */
|
|
175
|
+
function statusOf(result: SingleResult): FinishEntry["status"] {
|
|
176
|
+
if (result.timedOut) return "timedout";
|
|
177
|
+
if (result.stopReason === "aborted") return "cancelled";
|
|
178
|
+
if (isFailedResult(result)) return "failed";
|
|
179
|
+
return "finished";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The final assistant text, bounded for entry storage and collect. */
|
|
183
|
+
function finalOutput(result: SingleResult): string {
|
|
184
|
+
const text = stripControlChars(getFinalOutput(result.messages) || result.stderr.trim());
|
|
185
|
+
return text.length > MAX_OUTPUT_CHARS ? `${text.slice(0, MAX_OUTPUT_CHARS)}\n… output truncated` : text;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The latest output for a running run, bounded and kept at the tail. */
|
|
189
|
+
function liveOutput(result: SingleResult): string {
|
|
190
|
+
const text = stripControlChars(getFinalOutput(result.messages) || result.stderr.trim());
|
|
191
|
+
if (text.length <= MAX_PREVIEW_CHARS) return text;
|
|
192
|
+
return `… earlier output omitted\n${text.slice(-MAX_PREVIEW_CHARS)}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** One run from a start entry and an optional finish. */
|
|
196
|
+
function runFromEntries(start: StartEntry, finish?: FinishEntry): BackgroundRun {
|
|
197
|
+
return {
|
|
198
|
+
id: start.runId,
|
|
199
|
+
agent: start.agent,
|
|
200
|
+
task: start.task,
|
|
201
|
+
cwd: start.cwd,
|
|
202
|
+
startedAt: start.startedAt,
|
|
203
|
+
status: finish?.status ?? "interrupted",
|
|
204
|
+
...(finish ? { finishedAt: finish.finishedAt, exitCode: finish.exitCode } : {}),
|
|
205
|
+
...(finish?.stopReason ? { stopReason: finish.stopReason } : {}),
|
|
206
|
+
...(finish?.errorMessage ? { errorMessage: finish.errorMessage } : {}),
|
|
207
|
+
...(finish?.usage ? { usage: finish.usage } : {}),
|
|
208
|
+
...(finish?.output ? { output: finish.output } : {}),
|
|
209
|
+
...(finish?.artifactPath ? { artifactPath: finish.artifactPath } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Read the branch into runs, newest first. */
|
|
214
|
+
function runsFromBranch(branch: readonly unknown[]): BackgroundRun[] {
|
|
215
|
+
const starts = new Map<string, StartEntry>();
|
|
216
|
+
const finishes = new Map<string, FinishEntry>();
|
|
217
|
+
for (const raw of branch) {
|
|
218
|
+
const entry = readEntry(raw);
|
|
219
|
+
if (!entry) continue;
|
|
220
|
+
if (entry.phase === "start") starts.set(entry.runId, entry);
|
|
221
|
+
else finishes.set(entry.runId, entry);
|
|
222
|
+
}
|
|
223
|
+
const runs = [...starts.values()].map((start) => runFromEntries(start, finishes.get(start.runId)));
|
|
224
|
+
return runs.sort((a, b) => b.startedAt - a.startedAt || b.id.localeCompare(a.id));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The next `aN` id after every start entry on the branch. */
|
|
228
|
+
function nextId(branch: readonly unknown[], live: ReadonlyMap<string, BackgroundRun>): string {
|
|
229
|
+
let max = 0;
|
|
230
|
+
for (const raw of branch) {
|
|
231
|
+
const entry = readEntry(raw);
|
|
232
|
+
if (entry?.phase !== "start") continue;
|
|
233
|
+
const match = /^a(\d+)$/.exec(entry.runId);
|
|
234
|
+
if (match) max = Math.max(max, Number.parseInt(match[1] ?? "0", 10));
|
|
235
|
+
}
|
|
236
|
+
for (const id of live.keys()) {
|
|
237
|
+
const match = /^a(\d+)$/.exec(id);
|
|
238
|
+
if (match) max = Math.max(max, Number.parseInt(match[1] ?? "0", 10));
|
|
239
|
+
}
|
|
240
|
+
return `a${max + 1}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A background run that is still running in this process. */
|
|
244
|
+
interface LiveRun {
|
|
245
|
+
controller: AbortController;
|
|
246
|
+
run: BackgroundRun;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function createBackgroundRuntime(deps: BackgroundDeps): BackgroundRuntime {
|
|
250
|
+
const now = deps.now ?? (() => Date.now());
|
|
251
|
+
const live = new Map<string, LiveRun>();
|
|
252
|
+
const listeners = new Set<() => void>();
|
|
253
|
+
let disposed = false;
|
|
254
|
+
|
|
255
|
+
const notify = (): void => {
|
|
256
|
+
for (const listener of listeners) {
|
|
257
|
+
try {
|
|
258
|
+
listener();
|
|
259
|
+
} catch {
|
|
260
|
+
// A closed board must not break a run.
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const runningCount = (): number => [...live.values()].filter((entry) => entry.run.status === "running").length;
|
|
266
|
+
|
|
267
|
+
const finish = (id: string, result: SingleResult): void => {
|
|
268
|
+
const entry = live.get(id);
|
|
269
|
+
if (!entry || disposed) return;
|
|
270
|
+
const status = statusOf(result);
|
|
271
|
+
const output = finalOutput(result);
|
|
272
|
+
const finishedAt = result.finishedAt ?? now();
|
|
273
|
+
entry.run = {
|
|
274
|
+
...entry.run,
|
|
275
|
+
status,
|
|
276
|
+
finishedAt,
|
|
277
|
+
exitCode: result.exitCode,
|
|
278
|
+
...(result.stopReason ? { stopReason: result.stopReason } : {}),
|
|
279
|
+
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
|
280
|
+
usage: result.usage,
|
|
281
|
+
output,
|
|
282
|
+
...(result.artifactPath ? { artifactPath: result.artifactPath } : {}),
|
|
283
|
+
};
|
|
284
|
+
try {
|
|
285
|
+
deps.appendEntry(BACKGROUND_ENTRY, {
|
|
286
|
+
phase: "finish",
|
|
287
|
+
runId: id,
|
|
288
|
+
finishedAt,
|
|
289
|
+
status,
|
|
290
|
+
exitCode: result.exitCode,
|
|
291
|
+
...(result.stopReason ? { stopReason: result.stopReason } : {}),
|
|
292
|
+
...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
|
|
293
|
+
usage: result.usage,
|
|
294
|
+
output,
|
|
295
|
+
...(result.artifactPath ? { artifactPath: result.artifactPath } : {}),
|
|
296
|
+
} satisfies FinishEntry);
|
|
297
|
+
} catch {
|
|
298
|
+
// The live row still reflects the result; a failed append must not crash.
|
|
299
|
+
}
|
|
300
|
+
notify();
|
|
301
|
+
try {
|
|
302
|
+
deps.onFinish?.(entry.run);
|
|
303
|
+
} catch {
|
|
304
|
+
// A UI listener must not fail a finished run.
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const failStart = (id: string, error: unknown): void => {
|
|
309
|
+
const entry = live.get(id);
|
|
310
|
+
if (!entry || disposed) return;
|
|
311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
312
|
+
const result = createResult(entry.run.agent, entry.run.task);
|
|
313
|
+
result.exitCode = 1;
|
|
314
|
+
result.stderr = message;
|
|
315
|
+
result.errorMessage = message;
|
|
316
|
+
result.finishedAt = now();
|
|
317
|
+
finish(id, result);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const start = (
|
|
321
|
+
input: BackgroundStartInput,
|
|
322
|
+
branch: readonly unknown[],
|
|
323
|
+
depsRun: BackgroundRunDeps,
|
|
324
|
+
): BackgroundStartResult => {
|
|
325
|
+
if (disposed) return { ok: false, error: "The subagent session is shutting down." };
|
|
326
|
+
const agent = depsRun.agents.find((candidate) => candidate.name === input.agent);
|
|
327
|
+
if (!agent) return { ok: false, error: `Unknown agent: "${input.agent}".` };
|
|
328
|
+
if (runningCount() >= depsRun.backgroundMax) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
error: `Too many background runs (${depsRun.backgroundMax}); wait for one to finish or cancel it.`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const id = nextId(branch, new Map([...live].map(([key, value]) => [key, value.run])));
|
|
336
|
+
const cwd = input.cwd ? resolve(depsRun.defaultCwd, input.cwd) : depsRun.defaultCwd;
|
|
337
|
+
const startedAt = now();
|
|
338
|
+
const task = input.task.slice(0, MAX_TASK_CHARS);
|
|
339
|
+
const record: BackgroundRun = {
|
|
340
|
+
id,
|
|
341
|
+
agent: input.agent,
|
|
342
|
+
task,
|
|
343
|
+
cwd,
|
|
344
|
+
startedAt,
|
|
345
|
+
status: "running",
|
|
346
|
+
};
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
live.set(id, { controller, run: record });
|
|
349
|
+
try {
|
|
350
|
+
deps.appendEntry(BACKGROUND_ENTRY, {
|
|
351
|
+
phase: "start",
|
|
352
|
+
runId: id,
|
|
353
|
+
agent: input.agent,
|
|
354
|
+
task,
|
|
355
|
+
cwd,
|
|
356
|
+
startedAt,
|
|
357
|
+
...(agent.source ? { agentSource: agent.source } : {}),
|
|
358
|
+
...(input.readOnly === true ? { readOnly: true } : {}),
|
|
359
|
+
} satisfies StartEntry);
|
|
360
|
+
} catch {
|
|
361
|
+
live.delete(id);
|
|
362
|
+
return { ok: false, error: "Could not record the background run." };
|
|
363
|
+
}
|
|
364
|
+
notify();
|
|
365
|
+
|
|
366
|
+
Promise.resolve()
|
|
367
|
+
.then(() =>
|
|
368
|
+
depsRun.run({
|
|
369
|
+
defaultCwd: depsRun.defaultCwd,
|
|
370
|
+
defaults: depsRun.defaults,
|
|
371
|
+
agentName: input.agent,
|
|
372
|
+
agents: depsRun.agents,
|
|
373
|
+
task,
|
|
374
|
+
cwd: input.cwd,
|
|
375
|
+
readOnly: input.readOnly,
|
|
376
|
+
abortAsResult: true,
|
|
377
|
+
signal: controller.signal,
|
|
378
|
+
timeoutMs: depsRun.timeoutMs,
|
|
379
|
+
transcripts: depsRun.transcripts,
|
|
380
|
+
makeDetails: (results) => ({ mode: "single", results }),
|
|
381
|
+
onUpdate: (partial) => {
|
|
382
|
+
const current = partial.details?.results[0];
|
|
383
|
+
if (!current) return;
|
|
384
|
+
const entry = live.get(id);
|
|
385
|
+
if (!entry) return;
|
|
386
|
+
const preview = liveOutput(current);
|
|
387
|
+
entry.run = {
|
|
388
|
+
...entry.run,
|
|
389
|
+
...(current.usage ? { usage: current.usage } : {}),
|
|
390
|
+
...(preview ? { output: preview } : {}),
|
|
391
|
+
};
|
|
392
|
+
notify();
|
|
393
|
+
},
|
|
394
|
+
}),
|
|
395
|
+
)
|
|
396
|
+
.then((result) => finish(id, result))
|
|
397
|
+
.catch((error) => failStart(id, error));
|
|
398
|
+
|
|
399
|
+
return { ok: true, run: { ...record } };
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const list = (branch: readonly unknown[]): BackgroundRun[] => {
|
|
403
|
+
const byId = new Map(runsFromBranch(branch).map((run) => [run.id, run]));
|
|
404
|
+
for (const [id, entry] of live) byId.set(id, entry.run);
|
|
405
|
+
return [...byId.values()].sort((a, b) => b.startedAt - a.startedAt || b.id.localeCompare(a.id));
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const collect = (
|
|
409
|
+
runId: string,
|
|
410
|
+
branch: readonly unknown[],
|
|
411
|
+
): { ok: true; text: string } | { ok: false; reason: string } => {
|
|
412
|
+
const entry = live.get(runId);
|
|
413
|
+
const run = entry?.run ?? runsFromBranch(branch).find((candidate) => candidate.id === runId);
|
|
414
|
+
if (!run) return { ok: false, reason: `No background run ${runId}.` };
|
|
415
|
+
if (run.status === "running") return { ok: false, reason: `${runId} is still running.` };
|
|
416
|
+
if (run.status === "interrupted") return { ok: false, reason: `${runId} was interrupted before it finished.` };
|
|
417
|
+
const output = run.output ?? "";
|
|
418
|
+
const usage = run.usage ? formatUsageStats(run.usage) : "";
|
|
419
|
+
const lines = [
|
|
420
|
+
`Background subagent ${run.id} (${sanitizeLine(run.agent)}) ${run.status}.`,
|
|
421
|
+
`Task: ${sanitizeLine(run.task)}`,
|
|
422
|
+
`Cwd: ${sanitizeLine(run.cwd)}`,
|
|
423
|
+
...(usage ? [`Usage: ${usage}`] : []),
|
|
424
|
+
...(run.errorMessage ? [`Error: ${sanitizeLine(run.errorMessage)}`] : []),
|
|
425
|
+
"",
|
|
426
|
+
output || "(no output)",
|
|
427
|
+
];
|
|
428
|
+
if (run.artifactPath) lines.push("", `Transcript: ${run.artifactPath}`);
|
|
429
|
+
return { ok: true, text: lines.join("\n") };
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const cancel = (runId: string): boolean => {
|
|
433
|
+
const entry = live.get(runId);
|
|
434
|
+
if (!entry || entry.run.status !== "running") return false;
|
|
435
|
+
entry.controller.abort();
|
|
436
|
+
notify();
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const shutdown = (): void => {
|
|
441
|
+
disposed = true;
|
|
442
|
+
for (const entry of live.values()) entry.controller.abort();
|
|
443
|
+
live.clear();
|
|
444
|
+
listeners.clear();
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const reset = (): void => {
|
|
448
|
+
disposed = false;
|
|
449
|
+
live.clear();
|
|
450
|
+
listeners.clear();
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
const subscribe = (listener: () => void): (() => void) => {
|
|
454
|
+
listeners.add(listener);
|
|
455
|
+
return () => listeners.delete(listener);
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
return { start, list, collect, cancel, shutdown, reset, subscribe };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A human duration for a run; kept here so the board and collect agree. */
|
|
462
|
+
export function backgroundDuration(run: BackgroundRun, now = Date.now()): string {
|
|
463
|
+
const end = run.finishedAt ?? now;
|
|
464
|
+
return formatDuration(Math.max(0, end - run.startedAt));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Whether a run can be collected. */
|
|
468
|
+
export function canCollect(run: BackgroundRun): boolean {
|
|
469
|
+
return run.status !== "running" && run.status !== "interrupted";
|
|
470
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/agents` — inspect, collect, and cancel background subagent runs.
|
|
3
|
+
*
|
|
4
|
+
* The board is a view over the session branch plus the live runtime; collecting
|
|
5
|
+
* closes the board and sends the result as a user message so the next turn sees
|
|
6
|
+
* it. Headless modes print the same list without actions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
11
|
+
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
12
|
+
import type { BackgroundRuntime } from "./background.ts";
|
|
13
|
+
import { AgentsBoardComponent } from "./background-tui.ts";
|
|
14
|
+
import { formatAgentsList } from "./format.ts";
|
|
15
|
+
|
|
16
|
+
/** Open `/agents`; the TUI gets the board, other modes get the list. */
|
|
17
|
+
async function openAgents(
|
|
18
|
+
pi: ExtensionAPI,
|
|
19
|
+
background: BackgroundRuntime,
|
|
20
|
+
ctx: ExtensionCommandContext,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (isPlanModeEnabled(ctx.sessionManager.getBranch())) {
|
|
23
|
+
ctx.ui.notify("Plan mode is read-only. Approve the plan before managing background runs.", "warning");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const runs = background.list(ctx.sessionManager.getBranch());
|
|
28
|
+
if (ctx.mode !== "tui") {
|
|
29
|
+
ctx.ui.notify(formatAgentsList(runs), "info");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (runs.length === 0) {
|
|
33
|
+
ctx.ui.notify("No background subagents yet.", "info");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let collectId: string | undefined;
|
|
38
|
+
await withRailsSuppressed(pi, async () => {
|
|
39
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
40
|
+
const unsubscribe = background.subscribe(() => tui.requestRender());
|
|
41
|
+
const component = new AgentsBoardComponent({
|
|
42
|
+
runs: () => background.list(ctx.sessionManager.getBranch()),
|
|
43
|
+
theme,
|
|
44
|
+
onCollect: (runId) => {
|
|
45
|
+
collectId = runId;
|
|
46
|
+
unsubscribe();
|
|
47
|
+
done();
|
|
48
|
+
},
|
|
49
|
+
onCancel: (runId) => {
|
|
50
|
+
background.cancel(runId);
|
|
51
|
+
},
|
|
52
|
+
onClose: () => {
|
|
53
|
+
unsubscribe();
|
|
54
|
+
done();
|
|
55
|
+
},
|
|
56
|
+
requestRender: () => tui.requestRender(),
|
|
57
|
+
viewportRows: () => tui.terminal?.rows,
|
|
58
|
+
});
|
|
59
|
+
return component;
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!collectId) return;
|
|
64
|
+
const collected = background.collect(collectId, ctx.sessionManager.getBranch());
|
|
65
|
+
if (!collected.ok) {
|
|
66
|
+
ctx.ui.notify(collected.reason, "warning");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (ctx.isIdle()) await pi.sendUserMessage(collected.text);
|
|
70
|
+
else await pi.sendUserMessage(collected.text, { deliverAs: "followUp" });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function registerAgentsCommand(pi: ExtensionAPI, background: BackgroundRuntime): void {
|
|
74
|
+
pi.registerCommand("agents", {
|
|
75
|
+
description: "Inspect, collect, or cancel background subagent runs",
|
|
76
|
+
handler: async (_args, ctx) => {
|
|
77
|
+
await openAgents(pi, background, ctx);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -23,6 +23,8 @@ export interface SubagentConfig {
|
|
|
23
23
|
retentionDays: number;
|
|
24
24
|
/** Most run directories kept, oldest pruned first. */
|
|
25
25
|
maxRuns: number;
|
|
26
|
+
/** Background children allowed to run at once. */
|
|
27
|
+
backgroundMax: number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export const DEFAULT_SUBAGENT_CONFIG: SubagentConfig = {
|
|
@@ -30,6 +32,7 @@ export const DEFAULT_SUBAGENT_CONFIG: SubagentConfig = {
|
|
|
30
32
|
transcripts: "off",
|
|
31
33
|
retentionDays: 14,
|
|
32
34
|
maxRuns: 100,
|
|
35
|
+
backgroundMax: 4,
|
|
33
36
|
};
|
|
34
37
|
|
|
35
38
|
/** An unknown value read as one of {@link TRANSCRIPT_MODES}, or `fallback`. */
|
|
@@ -49,6 +52,7 @@ export function normalizeSubagentConfig(
|
|
|
49
52
|
transcripts: asTranscriptMode(raw.transcripts, base.transcripts),
|
|
50
53
|
retentionDays: clampInteger(raw.retentionDays, base.retentionDays, 1, 3650),
|
|
51
54
|
maxRuns: clampInteger(raw.maxRuns, base.maxRuns, 1, 1000),
|
|
55
|
+
backgroundMax: clampInteger(raw.backgroundMax, base.backgroundMax, 1, 8),
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { formatTokens, sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
7
|
+
import { formatDuration, formatTokens, sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
8
|
+
import type { BackgroundRun } from "./background.ts";
|
|
8
9
|
import type { UsageStats } from "./types.ts";
|
|
9
10
|
|
|
10
11
|
/** Dollar cost, keeping four decimals for the sub-cent amounts these runs produce. */
|
|
@@ -124,3 +125,25 @@ export function formatToolCall(
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
}
|
|
128
|
+
|
|
129
|
+
/** The elapsed (or finished) duration of one background run. */
|
|
130
|
+
export function formatBackgroundDuration(run: BackgroundRun, now = Date.now()): string {
|
|
131
|
+
const end = run.finishedAt ?? now;
|
|
132
|
+
return formatDuration(Math.max(0, end - run.startedAt));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The headless `/agents` listing: a count, then one line per run. */
|
|
136
|
+
export function formatAgentsList(runs: readonly BackgroundRun[], now = Date.now()): string {
|
|
137
|
+
if (runs.length === 0) return "No background subagents yet.";
|
|
138
|
+
const running = runs.filter((run) => run.status === "running").length;
|
|
139
|
+
const lines = [
|
|
140
|
+
`${runs.length} background run${runs.length === 1 ? "" : "s"}${running ? ` · ${running} running` : ""}:`,
|
|
141
|
+
];
|
|
142
|
+
for (const run of runs) {
|
|
143
|
+
const usage = run.usage ? formatUsageStats(run.usage) : "";
|
|
144
|
+
lines.push(
|
|
145
|
+
` ${run.id} ${sanitizeLine(run.agent)} · ${sanitizeLine(run.task)} · ${run.status} · ${formatBackgroundDuration(run, now)}${usage ? ` · ${usage}` : ""}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return lines.join("\n");
|
|
149
|
+
}
|