@gavin-hu/my-pi-agent 0.13.1 → 0.15.0
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 +66 -41
- package/README.zh-CN.md +53 -36
- package/extensions/bytefly/README.md +321 -186
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/config.ts +4 -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/config.ts +3 -2
- 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 +33 -10
- package/extensions/bytefly/header/config.ts +22 -0
- package/extensions/bytefly/header/register.ts +2 -1
- package/extensions/bytefly/index.ts +6 -6
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +7 -12
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +22 -2
- package/extensions/bytefly/panel/format.ts +17 -21
- package/extensions/bytefly/panel/settings-runtime.ts +86 -22
- package/extensions/bytefly/panel/settings.ts +279 -46
- 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 +98 -13
- package/extensions/bytefly/panel/tui.ts +176 -166
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +17 -6
- package/extensions/bytefly/separator/config.ts +15 -0
- package/extensions/bytefly/separator/format.ts +60 -6
- package/extensions/bytefly/separator/register.ts +68 -9
- package/extensions/bytefly/separator/types.ts +8 -0
- 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/bytefly/working/config.ts +4 -5
- package/extensions/bytefly/working/format.ts +24 -7
- package/extensions/bytefly/working/register.ts +34 -3
- 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 +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- 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 +3 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +106 -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 +34 -71
- package/extensions/plan/web.ts +13 -40
- 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 +185 -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 +74 -7
- 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/subagent/README.md +118 -8
- 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 +62 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +116 -1
- package/extensions/subagent/orchestrate.ts +16 -3
- package/extensions/subagent/render.ts +25 -0
- package/extensions/subagent/run.ts +113 -49
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/transcripts.ts +202 -0
- package/extensions/subagent/types.ts +23 -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 +16 -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 +3 -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 +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +83 -12
- package/lib/errors.ts +12 -0
- package/lib/format.ts +56 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -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 +91 -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/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 +410 -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 +23 -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/themes/bytefly-dark.json +2 -3
- package/themes/bytefly-light.json +2 -3
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Load with: pi --extension ./extensions/subagent
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { isExtensionEnabled, resolveEffectiveCwd } from "../../lib/env.ts";
|
|
20
20
|
import {
|
|
21
21
|
type AgentConfig,
|
|
@@ -25,8 +25,13 @@ import {
|
|
|
25
25
|
formatAgentList,
|
|
26
26
|
formatAgentNames,
|
|
27
27
|
} from "./agents.ts";
|
|
28
|
+
import { createBackgroundRuntime } from "./background.ts";
|
|
29
|
+
import { registerAgentsCommand } from "./commands.ts";
|
|
30
|
+
import { createSubagentUi } from "./ui.ts";
|
|
31
|
+
import { loadSubagentConfig, type SubagentConfig } from "./config.ts";
|
|
28
32
|
import { runChainMode, runParallelMode, runSingleMode, type ModeContext } from "./orchestrate.ts";
|
|
29
33
|
import { renderSubagentCall, renderSubagentResult } from "./render.ts";
|
|
34
|
+
import { pruneTranscripts, transcriptsDirFor } from "./transcripts.ts";
|
|
30
35
|
import { SUBAGENT_FINISHED_CHANNEL } from "../../lib/events.ts";
|
|
31
36
|
import { finishedEvent } from "./accounting.ts";
|
|
32
37
|
import { runSingleAgent, type RunOptions } from "./run.ts";
|
|
@@ -46,6 +51,7 @@ const DESCRIPTION = [
|
|
|
46
51
|
"Delegate tasks to specialized subagents with isolated context windows.",
|
|
47
52
|
"Modes: single (`agent` + `task`), parallel (`tasks`), chain (sequential, `{previous}` placeholder).",
|
|
48
53
|
"Each runs in its own process, inheriting the current model and thinking level.",
|
|
54
|
+
"A single run can start in the background (`background: true`) and is managed with `/agents`.",
|
|
49
55
|
`Built-in agents: ${AGENT_NAMES}.`,
|
|
50
56
|
].join(" ");
|
|
51
57
|
|
|
@@ -53,6 +59,7 @@ const GUIDELINES = [
|
|
|
53
59
|
"Use `subagent` for broad codebase exploration, parallel research, or isolated implementation work that would bloat the main context.",
|
|
54
60
|
"Prefer `explorer` first when another agent would otherwise re-read many files, then pass its output to `planner` or `worker`.",
|
|
55
61
|
"Run independent tasks with `tasks` (max 8, up to 4 at once); use `chain` only when each step needs the previous step's output.",
|
|
62
|
+
"Use `background: true` only for one long task you will collect from `/agents`.",
|
|
56
63
|
"Project-local agents are repository-controlled and load only when `agentScope` includes them; an untrusted project requires confirmation.",
|
|
57
64
|
];
|
|
58
65
|
|
|
@@ -68,6 +75,20 @@ function requestedAgentNames(args: SubagentArgs): string[] {
|
|
|
68
75
|
export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): void {
|
|
69
76
|
if (!isExtensionEnabled("subagent")) return;
|
|
70
77
|
const runner = deps.run ?? runSingleAgent;
|
|
78
|
+
// Read once per session; a call that arrives before `session_start` reads the
|
|
79
|
+
// same files lazily, so the two paths cannot disagree.
|
|
80
|
+
let settings: SubagentConfig | undefined;
|
|
81
|
+
let activeCtx: ExtensionContext | undefined;
|
|
82
|
+
|
|
83
|
+
const startSession = async (ctx: ExtensionContext): Promise<void> => {
|
|
84
|
+
settings = loadSubagentConfig(ctx.cwd);
|
|
85
|
+
if (settings.transcripts === "off") return;
|
|
86
|
+
try {
|
|
87
|
+
pruneTranscripts(await transcriptsDirFor(pi, ctx.cwd), settings);
|
|
88
|
+
} catch {
|
|
89
|
+
// Retention is housekeeping: an unreadable store must not fail a session.
|
|
90
|
+
}
|
|
91
|
+
};
|
|
71
92
|
|
|
72
93
|
/**
|
|
73
94
|
* Account for each finished child, then hand the result on unchanged.
|
|
@@ -89,6 +110,29 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
89
110
|
return result;
|
|
90
111
|
};
|
|
91
112
|
|
|
113
|
+
const background = createBackgroundRuntime({
|
|
114
|
+
appendEntry: (customType, data) => pi.appendEntry(customType, data),
|
|
115
|
+
onFinish: (run) => ui.notifyFinished(run),
|
|
116
|
+
});
|
|
117
|
+
const ui = createSubagentUi({
|
|
118
|
+
runs: () => background.list(activeCtx?.sessionManager.getBranch() ?? []),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
122
|
+
background.reset();
|
|
123
|
+
activeCtx = ctx;
|
|
124
|
+
ui.attach(ctx);
|
|
125
|
+
background.subscribe(() => ui.paint());
|
|
126
|
+
ui.paint();
|
|
127
|
+
await startSession(ctx);
|
|
128
|
+
});
|
|
129
|
+
pi.on("session_shutdown", () => {
|
|
130
|
+
background.shutdown();
|
|
131
|
+
ui.detach();
|
|
132
|
+
activeCtx = undefined;
|
|
133
|
+
});
|
|
134
|
+
registerAgentsCommand(pi, background);
|
|
135
|
+
|
|
92
136
|
pi.registerTool({
|
|
93
137
|
name: TOOL_NAME,
|
|
94
138
|
label: "Subagent",
|
|
@@ -127,6 +171,13 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
127
171
|
const discovery = discoverAgents(root, agentScope);
|
|
128
172
|
const agents = discovery.agents;
|
|
129
173
|
const requestedTotal = args.chain?.length ?? args.tasks?.length ?? 1;
|
|
174
|
+
const config = settings ?? loadSubagentConfig(ctx.cwd);
|
|
175
|
+
// Resolved against the session cwd, not the active root: a run's record outlives
|
|
176
|
+
// the worktree it ran in, the way the `job` registry does.
|
|
177
|
+
const transcripts =
|
|
178
|
+
config.transcripts === "off"
|
|
179
|
+
? undefined
|
|
180
|
+
: { dir: await transcriptsDirFor(pi, ctx.cwd), mode: config.transcripts };
|
|
130
181
|
const makeDetails = (results: SingleResult[]): SubagentDetails => ({
|
|
131
182
|
mode,
|
|
132
183
|
results,
|
|
@@ -171,6 +222,68 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
171
222
|
}
|
|
172
223
|
}
|
|
173
224
|
|
|
225
|
+
if (args.background) {
|
|
226
|
+
if (mode !== "single") {
|
|
227
|
+
return {
|
|
228
|
+
content: [{ type: "text" as const, text: "`background` supports single mode only." }],
|
|
229
|
+
details: makeDetails([]),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (!agents.some((candidate) => candidate.name === args.agent)) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text" as const,
|
|
237
|
+
text: `Unknown agent: "${args.agent ?? ""}". Available agents: ${formatAgentList(agents)}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
details: makeDetails([]),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const started = background.start(
|
|
244
|
+
{
|
|
245
|
+
agent: args.agent ?? "",
|
|
246
|
+
task: args.task ?? "",
|
|
247
|
+
cwd: args.cwd,
|
|
248
|
+
readOnly: args.readOnly,
|
|
249
|
+
},
|
|
250
|
+
ctx.sessionManager.getBranch(),
|
|
251
|
+
{
|
|
252
|
+
run,
|
|
253
|
+
agents,
|
|
254
|
+
defaults,
|
|
255
|
+
defaultCwd: root,
|
|
256
|
+
timeoutMs: config.timeoutMs,
|
|
257
|
+
transcripts,
|
|
258
|
+
backgroundMax: config.backgroundMax,
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
if (!started.ok) {
|
|
262
|
+
return {
|
|
263
|
+
content: [{ type: "text" as const, text: started.error }],
|
|
264
|
+
details: makeDetails([]),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text" as const,
|
|
271
|
+
text: `Started ${started.run.agent} as ${started.run.id} in the background. Use /agents to inspect, collect, or cancel it.`,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
details: {
|
|
275
|
+
...makeDetails([]),
|
|
276
|
+
background: {
|
|
277
|
+
runId: started.run.id,
|
|
278
|
+
agent: started.run.agent,
|
|
279
|
+
task: started.run.task,
|
|
280
|
+
cwd: started.run.cwd,
|
|
281
|
+
startedAt: started.run.startedAt,
|
|
282
|
+
},
|
|
283
|
+
} satisfies SubagentDetails,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
174
287
|
const context: ModeContext = {
|
|
175
288
|
run,
|
|
176
289
|
args,
|
|
@@ -180,6 +293,8 @@ export default function subagent(pi: ExtensionAPI, deps: SubagentDeps = {}): voi
|
|
|
180
293
|
onUpdate,
|
|
181
294
|
makeDetails,
|
|
182
295
|
agents,
|
|
296
|
+
timeoutMs: config.timeoutMs,
|
|
297
|
+
transcripts,
|
|
183
298
|
};
|
|
184
299
|
|
|
185
300
|
if (mode === "chain" && args.chain) return runChainMode(context);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
8
8
|
import type { AgentConfig } from "./agents.ts";
|
|
9
|
+
import type { TranscriptMode } from "./config.ts";
|
|
9
10
|
import type { RunOptions } from "./run.ts";
|
|
10
11
|
import { MAX_CONCURRENCY, PER_TASK_OUTPUT_CAP, type SubagentArgs } from "./schema.ts";
|
|
11
12
|
import {
|
|
@@ -30,11 +31,15 @@ export interface ModeContext {
|
|
|
30
31
|
onUpdate?: OnUpdateCallback;
|
|
31
32
|
/** Builds the details payload for this mode from the current results. */
|
|
32
33
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
34
|
+
/** Per-run wall-clock limit in ms; `0` or absent disables it. */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** Where and when a finished run writes a transcript. */
|
|
37
|
+
transcripts?: { dir: string; mode: Exclude<TranscriptMode, "off"> };
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/** Sequential chain: each step's `{previous}` is the prior step's final output. */
|
|
36
41
|
export async function runChainMode(ctx: ModeContext): Promise<AgentToolResult<SubagentDetails>> {
|
|
37
|
-
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents } = ctx;
|
|
42
|
+
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents, timeoutMs, transcripts } = ctx;
|
|
38
43
|
const chain = args.chain ?? [];
|
|
39
44
|
const results: SingleResult[] = [];
|
|
40
45
|
let previous = "";
|
|
@@ -62,6 +67,8 @@ export async function runChainMode(ctx: ModeContext): Promise<AgentToolResult<Su
|
|
|
62
67
|
signal,
|
|
63
68
|
onUpdate: chainUpdate,
|
|
64
69
|
makeDetails,
|
|
70
|
+
timeoutMs,
|
|
71
|
+
transcripts,
|
|
65
72
|
});
|
|
66
73
|
results.push(result);
|
|
67
74
|
|
|
@@ -89,7 +96,7 @@ export async function runChainMode(ctx: ModeContext): Promise<AgentToolResult<Su
|
|
|
89
96
|
|
|
90
97
|
/** Independent tasks with bounded concurrency, reporting running/done progress. */
|
|
91
98
|
export async function runParallelMode(ctx: ModeContext): Promise<AgentToolResult<SubagentDetails>> {
|
|
92
|
-
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents } = ctx;
|
|
99
|
+
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents, timeoutMs, transcripts } = ctx;
|
|
93
100
|
const tasks = args.tasks ?? [];
|
|
94
101
|
const allResults: SingleResult[] = tasks.map((task) => createResult(task.agent, task.task));
|
|
95
102
|
|
|
@@ -113,6 +120,10 @@ export async function runParallelMode(ctx: ModeContext): Promise<AgentToolResult
|
|
|
113
120
|
cwd: task.cwd,
|
|
114
121
|
readOnly: args.readOnly,
|
|
115
122
|
signal,
|
|
123
|
+
// Elapsed time is per run, so a deadline is too: one timer around the mode
|
|
124
|
+
// would bound the batch rather than each child.
|
|
125
|
+
timeoutMs,
|
|
126
|
+
transcripts,
|
|
116
127
|
onUpdate: (partial) => {
|
|
117
128
|
const current = partial.details?.results[0];
|
|
118
129
|
if (!current) return;
|
|
@@ -148,7 +159,7 @@ export async function runParallelMode(ctx: ModeContext): Promise<AgentToolResult
|
|
|
148
159
|
|
|
149
160
|
/** One agent + task. */
|
|
150
161
|
export async function runSingleMode(ctx: ModeContext): Promise<AgentToolResult<SubagentDetails>> {
|
|
151
|
-
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents } = ctx;
|
|
162
|
+
const { run, args, defaults, defaultCwd, signal, onUpdate, makeDetails, agents, timeoutMs, transcripts } = ctx;
|
|
152
163
|
const result = await run({
|
|
153
164
|
defaultCwd,
|
|
154
165
|
defaults,
|
|
@@ -160,6 +171,8 @@ export async function runSingleMode(ctx: ModeContext): Promise<AgentToolResult<S
|
|
|
160
171
|
signal,
|
|
161
172
|
onUpdate,
|
|
162
173
|
makeDetails,
|
|
174
|
+
timeoutMs,
|
|
175
|
+
transcripts,
|
|
163
176
|
});
|
|
164
177
|
|
|
165
178
|
if (isFailedResult(result)) {
|
|
@@ -296,6 +296,16 @@ function callText(args: StreamedSubagentArgs, theme: Theme, context?: { cwd?: st
|
|
|
296
296
|
return text;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
/** The transcript this run kept, or why it could not be written. */
|
|
300
|
+
function transcriptLine(result: SingleResult, theme: Theme): string | undefined {
|
|
301
|
+
if (result.artifactError) {
|
|
302
|
+
return paint(theme, "warn", `transcript failed: ${sanitizeLine(result.artifactError)}`);
|
|
303
|
+
}
|
|
304
|
+
if (!result.artifactPath) return undefined;
|
|
305
|
+
const cut = result.artifactTruncated ? paint(theme, "warn", " (truncated)") : "";
|
|
306
|
+
return paint(theme, "faint", `transcript: ${sanitizeLine(result.artifactPath)}${cut}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
299
309
|
/** Append one result's expanded block: header, error, task, output, usage. */
|
|
300
310
|
function addExpandedResult(
|
|
301
311
|
container: Container,
|
|
@@ -353,6 +363,8 @@ function addExpandedResult(
|
|
|
353
363
|
container.addChild(new Spacer(1));
|
|
354
364
|
container.addChild(new Text(paint(theme, "faint", usage), 0, 0));
|
|
355
365
|
}
|
|
366
|
+
const transcript = transcriptLine(result, theme);
|
|
367
|
+
if (transcript) container.addChild(new Text(transcript, 0, 0));
|
|
356
368
|
}
|
|
357
369
|
|
|
358
370
|
/** The collapsed single result: header, a word, body, expand hint, and usage. */
|
|
@@ -476,6 +488,19 @@ export function renderSubagentResult(
|
|
|
476
488
|
): Component {
|
|
477
489
|
const now = options.now ?? Date.now();
|
|
478
490
|
const details = toolDetails<SubagentDetails>(result.details);
|
|
491
|
+
if (details?.background) {
|
|
492
|
+
syncElapsedTimer(false, context);
|
|
493
|
+
const text = reuseText(context?.lastComponent);
|
|
494
|
+
const background = details.background;
|
|
495
|
+
text.setText(
|
|
496
|
+
paint(
|
|
497
|
+
theme,
|
|
498
|
+
"meta",
|
|
499
|
+
`Started ${sanitizeLine(background.agent)} as ${sanitizeLine(background.runId)} in the background. Use /agents to collect or cancel it.`,
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
return text;
|
|
503
|
+
}
|
|
479
504
|
if (!details || details.results.length === 0) {
|
|
480
505
|
syncElapsedTimer(false, context);
|
|
481
506
|
const text = reuseText(context?.lastComponent);
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Running one subagent subprocess.
|
|
3
3
|
*
|
|
4
4
|
* Spawns `pi --mode json`, folds the JSON event stream into a `SingleResult`,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* enforces the run's deadline, optionally records the raw stream, streams partial
|
|
6
|
+
* updates, and cleans up the temporary system-prompt file. The process factory and
|
|
7
|
+
* the transcript store are injectable so tests can drive a scripted child.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
@@ -11,9 +12,13 @@ import * as fs from "node:fs";
|
|
|
11
12
|
import * as os from "node:os";
|
|
12
13
|
import * as path from "node:path";
|
|
13
14
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { formatDuration } from "../../lib/format.ts";
|
|
16
|
+
import { LineSplitter } from "../../lib/lines.ts";
|
|
14
17
|
import { BUILTIN_AGENTS, formatAgentList, type AgentConfig } from "./agents.ts";
|
|
18
|
+
import type { TranscriptMode } from "./config.ts";
|
|
15
19
|
import { buildAgentArgs, getPiInvocation, READ_ONLY_NOTE } from "./invocation.ts";
|
|
16
|
-
import { applyEvent, createResult, parseJsonLine } from "./stream.ts";
|
|
20
|
+
import { applyEvent, createResult, isFailedResult, parseJsonLine } from "./stream.ts";
|
|
21
|
+
import { createTranscript, type Transcript } from "./transcripts.ts";
|
|
17
22
|
import type { DispatchDefaults, OnUpdateCallback, SingleResult, SpawnFn, SubagentDetails } from "./types.ts";
|
|
18
23
|
|
|
19
24
|
export interface RunOptions {
|
|
@@ -38,6 +43,12 @@ export interface RunOptions {
|
|
|
38
43
|
makeDetails: (results: SingleResult[]) => SubagentDetails;
|
|
39
44
|
/** Process factory; defaults to `node:child_process.spawn`. */
|
|
40
45
|
spawn?: SpawnFn;
|
|
46
|
+
/** Per-run wall-clock limit in ms; `0` or absent disables it. */
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
/** Return an aborted result instead of throwing (used by background runs). */
|
|
49
|
+
abortAsResult?: boolean;
|
|
50
|
+
/** Where and when a finished run writes a transcript. */
|
|
51
|
+
transcripts?: { dir: string; mode: Exclude<TranscriptMode, "off"> };
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
|
|
@@ -53,57 +64,83 @@ async function writePromptToTempFile(agentName: string, prompt: string): Promise
|
|
|
53
64
|
interface SpawnOutcome {
|
|
54
65
|
exitCode: number;
|
|
55
66
|
aborted: boolean;
|
|
67
|
+
timedOut: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** How long a killed child has to exit before it is killed again, hard. */
|
|
71
|
+
const KILL_ESCALATION_MS = 5000;
|
|
72
|
+
|
|
73
|
+
/** Why a child is being killed; a timeout fails the run, an abort rejects the call. */
|
|
74
|
+
type KillReason = "abort" | "timeout";
|
|
75
|
+
|
|
76
|
+
interface SpawnContext {
|
|
77
|
+
command: string;
|
|
78
|
+
args: string[];
|
|
79
|
+
cwd: string;
|
|
80
|
+
/** Per-run wall-clock limit in ms; `0` disables it. */
|
|
81
|
+
timeoutMs: number;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
result: SingleResult;
|
|
84
|
+
onEvent: () => void;
|
|
85
|
+
transcript?: Transcript;
|
|
86
|
+
spawn: SpawnFn;
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
/** Spawn the child, parse stdout line-by-line, and resolve when it closes. */
|
|
59
|
-
function spawnAndCollect(
|
|
60
|
-
command
|
|
61
|
-
args: string[],
|
|
62
|
-
cwd: string,
|
|
63
|
-
signal: AbortSignal | undefined,
|
|
64
|
-
result: SingleResult,
|
|
65
|
-
onEvent: () => void,
|
|
66
|
-
spawn: SpawnFn,
|
|
67
|
-
): Promise<SpawnOutcome> {
|
|
90
|
+
function spawnAndCollect(context: SpawnContext): Promise<SpawnOutcome> {
|
|
91
|
+
const { command, args, cwd, timeoutMs, signal, result, onEvent, transcript, spawn } = context;
|
|
68
92
|
return new Promise<SpawnOutcome>((resolve) => {
|
|
69
93
|
const proc = spawn(command, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
70
|
-
|
|
94
|
+
const splitter = new LineSplitter();
|
|
71
95
|
let settled = false;
|
|
72
|
-
let
|
|
96
|
+
let abortRequested = false;
|
|
97
|
+
let timedOut = false;
|
|
73
98
|
|
|
74
|
-
let kill: (() => void) | undefined;
|
|
75
99
|
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
100
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
101
|
+
|
|
102
|
+
const kill = (reason: KillReason) => {
|
|
103
|
+
if (settled) return;
|
|
104
|
+
if (reason === "abort") abortRequested = true;
|
|
105
|
+
else {
|
|
106
|
+
timedOut = true;
|
|
107
|
+
result.timedOut = true;
|
|
108
|
+
}
|
|
109
|
+
proc.kill("SIGTERM");
|
|
110
|
+
killTimer = setTimeout(() => {
|
|
111
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
112
|
+
}, KILL_ESCALATION_MS);
|
|
113
|
+
killTimer.unref();
|
|
114
|
+
};
|
|
115
|
+
const onAbort = () => kill("abort");
|
|
76
116
|
|
|
77
117
|
const finish = (exitCode: number) => {
|
|
78
118
|
if (settled) return;
|
|
79
119
|
settled = true;
|
|
80
|
-
if (
|
|
120
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
81
121
|
if (killTimer) clearTimeout(killTimer);
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
if (
|
|
122
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
123
|
+
for (const line of splitter.flush()) {
|
|
124
|
+
if (line.trim()) processLine(line);
|
|
85
125
|
}
|
|
86
|
-
resolve({ exitCode, aborted });
|
|
126
|
+
resolve({ exitCode, aborted: abortRequested, timedOut });
|
|
87
127
|
};
|
|
88
128
|
|
|
89
129
|
const processLine = (line: string) => {
|
|
130
|
+
transcript?.line(line);
|
|
90
131
|
const event = parseJsonLine(line);
|
|
91
132
|
if (event && applyEvent(result, event)) onEvent();
|
|
92
133
|
};
|
|
93
134
|
|
|
94
135
|
proc.stdout?.on("data", (chunk) => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let newline = buffer.indexOf("\n");
|
|
98
|
-
while (newline !== -1) {
|
|
99
|
-
processLine(buffer.slice(0, newline));
|
|
100
|
-
buffer = buffer.slice(newline + 1);
|
|
101
|
-
newline = buffer.indexOf("\n");
|
|
102
|
-
}
|
|
136
|
+
// The splitter holds the final partial line for the close handler.
|
|
137
|
+
for (const line of splitter.push(chunk)) processLine(line);
|
|
103
138
|
});
|
|
104
139
|
|
|
105
140
|
proc.stderr?.on("data", (chunk) => {
|
|
106
|
-
|
|
141
|
+
const text = chunk.toString();
|
|
142
|
+
result.stderr += text;
|
|
143
|
+
transcript?.stderr(text);
|
|
107
144
|
});
|
|
108
145
|
|
|
109
146
|
proc.on("close", (code, signal) => {
|
|
@@ -122,16 +159,12 @@ function spawnAndCollect(
|
|
|
122
159
|
});
|
|
123
160
|
|
|
124
161
|
if (signal) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
killTimer.unref();
|
|
132
|
-
};
|
|
133
|
-
if (signal.aborted) kill();
|
|
134
|
-
else signal.addEventListener("abort", kill, { once: true });
|
|
162
|
+
if (signal.aborted) onAbort();
|
|
163
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
}
|
|
165
|
+
if (timeoutMs > 0) {
|
|
166
|
+
timeoutTimer = setTimeout(() => kill("timeout"), timeoutMs);
|
|
167
|
+
timeoutTimer.unref();
|
|
135
168
|
}
|
|
136
169
|
});
|
|
137
170
|
}
|
|
@@ -163,6 +196,12 @@ export async function runSingleAgent(options: RunOptions): Promise<SingleResult>
|
|
|
163
196
|
|
|
164
197
|
const emit = () => options.onUpdate?.({ content: [], details: options.makeDetails([result]) });
|
|
165
198
|
|
|
199
|
+
const timeoutMs = options.timeoutMs ?? 0;
|
|
200
|
+
if (timeoutMs > 0) result.timeoutMs = timeoutMs;
|
|
201
|
+
const transcripts = options.transcripts;
|
|
202
|
+
const transcript = transcripts ? createTranscript() : undefined;
|
|
203
|
+
let runCwd = options.defaultCwd;
|
|
204
|
+
|
|
166
205
|
let tempDir: string | null = null;
|
|
167
206
|
let tempPath: string | null = null;
|
|
168
207
|
|
|
@@ -178,22 +217,47 @@ export async function runSingleAgent(options: RunOptions): Promise<SingleResult>
|
|
|
178
217
|
|
|
179
218
|
const args = buildAgentArgs(agent, options.task, options.defaults, tempPath, options.readOnly === true);
|
|
180
219
|
const invocation = getPiInvocation(args);
|
|
181
|
-
|
|
182
|
-
const outcome = await spawnAndCollect(
|
|
183
|
-
invocation.command,
|
|
184
|
-
invocation.args,
|
|
185
|
-
runCwd,
|
|
186
|
-
|
|
220
|
+
runCwd = options.cwd ? path.resolve(options.defaultCwd, options.cwd) : options.defaultCwd;
|
|
221
|
+
const outcome = await spawnAndCollect({
|
|
222
|
+
command: invocation.command,
|
|
223
|
+
args: invocation.args,
|
|
224
|
+
cwd: runCwd,
|
|
225
|
+
timeoutMs,
|
|
226
|
+
signal: options.signal,
|
|
187
227
|
result,
|
|
188
|
-
emit,
|
|
228
|
+
onEvent: emit,
|
|
229
|
+
transcript,
|
|
189
230
|
spawn,
|
|
190
|
-
);
|
|
231
|
+
});
|
|
191
232
|
|
|
192
|
-
result.exitCode = outcome.exitCode;
|
|
233
|
+
result.exitCode = outcome.timedOut || outcome.aborted ? 1 : outcome.exitCode;
|
|
193
234
|
result.finishedAt = Date.now();
|
|
194
|
-
|
|
235
|
+
// A deadline fails the run rather than rejecting it: a chain then stops at
|
|
236
|
+
// this step and reports why, instead of discarding the steps already done.
|
|
237
|
+
if (outcome.timedOut) result.errorMessage = `Timed out after ${formatDuration(timeoutMs)}`;
|
|
238
|
+
if (outcome.aborted) {
|
|
239
|
+
if (options.abortAsResult) {
|
|
240
|
+
result.stopReason = "aborted";
|
|
241
|
+
result.errorMessage = "Aborted";
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
throw new Error("Subagent was aborted");
|
|
245
|
+
}
|
|
195
246
|
return result;
|
|
196
247
|
} finally {
|
|
248
|
+
// Flushed here, not after the call: the abort path throws past every caller.
|
|
249
|
+
if (transcript && transcripts && (transcripts.mode === "all" || isFailedResult(result))) {
|
|
250
|
+
const written = transcript.write(transcripts.dir, result, {
|
|
251
|
+
cwd: runCwd,
|
|
252
|
+
readOnly: options.readOnly === true,
|
|
253
|
+
});
|
|
254
|
+
if (written.path) {
|
|
255
|
+
result.artifactPath = written.path;
|
|
256
|
+
result.artifactBytes = written.bytes;
|
|
257
|
+
if (written.truncated) result.artifactTruncated = true;
|
|
258
|
+
}
|
|
259
|
+
if (written.error) result.artifactError = written.error;
|
|
260
|
+
}
|
|
197
261
|
if (tempPath) {
|
|
198
262
|
try {
|
|
199
263
|
fs.unlinkSync(tempPath);
|
|
@@ -66,6 +66,11 @@ export const SubagentParams = Type.Object({
|
|
|
66
66
|
description: "Force a read-only tool set (readers only). Plan mode sets this.",
|
|
67
67
|
}),
|
|
68
68
|
),
|
|
69
|
+
background: Type.Optional(
|
|
70
|
+
Type.Boolean({
|
|
71
|
+
description: "Start a single run and return immediately; inspect and collect it with /agents.",
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
69
74
|
});
|
|
70
75
|
|
|
71
76
|
export type SubagentArgs = Static<typeof SubagentParams>;
|
|
@@ -85,6 +90,13 @@ export function resolveMode(params: SubagentArgs): ModeResolution {
|
|
|
85
90
|
const hasTasks = taskCount > 0;
|
|
86
91
|
const hasSingle = Boolean(params.agent && params.task);
|
|
87
92
|
|
|
93
|
+
if (params.background === true && (hasChain || hasTasks)) {
|
|
94
|
+
return { error: "`background` supports single mode only; provide `agent` and `task`." };
|
|
95
|
+
}
|
|
96
|
+
if (params.background === true && !hasSingle) {
|
|
97
|
+
return { error: "`background` requires `agent` and `task`." };
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
if (hasChain && hasTasks) return { error: "Provide either `chain` or `tasks`, not both." };
|
|
89
101
|
if (hasChain && hasSingle) return { error: "Provide either `chain` or `agent`+`task`, not both." };
|
|
90
102
|
if (hasTasks && hasSingle) return { error: "Provide either `tasks` or `agent`+`task`, not both." };
|