pi-subagents 0.40.0 → 0.41.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 +76 -0
- package/README.md +246 -525
- package/agents/oracle.md +1 -0
- package/package.json +12 -4
- package/prompts/parallel-context-build.md +1 -1
- package/prompts/parallel-handoff-plan.md +1 -1
- package/prompts/review-loop.md +1 -1
- package/skills/pi-subagents/SKILL.md +6 -6
- package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
- package/skills/pi-subagents/references/execution-controls.md +98 -97
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
- package/src/agents/agent-management.ts +155 -65
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +154 -71
- package/src/agents/chain-serializer.ts +10 -7
- package/src/agents/frontmatter.ts +5 -3
- package/src/agents/identity.ts +1 -1
- package/src/agents/proactive-skills.ts +13 -10
- package/src/agents/skills.ts +23 -6
- package/src/api/control-channel.ts +4 -0
- package/src/api/delegation.ts +26 -194
- package/src/api/external-runs.ts +129 -0
- package/src/api/intercom-bridge.ts +3 -0
- package/src/api/pi-args.ts +5 -0
- package/src/api/preflight.ts +3 -3
- package/src/api/shared-types.ts +19 -0
- package/src/extension/config.ts +10 -0
- package/src/extension/control-notices.ts +5 -39
- package/src/extension/doctor.ts +10 -9
- package/src/extension/fanout-child.ts +7 -4
- package/src/extension/index.ts +232 -68
- package/src/extension/rpc.ts +18 -12
- package/src/extension/schemas.ts +48 -37
- package/src/extension/tool-description.ts +36 -86
- package/src/inspectors/herdr/actions.ts +229 -0
- package/src/inspectors/herdr/client.ts +130 -0
- package/src/inspectors/herdr/inspector-runner.ts +141 -0
- package/src/inspectors/herdr/project-panes.ts +154 -0
- package/src/integrations/herdr-status.ts +330 -0
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/result-intercom.ts +5 -1
- package/src/missions/actions.ts +372 -0
- package/src/missions/lifecycle.ts +314 -0
- package/src/missions/store.ts +442 -0
- package/src/missions/types.ts +135 -0
- package/src/policy/authority.ts +46 -0
- package/src/profiles/profiles.ts +29 -3
- package/src/runs/background/async-execution.ts +98 -49
- package/src/runs/background/async-job-tracker.ts +10 -2
- package/src/runs/background/async-resume.ts +6 -6
- package/src/runs/background/async-status.ts +29 -1
- package/src/runs/background/auto-drain.ts +3 -3
- package/src/runs/background/chain-append.ts +3 -2
- package/src/runs/background/control-channel.ts +9 -7
- package/src/runs/background/fleet-view.ts +3 -4
- package/src/runs/background/notify.ts +2 -1
- package/src/runs/background/process-terminal.ts +5 -5
- package/src/runs/background/result-watcher.ts +13 -5
- package/src/runs/background/run-id-resolver.ts +3 -3
- package/src/runs/background/run-status.ts +35 -8
- package/src/runs/background/scheduled-runs.ts +602 -375
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +608 -445
- package/src/runs/background/subagent-wait.ts +50 -9
- package/src/runs/background/wait-subscriptions.ts +253 -0
- package/src/runs/background/wait-tool.ts +12 -4
- package/src/runs/foreground/async-steering-action.ts +3 -3
- package/src/runs/foreground/chain-clarify.ts +8 -4
- package/src/runs/foreground/chain-execution.ts +56 -30
- package/src/runs/foreground/execution.ts +15 -2
- package/src/runs/foreground/subagent-executor.ts +1023 -273
- package/src/runs/shared/acceptance.ts +28 -6
- package/src/runs/shared/child-protocol.ts +302 -22
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/external-cli-runner.ts +130 -0
- package/src/runs/shared/long-running-guard.ts +42 -1
- package/src/runs/shared/nested-events.ts +59 -5
- package/src/runs/shared/nested-render.ts +9 -4
- package/src/runs/shared/parallel-handoff.ts +86 -2
- package/src/runs/shared/parallel-utils.ts +11 -2
- package/src/runs/shared/permissions.ts +95 -0
- package/src/runs/shared/pi-args.ts +11 -1
- package/src/runs/shared/pi-spawn.ts +11 -1
- package/src/runs/shared/run-history.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
- package/src/runs/shared/subagent-startup-retry.ts +5 -2
- package/src/runs/shared/turn-budget.ts +6 -6
- package/src/runs/shared/worktree.ts +122 -12
- package/src/shared/accessible-dir.ts +29 -7
- package/src/shared/artifacts.ts +18 -1
- package/src/shared/fork-context.ts +3 -2
- package/src/shared/launch-contract.ts +1 -0
- package/src/shared/settings.ts +10 -0
- package/src/shared/types.ts +156 -14
- package/src/shared/utils.ts +8 -6
- package/src/slash/delegation-adapters.ts +32 -194
- package/src/slash/delegation-request.ts +43 -126
- package/src/slash/prompt-template-bridge.ts +158 -205
- package/src/slash/prompt-workflows.ts +21 -57
- package/src/slash/slash-bridge.ts +14 -0
- package/src/slash/slash-commands.ts +31 -632
- package/src/slash/subagents-admin.ts +18 -14
- package/src/tui/fleet-status.ts +156 -21
- package/src/tui/fleet-transcript.ts +110 -5
- package/src/tui/fleet.ts +56 -24
- package/src/tui/render.ts +291 -109
- package/src/types/pi-runtime-compat.d.ts +14 -0
- package/src/watchdog/lsp-diagnostics.ts +12 -7
- package/src/watchdog/model-selection.ts +2 -2
- package/src/watchdog/permission-arbiter.ts +145 -0
- package/src/watchdog/register-child.ts +1 -1
- package/src/watchdog/register-main.ts +1 -1
- package/src/watchdog/review.ts +4 -1
- package/src/watchdog/runtime.ts +3 -2
- package/src/workflows/chat-progress.ts +140 -0
- package/src/workflows/scripted-workflow.ts +415 -0
- package/agents/advisor.md +0 -73
- package/src/extension/chain-validation.ts +0 -181
package/src/extension/schemas.ts
CHANGED
|
@@ -79,7 +79,7 @@ const AcceptanceEvidenceKinds = [
|
|
|
79
79
|
|
|
80
80
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
81
|
anyOf: [
|
|
82
|
-
{ type: "string", enum: ["auto", "attested", "checked"
|
|
82
|
+
{ type: "string", enum: ["auto", "attested", "checked"] },
|
|
83
83
|
{
|
|
84
84
|
type: "string",
|
|
85
85
|
enum: ["reviewed"],
|
|
@@ -89,16 +89,16 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
89
89
|
{ type: "boolean", enum: [false] },
|
|
90
90
|
{ type: "object", additionalProperties: true },
|
|
91
91
|
],
|
|
92
|
-
description: `Optional acceptance policy. For reviewer/read-only calls, omit acceptance. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(", ")}. Evidence levels end at verified; use acceptance.review.required for review. Omitted means auto-inferred unless agentContract
|
|
92
|
+
description: `Optional acceptance policy. For reviewer/read-only calls, omit acceptance. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(", ")}. Evidence levels end at verified; use acceptance.review.required for review. Omitted means auto-inferred unless agentContract compatibility behavior is enabled.`,
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
const AgentContractOverride = Type.Object({
|
|
96
|
-
version: Type.Integer({ enum: [1], description: "
|
|
97
|
-
}, { additionalProperties: false, description: "
|
|
96
|
+
version: Type.Integer({ enum: [1], description: "Enable compatibility behavior for this run/child." }),
|
|
97
|
+
}, { additionalProperties: false, description: "Compatibility behavior. Omit for the default behavior." });
|
|
98
98
|
|
|
99
99
|
const ChainGateOverride = Type.String({
|
|
100
100
|
enum: ["execution", "acceptance"],
|
|
101
|
-
description: "For
|
|
101
|
+
description: "For chain steps with agentContract, choose whether the chain advances on execution success or acceptance success. Defaults to execution.",
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
const TurnBudgetOverride = Type.Object({
|
|
@@ -129,23 +129,6 @@ const UsageBudgetOverride = Type.Object({
|
|
|
129
129
|
costUsd: Type.Optional(UsageBudgetLimitOverride),
|
|
130
130
|
}, { additionalProperties: false, description: "Optional root-only reported-usage budget. Hard limits prevent future child launches; running children are not stopped." });
|
|
131
131
|
|
|
132
|
-
const TaskItem = Type.Object({
|
|
133
|
-
agent: Type.String(),
|
|
134
|
-
task: Type.String(),
|
|
135
|
-
cwd: Type.Optional(Type.String()),
|
|
136
|
-
count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
|
|
137
|
-
output: Type.Optional(OutputOverride),
|
|
138
|
-
outputMode: Type.Optional(OutputModeOverride),
|
|
139
|
-
reads: Type.Optional(ReadsOverride),
|
|
140
|
-
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
141
|
-
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
142
|
-
skill: Type.Optional(SkillOverride),
|
|
143
|
-
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
144
|
-
outputSchema: Type.Optional(JsonSchemaObject),
|
|
145
|
-
acceptance: Type.Optional(AcceptanceOverride),
|
|
146
|
-
agentContract: Type.Optional(AgentContractOverride),
|
|
147
|
-
});
|
|
148
|
-
|
|
149
132
|
// Parallel task item (within a parallel step)
|
|
150
133
|
export const ParallelTaskSchema = Type.Object({
|
|
151
134
|
agent: Type.String(),
|
|
@@ -245,6 +228,17 @@ export const ChainItem = Type.Object({
|
|
|
245
228
|
additionalProperties: false,
|
|
246
229
|
});
|
|
247
230
|
|
|
231
|
+
// Runtime mission handlers validate these untrusted nested objects loudly. Keeping
|
|
232
|
+
// their provider schema shallow avoids repeating a full durable-record schema in
|
|
233
|
+
// every tool request.
|
|
234
|
+
const MissionLaunchOverride = Type.Unsafe({
|
|
235
|
+
anyOf: [
|
|
236
|
+
{ type: "object", additionalProperties: true },
|
|
237
|
+
{ type: "boolean", enum: [false] },
|
|
238
|
+
],
|
|
239
|
+
});
|
|
240
|
+
const MissionUpdateOverride = Type.Unsafe({ type: "object", additionalProperties: true });
|
|
241
|
+
|
|
248
242
|
const ControlOverrides = Type.Object({
|
|
249
243
|
enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
|
|
250
244
|
needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-observed-activity window before a run needs attention" })),
|
|
@@ -265,31 +259,48 @@ const SubagentParamsSchema = Type.Object({
|
|
|
265
259
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
266
260
|
// Management action (when present, tool operates in management mode)
|
|
267
261
|
action: Type.Optional(Type.String({
|
|
268
|
-
description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}
|
|
262
|
+
description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task} or {workflowScript}); use it only for management/control actions."
|
|
269
263
|
})),
|
|
264
|
+
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
270
265
|
id: Type.Optional(Type.String({
|
|
271
|
-
description: "Run id or prefix for
|
|
266
|
+
description: "Run id or prefix for status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run."
|
|
272
267
|
})),
|
|
273
268
|
runId: Type.Optional(Type.String({
|
|
274
|
-
description: "Target run ID for
|
|
269
|
+
description: "Target run ID for interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint, or mission.attach-run. Prefer id for new calls."
|
|
275
270
|
})),
|
|
276
271
|
dir: Type.Optional(Type.String({
|
|
277
272
|
description: "Async run directory for action='status', action='stop', action='resume', or action='steer'."
|
|
278
273
|
})),
|
|
274
|
+
handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
|
|
279
275
|
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
280
276
|
view: Type.Optional(Type.String({
|
|
281
277
|
enum: ["fleet", "transcript"],
|
|
282
278
|
description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
|
|
283
279
|
})),
|
|
284
280
|
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
|
|
285
|
-
message: Type.Optional(Type.String({ description: "Follow-up message for
|
|
281
|
+
message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
|
|
286
282
|
steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls; extension RPC steering forces false so callers retain exact child ownership." })),
|
|
287
283
|
additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
|
|
288
284
|
scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
|
|
289
|
-
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for
|
|
285
|
+
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
|
|
286
|
+
focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
|
|
290
287
|
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' (off/minimal/low/medium/high/xhigh/max, inherit, or false for off)." })),
|
|
291
|
-
schedule: Type.Optional(Type.String({ description: "
|
|
292
|
-
scheduleName: Type.Optional(Type.String({ description: "
|
|
288
|
+
schedule: Type.Optional(Type.String({ deprecated: true, description: "Removed one-shot schedule field. Use action='schedule.create' with at." })),
|
|
289
|
+
scheduleName: Type.Optional(Type.String({ deprecated: true, description: "Removed schedule display field. Use name." })),
|
|
290
|
+
at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
|
|
291
|
+
every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
|
|
292
|
+
on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
|
|
293
|
+
timezone: Type.Optional(Type.String({ description: "IANA timezone reserved for a later calendar schedule slice." })),
|
|
294
|
+
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
295
|
+
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
296
|
+
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
297
|
+
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission." })),
|
|
298
|
+
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
299
|
+
missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
|
|
300
|
+
missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
|
|
301
|
+
runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
|
|
302
|
+
runStatus: Type.Optional(Type.String({ description: "Attached run status." })),
|
|
303
|
+
summary: Type.Optional(Type.String({ description: "Mission close summary." })),
|
|
293
304
|
// Chain identifier for management (can't reuse 'chain' — that's the execution array)
|
|
294
305
|
chainName: Type.Optional(Type.String({
|
|
295
306
|
description: "Chain name for get/update/delete management actions"
|
|
@@ -302,17 +313,14 @@ const SubagentParamsSchema = Type.Object({
|
|
|
302
313
|
],
|
|
303
314
|
description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
|
|
304
315
|
})),
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
worktree: Type.Optional(Type.Boolean({
|
|
308
|
-
|
|
309
|
-
})),
|
|
310
|
-
chain: Type.Optional(Type.Array(ChainItem, { description: "CHAIN mode: sequential steps; each result becomes {previous}. append-step takes one tail step and may use {chain_dir}/{outputs.name}." })),
|
|
316
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript orchestration. Starts asynchronously by default; pass async:false for a small foreground run. Use await runs.run(key, {agent, task, worktree?}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Set worktree:true at workflow or child level for a separate managed worktree per child; child fields override workflow defaults. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
317
|
+
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "terminal", "milestones", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; background and other-repo workflows stay quieter with terminal/milestone summaries." })),
|
|
318
|
+
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives a direct single child or each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
319
|
+
step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
|
|
311
320
|
context: Type.Optional(Type.String({
|
|
312
321
|
enum: ["fresh", "fork"],
|
|
313
322
|
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child in the invocation. If omitted, each requested agent uses its own defaultContext; agents without defaultContext: 'fork' run fresh.",
|
|
314
323
|
})),
|
|
315
|
-
chainDir: Type.Optional(Type.String({ description: "Persistent chain artifact directory; defaults to user-scoped temp storage." })),
|
|
316
324
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
317
325
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout for foreground and async/background runs; foreground defaults to 30m absent call/agent. Alias maxRuntimeMs." })),
|
|
318
326
|
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs for foreground and async/background runs; foreground defaults to 30m absent call/agent." })),
|
|
@@ -320,7 +328,7 @@ const SubagentParamsSchema = Type.Object({
|
|
|
320
328
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
321
329
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
322
330
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
323
|
-
cwd: Type.Optional(Type.String()),
|
|
331
|
+
cwd: Type.Optional(Type.String({ description: "Execution cwd, or target project directory for project.open/status/close." })),
|
|
324
332
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
325
333
|
includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
|
|
326
334
|
share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
|
|
@@ -352,6 +360,9 @@ const SubagentWaitParamsSchema = Type.Object({
|
|
|
352
360
|
id: Type.Optional(Type.String({
|
|
353
361
|
description: "Async run or remembered detached foreground run id/prefix to wait for one specific run. Omit to wait across every active async run started in this session.",
|
|
354
362
|
})),
|
|
363
|
+
nonBlocking: Type.Optional(Type.Boolean({
|
|
364
|
+
description: "When true, resolve id to one exact run, persist a wake subscription, and return immediately. The originating session is woken on completion, failure, attention, reconciliation failure, or timeout. Requires id and cannot be combined with all.",
|
|
365
|
+
})),
|
|
355
366
|
all: Type.Optional(Type.Boolean({
|
|
356
367
|
description: "Wait for ALL active runs to finish. Default false: return as soon as the first run finishes, so a fleet manager can spawn a replacement and wait again. Ignored when id targets a single run.",
|
|
357
368
|
})),
|
|
@@ -7,99 +7,49 @@ const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
|
-
• Use { action: "list" } before execution and only run executable/non-disabled agents
|
|
11
|
-
• Keep execution and management separate: omit action for
|
|
12
|
-
• Async/background runs
|
|
13
|
-
•
|
|
14
|
-
•
|
|
15
|
-
•
|
|
16
|
-
|
|
17
|
-
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `
|
|
18
|
-
|
|
19
|
-
EXECUTION (use exactly
|
|
20
|
-
• Before executing, use { action: "list" }
|
|
21
|
-
• SINGLE: { agent, task? }
|
|
22
|
-
•
|
|
23
|
-
•
|
|
24
|
-
•
|
|
25
|
-
•
|
|
26
|
-
•
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
• {task} -
|
|
32
|
-
•
|
|
33
|
-
• {
|
|
34
|
-
|
|
35
|
-
CHAIN EXAMPLES (quick reference for the nested schema):
|
|
36
|
-
• Sequential: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
|
|
37
|
-
• Parallel fan-out: { chain: [{parallel: [{agent:"agent-a", task:"Check part of {task}", count: 3}]}] }
|
|
38
|
-
• Mixed: { chain: [{agent:"agent-a", task:"Research {task}"}, {checkpoint:"review", message:"Approve implementation?"}, {parallel: [{agent:"agent-b", task:"Review {previous}", count: 2}]}, {agent:"agent-c", task:"Summarize {previous}"}] }
|
|
39
|
-
|
|
40
|
-
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
41
|
-
• { action: "list" } - discover executable agents/chains
|
|
42
|
-
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
43
|
-
• { action: "models", agent?: "name" } - show the runtime-loaded builtin subagent model mapping, optionally filtered to one builtin
|
|
44
|
-
• { action: "watchdog.status" | "watchdog.check" | "watchdog.recommend-model" } - inspect the opt-in subagent watchdog and its strong complementary model recommendation
|
|
45
|
-
• { action: "watchdog.configure", model: "recommended" | "inherit" | "provider/model[:thinking]", scope?: "session" | "user" | "project", target?: "main" | "children" | "child", agent?: "name", thinking?: "inherit" | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max" } - configure watchdog model selection; default scope is session, use persistent scopes only when the user asks
|
|
46
|
-
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, acceptance, acceptanceRole: "read-only" | "writer", ... } }
|
|
47
|
-
• acceptanceRole affects inferred acceptance only, never tool access. Explicit task mutation/no-edit intent wins; omission preserves name heuristics. Update with false or an empty string to clear it.
|
|
48
|
-
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
49
|
-
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
50
|
-
• { action: "eject", agent: "reviewer", agentScope?: "user" | "project" } - copy a bundled/package agent to user/project scope as an editable custom file that shadows the original (default scope: user)
|
|
51
|
-
• { action: "disable", agent: "reviewer", agentScope?: "user" | "project" } - hide any agent from runtime discovery via a reversible settings override (default scope: user)
|
|
52
|
-
• { action: "enable", agent: "reviewer", agentScope?: "user" | "project" } - remove a disabled override and restore discovery
|
|
53
|
-
• { action: "reset", agent: "reviewer", agentScope?: "user" | "project" } - delete the scope's custom agent file and/or settings override, restoring the bundled default
|
|
54
|
-
• { action: "grant-spawn-budget", additional: 10 } - add bounded capacity from the root interactive parent after native user confirmation; grants are rejected while children are active and cumulative grants cannot exceed the original configured cap
|
|
55
|
-
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
56
|
-
|
|
57
|
-
CONTROL:
|
|
58
|
-
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
59
|
-
• { action: "status", view: "fleet" } - read-only active foreground/async fleet view with transcript commands
|
|
60
|
-
• { action: "status", id: "...", view: "transcript", index?: 0, lines?: 80 } - tail a run or child output/session transcript
|
|
61
|
-
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
62
|
-
• { action: "stop", id: "..." } - stop a current-session top-level async run; stopped runs finish with state "stopped"
|
|
63
|
-
• { action: "resume", id: "...", message: "...", index?: 0 } - revive a paused, completed, or failed async/foreground child from its session; stopped runs are non-resumable; routed nested runs may accept live follow-ups; use steer for a live top-level async child
|
|
64
|
-
• { action: "steer", id: "...", message: "...", index?: 0 } - await correlated child-Pi input acceptance for up to 3 seconds; returns delivered, scheduled, pending, partial, recovered, or failed with a request id. Only top-level single runs may recover after a further 15-second pause/revival bound; chain, parallel, and nested runs never auto-interrupt.
|
|
65
|
-
• { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
|
|
66
|
-
• { action: "approve-checkpoint", id: "..." } / { action: "reject-checkpoint", id: "..." } - decide a paused current-session async chain checkpoint
|
|
67
|
-
|
|
68
|
-
SCHEDULE (opt-in; requires { "scheduledRuns": { "enabled": true } } in config.json):
|
|
69
|
-
• { action: "schedule", agent, task?, schedule: "+10m" | "2030-01-01T09:00:00Z", scheduleName? } - defer a subagent launch until a future time. Also accepts tasks[] or chain[]. Scheduled runs always launch async with fresh context; they become normal tracked async runs once they fire. Only schedule explicit delayed runs the user asked for.
|
|
70
|
-
• { action: "schedule-list" } - list scheduled runs for this session
|
|
71
|
-
• { action: "schedule-status", id: "..." } - inspect one scheduled run
|
|
72
|
-
• { action: "schedule-cancel", id: "..." } - cancel a scheduled run before it fires
|
|
73
|
-
|
|
74
|
-
DIAGNOSTICS:
|
|
75
|
-
• { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom
|
|
10
|
+
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
|
+
• Keep execution and management separate: omit action for single-child and workflowScript execution; use action only for management/control.
|
|
12
|
+
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
|
+
• 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
|
+
• 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.
|
|
15
|
+
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
|
|
16
|
+
|
|
17
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent, task } or compose work with { workflowScript }; omit action. workflowScript is the sole public orchestration surface. Use action only for management/control actions.
|
|
18
|
+
|
|
19
|
+
EXECUTION (use exactly one mode):
|
|
20
|
+
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
+
• SINGLE: { agent, task? } launches one child. Omit task for a self-contained agent.
|
|
22
|
+
• SCRIPTED WORKFLOW: { workflowScript: "const scan = await runs.run('scan', {agent:'agent-a', task:'...'}); return scan.output" }. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, terminal, milestones, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Direct single-child calls also support worktree:true; use workflowScript only when coordination is needed. For repository mutation lanes, set worktree:true on a direct single child, workflow, or individual runs.run/runs.all item for managed isolation instead of manual Git worktrees; 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.status, runs.ref/refs, emit, console, and standard JavaScript only. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
23
|
+
• Sequential replacement: { 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" }
|
|
24
|
+
• Parallel replacement: { 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}" }
|
|
25
|
+
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
26
|
+
• 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.
|
|
27
|
+
|
|
28
|
+
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
29
|
+
• list, get, models, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, mission.create/list/show/update/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available.
|
|
30
|
+
• 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.
|
|
31
|
+
• { 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.
|
|
32
|
+
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
33
|
+
• Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, agent, 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.
|
|
76
34
|
|
|
77
35
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
78
36
|
|
|
79
|
-
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `
|
|
37
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate one child with { agent, task } or orchestrate with { workflowScript }; omit action. workflowScript is the sole public orchestration surface.
|
|
80
38
|
|
|
81
39
|
EXECUTE:
|
|
82
|
-
•
|
|
83
|
-
• SINGLE {agent, task?};
|
|
84
|
-
•
|
|
85
|
-
•
|
|
86
|
-
• Chain templates may use {task}, {previous}, {chain_dir}, and named outputs. Parallel worktree isolation requires a clean git repo.
|
|
87
|
-
• Chain example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {parallel: [{agent:"agent-b", task:"Check {previous}", count: 3}]}] }
|
|
88
|
-
• If list shows proactive skill subagent suggestions, use a small fresh-context fanout only when the task is broad enough.
|
|
40
|
+
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
|
+
• SINGLE {agent, task?}; SCRIPT {workflowScript:"..."} with stable-key runs.run for one child and runs.all for parallel work. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on a direct single child or runs.run/runs.all item for managed isolation instead of manual Git worktrees. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off/terminal/milestones.
|
|
42
|
+
• 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]"}
|
|
43
|
+
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async runs. Omit acceptance for reviewer/read-only calls.
|
|
89
44
|
|
|
90
45
|
MANAGE / CONTROL:
|
|
91
|
-
• Use action without execution fields
|
|
92
|
-
•
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
•
|
|
98
|
-
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, session files, and { action:"status", id:"..." }.
|
|
99
|
-
|
|
100
|
-
SAFETY:
|
|
101
|
-
• Ordinary child subagents are not orchestrators and must not run subagents. Only explicit fanout children may use child-safe subagent, still bounded by depth/session limits.
|
|
102
|
-
• Keep one writer per cwd/worktree. Use fresh read-only review/validation fanout, then synthesize and apply fixes from the parent unless isolated worktrees were intentionally requested.`;
|
|
46
|
+
• Use action without execution fields for list/get/models/authoring, mission, watchdog, status, interrupt, stop, resume, steer, scheduling, diagnostics, and other management actions.
|
|
47
|
+
• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
|
|
48
|
+
|
|
49
|
+
ASYNC / SAFETY:
|
|
50
|
+
• Omitted async detaches background work. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
51
|
+
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
52
|
+
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
103
53
|
|
|
104
54
|
function isToolDescriptionMode(value: unknown): value is ToolDescriptionMode {
|
|
105
55
|
return value === "full" || value === "compact" || value === "custom";
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
|
+
import { readMissionBinding } from "../../missions/lifecycle.ts";
|
|
6
|
+
import { listMissions, missionRecordPath, resolveMissionStoreLocation } from "../../missions/store.ts";
|
|
7
|
+
import type { MissionStoreConfig } from "../../missions/types.ts";
|
|
8
|
+
import { resolveAuthorityDecision, type AuthorityPolicyConfig } from "../../policy/authority.ts";
|
|
9
|
+
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
10
|
+
import { DIRS, type Details, type SubagentState } from "../../shared/types.ts";
|
|
11
|
+
import { readStatus } from "../../shared/utils.ts";
|
|
12
|
+
import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
|
|
13
|
+
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
14
|
+
|
|
15
|
+
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
16
|
+
export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
|
|
17
|
+
|
|
18
|
+
export interface HerdrInspectorBinding {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
kind: "herdr-inspector";
|
|
21
|
+
runId: string;
|
|
22
|
+
asyncDir: string;
|
|
23
|
+
childIndex?: number;
|
|
24
|
+
missionId?: string;
|
|
25
|
+
missionPath?: string;
|
|
26
|
+
paneId: string;
|
|
27
|
+
openedAt: string;
|
|
28
|
+
lastFocusedAt?: string;
|
|
29
|
+
herdrVersion?: string;
|
|
30
|
+
command: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface InspectorParams {
|
|
34
|
+
id?: string;
|
|
35
|
+
runId?: string;
|
|
36
|
+
dir?: string;
|
|
37
|
+
index?: number;
|
|
38
|
+
focus?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface InspectorDeps {
|
|
42
|
+
state?: SubagentState;
|
|
43
|
+
asyncDirRoot?: string;
|
|
44
|
+
resultsDir?: string;
|
|
45
|
+
client?: HerdrClient;
|
|
46
|
+
missions?: MissionStoreConfig;
|
|
47
|
+
authorityPolicy?: AuthorityPolicyConfig;
|
|
48
|
+
cwd: string;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
now?: () => Date;
|
|
51
|
+
runnerPath?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
55
|
+
return { content: [{ type: "text", text }], ...(isError ? { isError: true } : {}), details: { mode: "management", results: [] } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatHerdrError(input: { code: HerdrErrorCode; message: string }): string {
|
|
59
|
+
return `Herdr inspector error (${input.code}): ${input.message}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function bindingPath(asyncDir: string, index?: number): string {
|
|
63
|
+
return path.join(asyncDir, "inspectors", `herdr${index === undefined ? "" : `-${index}`}.json`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseBinding(value: unknown): HerdrInspectorBinding | undefined {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
68
|
+
const input = value as Partial<HerdrInspectorBinding>;
|
|
69
|
+
if (input.schemaVersion !== 1 || input.kind !== "herdr-inspector") return undefined;
|
|
70
|
+
if (typeof input.runId !== "string" || typeof input.asyncDir !== "string" || typeof input.paneId !== "string" || typeof input.openedAt !== "string" || typeof input.command !== "string") return undefined;
|
|
71
|
+
if (input.childIndex !== undefined && (!Number.isInteger(input.childIndex) || input.childIndex < 0)) return undefined;
|
|
72
|
+
return input as HerdrInspectorBinding;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readHerdrInspectorBinding(asyncDir: string, index?: number): HerdrInspectorBinding | undefined {
|
|
76
|
+
try { return parseBinding(JSON.parse(fs.readFileSync(bindingPath(asyncDir, index), "utf-8"))); } catch { return undefined; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractPaneId(value: unknown): string | undefined {
|
|
80
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
81
|
+
const record = value as Record<string, unknown>;
|
|
82
|
+
const pane = record.pane && typeof record.pane === "object" && !Array.isArray(record.pane) ? record.pane as Record<string, unknown> : record;
|
|
83
|
+
for (const key of ["pane_id", "paneId", "id"]) if (typeof pane[key] === "string") return pane[key];
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shellQuote(value: string): string {
|
|
88
|
+
if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
89
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean }): string {
|
|
93
|
+
const args = [process.execPath, "--experimental-strip-types", input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop)];
|
|
94
|
+
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
95
|
+
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
96
|
+
return args.map(shellQuote).join(" ");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
|
|
100
|
+
try {
|
|
101
|
+
const binding = readMissionBinding(asyncDir);
|
|
102
|
+
if (binding) return { id: binding.missionId, path: missionRecordPath(binding.location, binding.missionId) };
|
|
103
|
+
const location = resolveMissionStoreLocation({ projectRoot: cwd, ...(config ? { config } : {}) });
|
|
104
|
+
const mission = listMissions(location).records.find((record) => record.runs.some((run) => run.runId === runId));
|
|
105
|
+
return mission ? { id: mission.id, path: missionRecordPath(location, mission.id) } : undefined;
|
|
106
|
+
} catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function pathWithin(base: string, candidate: string): boolean {
|
|
112
|
+
const resolvedBase = path.resolve(base);
|
|
113
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
114
|
+
return resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isTrustedAsyncDir(asyncDir: string, deps: InspectorDeps): boolean {
|
|
118
|
+
try {
|
|
119
|
+
if (fs.lstatSync(asyncDir).isSymbolicLink() || !fs.statSync(asyncDir).isDirectory()) return false;
|
|
120
|
+
const realDir = fs.realpathSync(asyncDir);
|
|
121
|
+
const registered = [...(deps.state?.asyncJobs.values() ?? [])].some((job) => {
|
|
122
|
+
try { return fs.realpathSync(job.asyncDir) === realDir; } catch { return false; }
|
|
123
|
+
});
|
|
124
|
+
if (registered) return true;
|
|
125
|
+
const root = deps.asyncDirRoot ?? DIRS.async;
|
|
126
|
+
if (!fs.existsSync(root) || !pathWithin(root, asyncDir)) return false;
|
|
127
|
+
return pathWithin(fs.realpathSync(root), realDir);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveAsyncTarget(params: InspectorParams, deps: InspectorDeps): { runId: string; asyncDir: string } | { error: string } {
|
|
134
|
+
const requestedId = params.id ?? params.runId;
|
|
135
|
+
if (params.dir) {
|
|
136
|
+
const asyncDir = path.resolve(params.dir);
|
|
137
|
+
if (!isTrustedAsyncDir(asyncDir, deps)) return { error: `Async run directory '${asyncDir}' is outside trusted run roots.` };
|
|
138
|
+
const status = readStatus(asyncDir);
|
|
139
|
+
if (!status) return { error: `No async run status found in '${asyncDir}'.` };
|
|
140
|
+
if (requestedId && requestedId !== status.runId && !status.runId.startsWith(requestedId)) return { error: `Run '${requestedId}' does not match status run '${status.runId}'.` };
|
|
141
|
+
return { runId: status.runId, asyncDir };
|
|
142
|
+
}
|
|
143
|
+
if (!requestedId) return { error: "Herdr inspector actions require id or dir." };
|
|
144
|
+
try {
|
|
145
|
+
const resolved = resolveSubagentRunId(requestedId, { state: deps.state, asyncDirRoot: deps.asyncDirRoot ?? DIRS.async, resultsDir: deps.resultsDir ?? DIRS.results });
|
|
146
|
+
if (!resolved) return { error: `No subagent run found for '${requestedId}'.` };
|
|
147
|
+
if (resolved.kind !== "async" || !resolved.location.asyncDir) return { error: `Run '${resolved.id}' is not an inspectable async run with lifecycle artifacts.` };
|
|
148
|
+
return { runId: resolved.id, asyncDir: resolved.location.asyncDir };
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
return { error: cause instanceof Error ? cause.message : String(cause) };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function paneExists(client: HerdrClient, paneId: string, signal?: AbortSignal): Promise<HerdrResult<unknown>> {
|
|
155
|
+
return client.run(["pane", "get", paneId], { timeoutMs: 5_000, signal });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function handleHerdrInspectorAction(action: HerdrInspectorAction, params: InspectorParams, deps: InspectorDeps): Promise<AgentToolResult<Details>> {
|
|
159
|
+
const target = resolveAsyncTarget(params, deps);
|
|
160
|
+
if ("error" in target) return result(target.error, true);
|
|
161
|
+
const status = readStatus(target.asyncDir);
|
|
162
|
+
if (!status) return result(`No lifecycle status exists for async run '${target.runId}'.`, true);
|
|
163
|
+
if (params.index !== undefined && (params.index < 0 || params.index >= (status.steps?.length ?? 0))) {
|
|
164
|
+
return result(`Async run '${target.runId}' has ${status.steps?.length ?? 0} children. Index ${params.index} is out of range.`, true);
|
|
165
|
+
}
|
|
166
|
+
const existing = readHerdrInspectorBinding(target.asyncDir, params.index);
|
|
167
|
+
const client = deps.client ?? createHerdrClient();
|
|
168
|
+
|
|
169
|
+
if (action === "inspector.status") {
|
|
170
|
+
if (!existing) return result(`No Herdr inspector binding exists for async run ${target.runId}${params.index === undefined ? "" : ` child ${params.index}`}.`);
|
|
171
|
+
const live = await paneExists(client, existing.paneId, deps.signal);
|
|
172
|
+
if (live.ok === false) return result(`${formatHerdrError(live.error)}\nBinding: ${bindingPath(target.asyncDir, params.index)}\nRun state remains authoritative: ${status.state}.`, true);
|
|
173
|
+
return result(`Herdr inspector ${existing.paneId} is open for async run ${target.runId}.\nRun state: ${status.state}\nBinding: ${bindingPath(target.asyncDir, params.index)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if (action === "inspector.close") {
|
|
178
|
+
if (!existing) return result(`No Herdr inspector binding exists for async run ${target.runId}.`);
|
|
179
|
+
const closed = await client.run(["pane", "close", existing.paneId], { timeoutMs: 10_000, signal: deps.signal });
|
|
180
|
+
if (closed.ok === false && closed.error.code !== "NOT_FOUND" && closed.error.code !== "PANE_GONE") return result(formatHerdrError(closed.error), true);
|
|
181
|
+
fs.rmSync(bindingPath(target.asyncDir, params.index), { force: true });
|
|
182
|
+
return result(`Closed Herdr inspector pane ${existing.paneId} for async run ${target.runId}. The subagent run was not stopped.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const detected = await detectHerdr(client, deps.signal);
|
|
186
|
+
if (detected.ok === false) return result(formatHerdrError(detected.error), true);
|
|
187
|
+
if (existing) {
|
|
188
|
+
const live = await paneExists(client, existing.paneId, deps.signal);
|
|
189
|
+
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." : ""}`);
|
|
190
|
+
}
|
|
191
|
+
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", status.cwd ?? deps.cwd];
|
|
192
|
+
if (params.focus !== false) splitArgs.push("--focus");
|
|
193
|
+
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: deps.signal });
|
|
194
|
+
if (split.ok === false) return result(formatHerdrError(split.error), true);
|
|
195
|
+
const paneId = extractPaneId(split.data);
|
|
196
|
+
if (!paneId) return result("Herdr inspector error (PANE_GONE): pane split returned no pane id.", true);
|
|
197
|
+
const mission = missionForRun(target.asyncDir, deps.cwd, deps.missions, target.runId);
|
|
198
|
+
const runnerPath = deps.runnerPath ?? fileURLToPath(new URL("./inspector-runner.ts", import.meta.url));
|
|
199
|
+
const command = inspectorCommand({
|
|
200
|
+
runnerPath,
|
|
201
|
+
asyncDir: target.asyncDir,
|
|
202
|
+
runId: target.runId,
|
|
203
|
+
index: params.index,
|
|
204
|
+
missionPath: mission?.path,
|
|
205
|
+
allowSteer: resolveAuthorityDecision({ action: "steerRun", policy: deps.authorityPolicy }) === "auto",
|
|
206
|
+
allowStop: resolveAuthorityDecision({ action: "stopRun", policy: deps.authorityPolicy }) === "auto",
|
|
207
|
+
});
|
|
208
|
+
const started = await client.run(["pane", "run", paneId, command], { timeoutMs: 15_000, signal: deps.signal });
|
|
209
|
+
if (started.ok === false) {
|
|
210
|
+
await client.run(["pane", "close", paneId], { timeoutMs: 5_000 });
|
|
211
|
+
return result(formatHerdrError(started.error), true);
|
|
212
|
+
}
|
|
213
|
+
const now = (deps.now?.() ?? new Date()).toISOString();
|
|
214
|
+
const binding: HerdrInspectorBinding = {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
kind: "herdr-inspector",
|
|
217
|
+
runId: target.runId,
|
|
218
|
+
asyncDir: target.asyncDir,
|
|
219
|
+
...(params.index !== undefined ? { childIndex: params.index } : {}),
|
|
220
|
+
...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),
|
|
221
|
+
paneId,
|
|
222
|
+
openedAt: now,
|
|
223
|
+
...(params.focus !== false ? { lastFocusedAt: now } : {}),
|
|
224
|
+
herdrVersion: detected.data.versionText,
|
|
225
|
+
command,
|
|
226
|
+
};
|
|
227
|
+
writeAtomicJson(bindingPath(target.asyncDir, params.index), binding);
|
|
228
|
+
return result(`Opened read-only Herdr inspector pane ${paneId} for async run ${target.runId}. Closing the pane does not stop the run.\nControls inside the pane: steer <message>, stop, status.`);
|
|
229
|
+
}
|