pi-subagents 0.50.0 → 0.51.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/CHANGELOG.md +62 -0
- package/agents/gpt-pro.md +17 -0
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +35 -2
- package/docs/configuration.md +30 -12
- package/docs/extension-api.md +42 -1
- package/docs/observability.md +4 -4
- package/docs/tool-reference.md +38 -39
- package/docs/workflows.md +169 -3
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +6 -4
- package/skills/pi-subagents/references/execution-controls.md +18 -15
- package/skills/pi-subagents/references/management-authoring-rpc.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agents.ts +125 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/preflight.ts +31 -6
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +3 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +165 -88
- package/src/extension/public-execution.ts +28 -1
- package/src/extension/schemas.ts +12 -35
- package/src/extension/tool-description.ts +35 -24
- package/src/inspectors/herdr/actions.ts +2 -2
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +2 -2
- package/src/intercom/native-supervisor-channel.ts +30 -9
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +31 -8
- package/src/runs/background/async-execution.ts +49 -31
- package/src/runs/background/async-job-tracker.ts +20 -4
- package/src/runs/background/async-resume.ts +28 -13
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status.ts +39 -53
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +158 -90
- package/src/runs/background/result-watcher.ts +72 -20
- package/src/runs/background/retained-children.ts +13 -3
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +11 -9
- package/src/runs/background/run-status.ts +27 -18
- package/src/runs/background/scheduled-runs.ts +22 -4
- package/src/runs/background/stale-run-reconciler.ts +6 -3
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +250 -138
- package/src/runs/background/subagent-wait.ts +7 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +21 -4
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +2 -0
- package/src/runs/foreground/subagent-executor.ts +460 -1536
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +22 -5
- package/src/runs/shared/orca-progress-tabs.ts +84 -22
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +4 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +26 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -6
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +60 -26
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +11 -3
- package/src/tui/fleet.ts +63 -15
- package/src/workflows/chat-progress.ts +1 -1
- package/src/workflows/scripted-workflow.ts +207 -65
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1581
|
@@ -6,10 +6,23 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
8
|
|
|
9
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child or workflowScript for orchestration. For multi-step or parallel work, make exactly one top-level subagent call with workflowScript and async:true; launch children only inside that script and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and pass completed child results via .output. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
10
|
+
|
|
11
|
+
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
12
|
+
|
|
13
|
+
export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
14
|
+
"Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
|
|
15
|
+
"Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
|
|
16
|
+
"workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
|
|
17
|
+
"For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
|
|
18
|
+
"Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
|
|
19
|
+
"Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
|
|
20
|
+
];
|
|
21
|
+
|
|
9
22
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
23
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
24
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
12
|
-
• Async/background runs are the default. Use async:false only when
|
|
25
|
+
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
26
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
27
|
• Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
|
|
15
28
|
• Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
|
|
@@ -20,17 +33,15 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
|
|
|
20
33
|
EXECUTION:
|
|
21
34
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
22
35
|
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
|
|
23
|
-
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children;
|
|
36
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
24
37
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
25
38
|
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
26
|
-
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
39
|
+
• Optional context is "fresh" or "fork". Explicit context wins. When omitted, config defaultSubagentContext wins over agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
27
40
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
28
41
|
|
|
29
42
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
30
43
|
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
31
44
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
32
|
-
• { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
|
|
33
|
-
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
34
45
|
• Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" } or { every:"6h", workflowScript:"..." }. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
35
46
|
|
|
36
47
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
@@ -40,17 +51,16 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
|
|
|
40
51
|
EXECUTE:
|
|
41
52
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
42
53
|
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
43
|
-
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work.
|
|
54
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
44
55
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
45
|
-
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
56
|
+
• context can be fresh or fork. Explicit context wins; omitted context follows defaultSubagentContext before agent defaultContext. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
46
57
|
|
|
47
58
|
MANAGE / CONTROL:
|
|
48
59
|
• Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
|
|
49
|
-
• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
|
|
50
60
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
51
61
|
|
|
52
62
|
ASYNC / SAFETY:
|
|
53
|
-
• Omitted async
|
|
63
|
+
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
54
64
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
55
65
|
• Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
|
|
56
66
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
@@ -70,6 +80,19 @@ export interface ToolDescriptionOptions {
|
|
|
70
80
|
warn?: (message: string) => void;
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
export interface SubagentToolPromptMetadata {
|
|
84
|
+
promptSnippet?: string;
|
|
85
|
+
promptGuidelines?: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildSubagentToolPromptMetadata(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}): SubagentToolPromptMetadata {
|
|
89
|
+
if (config.toolDescriptionMode !== undefined) return {};
|
|
90
|
+
return {
|
|
91
|
+
promptSnippet: SUBAGENT_TOOL_PROMPT_SNIPPET,
|
|
92
|
+
promptGuidelines: SUBAGENT_TOOL_PROMPT_GUIDELINES,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
export function resolveToolDescriptionMode(config: Pick<ExtensionConfig, "toolDescriptionMode">, options?: ToolDescriptionOptions): ToolDescriptionMode {
|
|
74
97
|
const mode = config.toolDescriptionMode;
|
|
75
98
|
if (mode === undefined) return "full";
|
|
@@ -157,20 +180,8 @@ function withMandatorySafetyGuidance(description: string): string {
|
|
|
157
180
|
: SUBAGENT_SAFETY_GUIDANCE;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.",
|
|
163
|
-
"• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.",
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
function withoutLegacyChainControlGuidance(description: string): string {
|
|
167
|
-
return description
|
|
168
|
-
.split("\n")
|
|
169
|
-
.filter((line) => !LEGACY_CHAIN_CONTROL_GUIDANCE_LINES.has(line.trim()))
|
|
170
|
-
.join("\n");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode" | "legacyChainControls"> = {}, options?: ToolDescriptionOptions): string {
|
|
183
|
+
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}, options?: ToolDescriptionOptions): string {
|
|
184
|
+
if (config.toolDescriptionMode === undefined) return DEFAULT_SUBAGENT_TOOL_DESCRIPTION;
|
|
174
185
|
const mode = resolveToolDescriptionMode(config, options);
|
|
175
186
|
let description: string;
|
|
176
187
|
if (mode === "compact") description = COMPACT_SUBAGENT_TOOL_DESCRIPTION;
|
|
@@ -182,5 +193,5 @@ export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "tool
|
|
|
182
193
|
description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
183
194
|
}
|
|
184
195
|
} else description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
185
|
-
return
|
|
196
|
+
return description;
|
|
186
197
|
}
|
|
@@ -193,7 +193,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
193
193
|
if (live.ok) return result(`Herdr inspector pane ${existing.paneId} is already open for async run ${target.runId}.${params.focus ? " Herdr cannot refocus an arbitrary raw pane id; select it in the Herdr UI." : ""}`);
|
|
194
194
|
}
|
|
195
195
|
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", status.cwd ?? deps.cwd];
|
|
196
|
-
|
|
196
|
+
splitArgs.push(params.focus === true ? "--focus" : "--no-focus");
|
|
197
197
|
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: deps.signal });
|
|
198
198
|
if (split.ok === false) return result(formatHerdrError(split.error), true);
|
|
199
199
|
const paneId = extractPaneId(split.data);
|
|
@@ -225,7 +225,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
225
225
|
...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),
|
|
226
226
|
paneId,
|
|
227
227
|
openedAt: now,
|
|
228
|
-
...(params.focus
|
|
228
|
+
...(params.focus === true ? { lastFocusedAt: now } : {}),
|
|
229
229
|
herdrVersion: detected.data.versionText,
|
|
230
230
|
command,
|
|
231
231
|
};
|
|
@@ -6,6 +6,7 @@ import { parseMissionRecord } from "../../missions/store.ts";
|
|
|
6
6
|
import type { MissionRecord } from "../../missions/types.ts";
|
|
7
7
|
import { requestAsyncSteer, requestAsyncStop } from "../../runs/background/control-channel.ts";
|
|
8
8
|
import { formatAsyncRunTranscript } from "../../runs/background/fleet-view.ts";
|
|
9
|
+
import { steeringReceipt } from "../../runs/background/steering.ts";
|
|
9
10
|
import type { AsyncStatus } from "../../shared/types.ts";
|
|
10
11
|
import { readStatus } from "../../shared/utils.ts";
|
|
11
12
|
|
|
@@ -112,7 +113,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
|
|
|
112
113
|
...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
|
|
113
114
|
source: "herdr-inspector",
|
|
114
115
|
});
|
|
115
|
-
return `Steering queued for run ${options.runId}
|
|
116
|
+
return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
|
|
116
117
|
}
|
|
117
118
|
if (command.startsWith("reply ")) throw new Error("Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.");
|
|
118
119
|
throw new Error("Unknown control. Use steer <message>, stop, or status.");
|
|
@@ -411,7 +411,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", projectRoot];
|
|
414
|
-
|
|
414
|
+
splitArgs.push(input.focus === true ? "--focus" : "--no-focus");
|
|
415
415
|
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: input.signal });
|
|
416
416
|
if (!split.ok) return projectPaneError(split.error.code, split.error.message, { projectRoot, details: split.error.details });
|
|
417
417
|
const paneId = extractPaneId(split.data);
|
|
@@ -430,7 +430,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
430
430
|
projectRoot,
|
|
431
431
|
paneId,
|
|
432
432
|
openedAt: now,
|
|
433
|
-
...(input.focus
|
|
433
|
+
...(input.focus === true ? { lastFocusedAt: now } : {}),
|
|
434
434
|
herdrVersion: detected.data.versionText,
|
|
435
435
|
command,
|
|
436
436
|
...(startupMessage ? { startupMessage } : {}),
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../runs/shared/pi-args.ts";
|
|
15
15
|
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
16
16
|
import { writeAtomicJson } from "../shared/atomic-json.ts";
|
|
17
|
+
import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
|
|
17
18
|
|
|
18
19
|
const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
|
|
19
20
|
const REQUESTS_DIR = "requests";
|
|
@@ -75,6 +76,7 @@ type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string
|
|
|
75
76
|
interface NativeSupervisorChannelDeps {
|
|
76
77
|
platform?: NodeJS.Platform;
|
|
77
78
|
watch?: SupervisorWatch;
|
|
79
|
+
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval" | "setImmediate" | "clearImmediate">;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const ContactSupervisorParamsSchema = Type.Object({
|
|
@@ -612,8 +614,9 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
|
|
|
612
614
|
};
|
|
613
615
|
}
|
|
614
616
|
|
|
615
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
617
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; activateTransport: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
616
618
|
const watch = deps.watch ?? fs.watch;
|
|
619
|
+
const timers = deps.timers ?? globalThis;
|
|
617
620
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
618
621
|
const seenFiles = new Set<string>();
|
|
619
622
|
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
@@ -623,6 +626,13 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
623
626
|
let deferredWatcherRefresh: ReturnType<typeof setImmediate> | undefined;
|
|
624
627
|
let started = false;
|
|
625
628
|
let lastStaleCleanupAt = 0;
|
|
629
|
+
const platform = deps.platform ?? process.platform;
|
|
630
|
+
const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
631
|
+
const hasTransportDemand = () => {
|
|
632
|
+
if (pending.size > 0) return true;
|
|
633
|
+
if (state.foregroundControls.size > 0) return true;
|
|
634
|
+
return [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
|
|
635
|
+
};
|
|
626
636
|
|
|
627
637
|
const registerParentTools = (): void => {
|
|
628
638
|
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
|
|
@@ -689,12 +699,18 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
689
699
|
|
|
690
700
|
const startPolling = (): void => {
|
|
691
701
|
if (poller) return;
|
|
692
|
-
poller = setInterval(
|
|
702
|
+
poller = timers.setInterval(() => {
|
|
703
|
+
poll();
|
|
704
|
+
if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
|
|
705
|
+
if (poller) timers.clearInterval(poller);
|
|
706
|
+
poller = undefined;
|
|
707
|
+
}
|
|
708
|
+
}, CHANNEL_POLL_MS);
|
|
693
709
|
poller.unref?.();
|
|
694
710
|
};
|
|
695
711
|
const startSafetyPolling = (): void => {
|
|
696
712
|
if (safetyPoller) return;
|
|
697
|
-
safetyPoller = setInterval(() => {
|
|
713
|
+
safetyPoller = timers.setInterval(() => {
|
|
698
714
|
watchExistingRequestDirs();
|
|
699
715
|
poll();
|
|
700
716
|
}, CHANNEL_SAFETY_POLL_MS);
|
|
@@ -730,7 +746,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
730
746
|
};
|
|
731
747
|
const scheduleWatcherRefresh = (): void => {
|
|
732
748
|
if (deferredWatcherRefresh) return;
|
|
733
|
-
deferredWatcherRefresh = setImmediate(() => {
|
|
749
|
+
deferredWatcherRefresh = timers.setImmediate(() => {
|
|
734
750
|
deferredWatcherRefresh = undefined;
|
|
735
751
|
if (!started) return;
|
|
736
752
|
watchExistingRequestDirs();
|
|
@@ -740,6 +756,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
740
756
|
};
|
|
741
757
|
|
|
742
758
|
return {
|
|
759
|
+
activateTransport: () => {
|
|
760
|
+
if (!started) return;
|
|
761
|
+
poll();
|
|
762
|
+
if (!useNativeWatcher() && hasTransportDemand()) startPolling();
|
|
763
|
+
},
|
|
743
764
|
start: () => {
|
|
744
765
|
if (started) return;
|
|
745
766
|
started = true;
|
|
@@ -747,8 +768,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
747
768
|
poll();
|
|
748
769
|
try {
|
|
749
770
|
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
750
|
-
if ((
|
|
751
|
-
startPolling();
|
|
771
|
+
if (!useNativeWatcher()) {
|
|
772
|
+
if (platform === "win32") startPolling();
|
|
752
773
|
return;
|
|
753
774
|
}
|
|
754
775
|
watchExistingRequestDirs();
|
|
@@ -776,11 +797,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
776
797
|
try { watcher.close(); } catch {}
|
|
777
798
|
}
|
|
778
799
|
requestWatchers.clear();
|
|
779
|
-
if (poller) clearInterval(poller);
|
|
800
|
+
if (poller) timers.clearInterval(poller);
|
|
780
801
|
poller = undefined;
|
|
781
|
-
if (safetyPoller) clearInterval(safetyPoller);
|
|
802
|
+
if (safetyPoller) timers.clearInterval(safetyPoller);
|
|
782
803
|
safetyPoller = undefined;
|
|
783
|
-
if (deferredWatcherRefresh) clearImmediate(deferredWatcherRefresh);
|
|
804
|
+
if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
|
|
784
805
|
deferredWatcherRefresh = undefined;
|
|
785
806
|
pending.clear();
|
|
786
807
|
seenFiles.clear();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
5
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
5
6
|
import { PROMPT_REDACTED } from "../shared/utils.ts";
|
|
@@ -294,13 +295,17 @@ export function syncMissionFromAsyncCompletion(value: unknown): MissionRecord |
|
|
|
294
295
|
current = readMission(binding.location, binding.missionId);
|
|
295
296
|
} catch (error) {
|
|
296
297
|
if (!(error instanceof MissionNotFoundError)) throw error;
|
|
298
|
+
const reason = "mission-record-missing";
|
|
299
|
+
const markerId = createHash("sha256").update(JSON.stringify([runId, binding.missionId, reason])).digest("hex");
|
|
300
|
+
const markerPath = path.join(event.asyncDir, `.mission-sync-skipped-${markerId}.json`);
|
|
297
301
|
try {
|
|
302
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({ runId, missionId: binding.missionId, reason })}\n`, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
298
303
|
fs.appendFileSync(path.join(event.asyncDir, "events.jsonl"), `${JSON.stringify({
|
|
299
304
|
type: "subagent.mission.sync.skipped",
|
|
300
305
|
ts: Date.now(),
|
|
301
306
|
runId,
|
|
302
307
|
missionId: binding.missionId,
|
|
303
|
-
reason
|
|
308
|
+
reason,
|
|
304
309
|
missionPath: missionRecordPath(binding.location, binding.missionId),
|
|
305
310
|
})}\n`, "utf-8");
|
|
306
311
|
} catch {
|
package/src/missions/store.ts
CHANGED
|
@@ -77,11 +77,6 @@ function nonNegativeTokenCount(value: unknown, label: string): number {
|
|
|
77
77
|
return value as number;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function parseStoredGoal(value: unknown, label: string): { goal?: MissionGoal; legacyObjective?: string } {
|
|
81
|
-
if (typeof value === "string") return { legacyObjective: requiredString(value, label).trim() };
|
|
82
|
-
return { goal: parseGoal(value, label) };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
80
|
function parseGoal(value: unknown, label: string): MissionGoal {
|
|
86
81
|
const input = asObject(value, label);
|
|
87
82
|
if (input.status !== "active" && input.status !== "paused" && input.status !== "budget-exhausted") throw new Error(`${label}.status is invalid`);
|
|
@@ -223,18 +218,18 @@ export function parseMissionRecord(value: unknown, source = "mission record"): M
|
|
|
223
218
|
const decisions = input.decisions as unknown[];
|
|
224
219
|
const artifacts = input.artifacts as unknown[];
|
|
225
220
|
const receipts = (input.receipts ?? []) as unknown[];
|
|
226
|
-
const
|
|
221
|
+
const goal = input.goal !== undefined ? parseGoal(input.goal, `${source}.goal`) : undefined;
|
|
227
222
|
const budget = input.budget !== undefined ? parseBudget(input.budget, `${source}.budget`) : undefined;
|
|
228
223
|
const usage = input.usage !== undefined ? parseUsage(input.usage, `${source}.usage`) : undefined;
|
|
229
|
-
const objective = optionalString(input.objective, `${source}.objective`)?.trim()
|
|
224
|
+
const objective = optionalString(input.objective, `${source}.objective`)?.trim();
|
|
230
225
|
if (!objective) throw new Error(`${source}.objective must be a non-empty string`);
|
|
231
|
-
if (
|
|
226
|
+
if (goal && !budget) throw new Error(`${source}.budget is required for a goal mission`);
|
|
232
227
|
return {
|
|
233
228
|
schemaVersion: 1,
|
|
234
229
|
id: validateMissionId(input.id, `${source}.id`),
|
|
235
230
|
title: requiredString(input.title, `${source}.title`),
|
|
236
231
|
objective,
|
|
237
|
-
...(
|
|
232
|
+
...(goal ? { goal } : {}),
|
|
238
233
|
...(budget ? { budget } : {}),
|
|
239
234
|
...(usage ? { usage } : {}),
|
|
240
235
|
status: missionStatus(input.status, `${source}.status`),
|
package/src/profiles/profiles.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { BUILTIN_AGENT_NAMES } from "../agents/agents.ts";
|
|
6
|
+
import { getPiSpawnCommand } from "../runs/shared/pi-spawn.ts";
|
|
6
7
|
import { findModelInfo, getSupportedThinkingLevels, splitKnownThinkingSuffix, toModelInfo } from "../shared/model-info.ts";
|
|
7
8
|
import { getAgentDir } from "../shared/utils.ts";
|
|
8
9
|
|
|
@@ -340,7 +341,8 @@ async function probeModel(
|
|
|
340
341
|
if (typeof pi.exec !== "function") {
|
|
341
342
|
return { status: "skipped", message: "pi.exec is unavailable in this runtime." };
|
|
342
343
|
}
|
|
343
|
-
const
|
|
344
|
+
const spawnSpec = getPiSpawnCommand(["-p", "--model", fullId, "--no-tools", 'Reply with exactly "OK".']);
|
|
345
|
+
const result = await pi.exec(spawnSpec.command, spawnSpec.args, {
|
|
344
346
|
cwd: os.tmpdir(),
|
|
345
347
|
timeout: 45_000,
|
|
346
348
|
} as Record<string, unknown>);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AsyncStatus } from "../../shared/types.ts";
|
|
4
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
5
|
+
import { encodeIndexSegment } from "./index-segment.ts";
|
|
6
|
+
import { updateTerminalRunIndex } from "./terminal-run-index.ts";
|
|
7
|
+
import { isStorageCapacityError } from "../../shared/file-system-retry.ts";
|
|
4
8
|
|
|
5
9
|
export const ACTIVE_RUN_INDEX_DIR = ".active-runs";
|
|
6
10
|
export const DEFAULT_STALE_TERMINAL_ACTIVE_MARKER_MS = 24 * 60 * 60 * 1000;
|
|
@@ -12,7 +16,7 @@ function indexDir(asyncDirRoot: string): string {
|
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
function toolCallIndexDir(asyncDirRoot: string, toolCallId: string): string {
|
|
15
|
-
return path.join(indexDir(asyncDirRoot), TOOL_CALL_INDEX_DIR,
|
|
19
|
+
return path.join(indexDir(asyncDirRoot), TOOL_CALL_INDEX_DIR, encodeIndexSegment(toolCallId));
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
function toolCallIndexPath(asyncDir: string, toolCallId: string): string {
|
|
@@ -45,8 +49,11 @@ function releaseToolCallAliases(asyncDir: string): void {
|
|
|
45
49
|
try {
|
|
46
50
|
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
47
51
|
} catch (error) {
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
53
|
+
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
54
|
+
console.error(`Failed to inspect async active-run tool-call index root '${root}':`, error);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
50
57
|
}
|
|
51
58
|
for (const entry of entries) {
|
|
52
59
|
if (!entry.isDirectory()) continue;
|
|
@@ -69,19 +76,34 @@ export function releaseActiveRunIndex(asyncDir: string): void {
|
|
|
69
76
|
releaseToolCallAliases(asyncDir);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string): void {
|
|
79
|
+
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean } = {}): void {
|
|
73
80
|
const marker = markerPath(asyncDir);
|
|
74
81
|
if (isActiveAsyncState(state)) {
|
|
75
82
|
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
76
83
|
fs.writeFileSync(marker, "", { flag: "a" });
|
|
77
84
|
if (toolCallId) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
try {
|
|
86
|
+
const toolCallMarker = toolCallIndexPath(asyncDir, toolCallId);
|
|
87
|
+
fs.mkdirSync(path.dirname(toolCallMarker), { recursive: true });
|
|
88
|
+
fs.writeFileSync(toolCallMarker, "", { flag: "a" });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Tool-call aliases are optional. The authoritative active-run marker
|
|
91
|
+
// must keep the launch discoverable even when alias indexing fails.
|
|
92
|
+
console.error(`Failed to write async active-run tool-call index for '${asyncDir}':`, error);
|
|
93
|
+
}
|
|
81
94
|
}
|
|
82
95
|
return;
|
|
83
96
|
}
|
|
84
97
|
releaseActiveRunIndex(asyncDir);
|
|
98
|
+
const status = readStatus(asyncDir);
|
|
99
|
+
if (status && status.state === state) {
|
|
100
|
+
try {
|
|
101
|
+
updateTerminalRunIndex(asyncDir, status);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (options.retryCapacityErrors && isStorageCapacityError(error)) throw error;
|
|
104
|
+
console.error(`Failed to write async terminal-run index for '${asyncDir}':`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
export function activeRunMarkerAgeMs(asyncDir: string, now = Date.now()): number | undefined {
|
|
@@ -110,7 +132,8 @@ export function readActiveRunToolCallIndex(asyncDirRoot: string, toolCallId: str
|
|
|
110
132
|
.filter((entry) => entry.isFile())
|
|
111
133
|
.map((entry) => entry.name);
|
|
112
134
|
} catch (error) {
|
|
113
|
-
|
|
135
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
136
|
+
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
114
137
|
throw error;
|
|
115
138
|
}
|
|
116
139
|
}
|