pi-subagents 0.53.0 → 0.55.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 +66 -5
- package/README.md +1 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +6 -6
- package/docs/extension-api.md +14 -4
- package/docs/models.md +41 -6
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +19 -7
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +50 -26
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
- package/src/agents/agent-management.ts +115 -14
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +195 -47
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +33 -19
- package/src/api/project-panes.ts +2 -0
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +24 -4
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +112 -13
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +251 -38
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +110 -8
- package/src/runs/foreground/subagent-executor.ts +361 -45
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +46 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +31 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +72 -5
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +107 -10
package/src/api/preflight.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import * as fs from "node:fs";
|
|
3
2
|
import * as path from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
@@ -7,19 +6,22 @@ import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
|
7
6
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
8
7
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
9
8
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
|
|
9
|
+
import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
|
|
10
10
|
import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/pi-args.ts";
|
|
11
11
|
import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
|
|
12
12
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
13
13
|
import { resolveEffectiveThinking } from "../shared/model-info.ts";
|
|
14
|
+
import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
|
|
14
15
|
import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
|
|
15
16
|
import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
|
|
16
17
|
import { appendTurnBudgetSystemPrompt } from "../runs/shared/turn-budget.ts";
|
|
18
|
+
import { resolvePermissionRules } from "../runs/shared/permissions.ts";
|
|
17
19
|
import type { ResolvedTurnBudget } from "../shared/types.ts";
|
|
18
20
|
import type { ResolvedMcpDirectToolSelection } from "../runs/shared/mcp-direct-tool-allowlist.ts";
|
|
19
21
|
import { resolveStepBehavior } from "../shared/settings.ts";
|
|
20
22
|
import { canPreferForkFromSnapshot, resolveSubagentLaunchContext } from "../shared/fork-context.ts";
|
|
21
23
|
import { loadConfig } from "../extension/config.ts";
|
|
22
|
-
import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest } from "../shared/launch-contract.ts";
|
|
24
|
+
import { agentDefinitionDigest, AGENT_DEFINITION_PROJECTION_VERSION, launchBindingDigest, stableJsonDigest } from "../shared/launch-contract.ts";
|
|
23
25
|
import { DIRS, TEMP_ROOT_DIR } from "../shared/types.ts";
|
|
24
26
|
import { processTerminalCandidatePath, processTerminalPath } from "../runs/background/process-terminal.ts";
|
|
25
27
|
import { resultFilePath } from "../runs/background/result-files.ts";
|
|
@@ -35,7 +37,8 @@ export type SubagentLaunchContractReasonCode =
|
|
|
35
37
|
| "invalid_artifact_dir"
|
|
36
38
|
| "invalid_cwd"
|
|
37
39
|
| "unsupported_mode"
|
|
38
|
-
| "restricted_agent"
|
|
40
|
+
| "restricted_agent"
|
|
41
|
+
| "thinking_ceiling";
|
|
39
42
|
|
|
40
43
|
export interface SubagentLaunchContractDiagnostic {
|
|
41
44
|
code: SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning";
|
|
@@ -51,6 +54,8 @@ export interface SubagentLaunchContractInput {
|
|
|
51
54
|
context?: "fresh" | "fork";
|
|
52
55
|
model?: string;
|
|
53
56
|
thinking?: string | false;
|
|
57
|
+
thinkingCeiling?: ThinkingLevel;
|
|
58
|
+
inheritedThinkingCeiling?: ThinkingLevel;
|
|
54
59
|
parentModel?: ParentModel;
|
|
55
60
|
availableModels?: ReadonlyArray<AvailableModelInfo | { provider: string; id: string; fullId?: string; reasoning?: boolean }>;
|
|
56
61
|
preferredProvider?: string;
|
|
@@ -145,6 +150,7 @@ export interface SubagentLaunchContract {
|
|
|
145
150
|
model?: string;
|
|
146
151
|
modelCandidates: string[];
|
|
147
152
|
thinking?: string;
|
|
153
|
+
thinkingCeiling?: ThinkingLevel;
|
|
148
154
|
systemPromptMode: AgentConfig["systemPromptMode"];
|
|
149
155
|
inheritProjectContext: boolean;
|
|
150
156
|
inheritSkills: boolean;
|
|
@@ -174,20 +180,8 @@ function packageVersion(): string {
|
|
|
174
180
|
return parsed.version;
|
|
175
181
|
}
|
|
176
182
|
|
|
177
|
-
function stableJson(value: unknown): string {
|
|
178
|
-
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
179
|
-
if (value && typeof value === "object") {
|
|
180
|
-
return `{${Object.entries(value as Record<string, unknown>)
|
|
181
|
-
.filter(([, entry]) => entry !== undefined)
|
|
182
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
183
|
-
.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
|
|
184
|
-
.join(",")}}`;
|
|
185
|
-
}
|
|
186
|
-
return JSON.stringify(value);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
183
|
function digestContract(contract: Omit<SubagentLaunchContract, "digest">): string {
|
|
190
|
-
return
|
|
184
|
+
return stableJsonDigest(contract);
|
|
191
185
|
}
|
|
192
186
|
|
|
193
187
|
function normalizeAvailableModels(models: SubagentLaunchContractInput["availableModels"]): AvailableModelInfo[] {
|
|
@@ -287,20 +281,38 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
287
281
|
|
|
288
282
|
const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
|
|
289
283
|
const availableModels = normalizeAvailableModels(input.availableModels);
|
|
290
|
-
const preferredProvider = input.preferredProvider ?? input.parentModel?.provider;
|
|
284
|
+
const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
|
|
285
|
+
const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
|
|
291
286
|
const primaryModel = externalRunner
|
|
292
287
|
? undefined
|
|
293
|
-
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope:
|
|
288
|
+
: resolveEffectiveSubagentModel(input.model, agent.model, input.parentModel, availableModels, preferredProvider, { scope: modelScopes });
|
|
294
289
|
const effectiveThinkingConfig = input.thinking !== undefined ? input.thinking : agent.thinking;
|
|
290
|
+
const thinkingCeiling = externalRunner ? undefined : intersectThinkingCeilings(
|
|
291
|
+
discovered.maxThinking,
|
|
292
|
+
input.thinkingCeiling,
|
|
293
|
+
input.inheritedThinkingCeiling,
|
|
294
|
+
decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
|
|
295
|
+
);
|
|
295
296
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
296
297
|
const modelCandidates = externalRunner
|
|
297
298
|
? []
|
|
298
299
|
: buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
|
|
299
|
-
scope:
|
|
300
|
+
scope: modelScopes,
|
|
300
301
|
primaryModelFromParent: inheritsParentModel(input.model, agent.model, input.parentModel),
|
|
301
302
|
})
|
|
302
303
|
.map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
|
|
304
|
+
if (!externalRunner) {
|
|
305
|
+
try {
|
|
306
|
+
assertThinkingWithinCeiling({ model, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
|
|
307
|
+
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
310
|
+
diagnostics.push({ code: "thinking_ceiling", severity: "error", message });
|
|
311
|
+
return { ok: false, code: "thinking_ceiling", message, diagnostics };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
303
314
|
let toolPlan: PiLaunchToolPlan;
|
|
315
|
+
const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
|
|
304
316
|
try {
|
|
305
317
|
toolPlan = resolvePiLaunchToolPlan({
|
|
306
318
|
tools: agent.tools,
|
|
@@ -312,6 +324,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
312
324
|
structuredOutput: Boolean(input.outputSchema),
|
|
313
325
|
capabilityCeiling: effectiveCapabilityCeiling,
|
|
314
326
|
agentName: agent.name,
|
|
327
|
+
permissionRules,
|
|
315
328
|
});
|
|
316
329
|
} catch (error) {
|
|
317
330
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -367,6 +380,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
367
380
|
...(model ? { model } : {}),
|
|
368
381
|
modelCandidates,
|
|
369
382
|
...(resolveEffectiveThinking(model, effectiveThinkingConfig) ? { thinking: resolveEffectiveThinking(model, effectiveThinkingConfig) } : {}),
|
|
383
|
+
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
370
384
|
systemPromptMode: agent.systemPromptMode,
|
|
371
385
|
inheritProjectContext: agent.inheritProjectContext,
|
|
372
386
|
inheritSkills: agent.inheritSkills,
|
package/src/api/project-panes.ts
CHANGED
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
createProjectPaneManager,
|
|
11
11
|
openProjectPane,
|
|
12
12
|
getProjectPaneStatus,
|
|
13
|
+
focusProjectPane,
|
|
13
14
|
closeProjectPane,
|
|
14
15
|
readProjectPaneBinding,
|
|
15
16
|
projectPaneBindingPath,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
type CloseProjectPaneOptions,
|
|
22
23
|
type OpenProjectPaneData,
|
|
23
24
|
type ProjectPaneStatusData,
|
|
25
|
+
type FocusProjectPaneData,
|
|
24
26
|
type CloseProjectPaneData,
|
|
25
27
|
type ProjectPaneRuntime,
|
|
26
28
|
type ProjectPaneError,
|
package/src/extension/doctor.ts
CHANGED
|
@@ -224,6 +224,13 @@ function formatPermissionSystemSection(): string[] {
|
|
|
224
224
|
return lines;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function formatWorkflowScriptSection(): string[] {
|
|
228
|
+
return [
|
|
229
|
+
"- helpers: runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console",
|
|
230
|
+
"- recovery: if runs.all is missing, reload or update pi-subagents; await Promise.all([runs.run(...)]) is also supported",
|
|
231
|
+
];
|
|
232
|
+
}
|
|
233
|
+
|
|
227
234
|
export function buildDoctorReport(input: DoctorReportInput): string {
|
|
228
235
|
const paths = input.paths ?? defaultPaths();
|
|
229
236
|
const deps = { ...DEFAULT_DEPS, ...input.deps };
|
|
@@ -253,6 +260,9 @@ export function buildDoctorReport(input: DoctorReportInput): string {
|
|
|
253
260
|
"Active async capacity",
|
|
254
261
|
...formatActiveAsyncCapacitySection(input),
|
|
255
262
|
"",
|
|
263
|
+
"Workflow script",
|
|
264
|
+
...formatWorkflowScriptSection(),
|
|
265
|
+
"",
|
|
256
266
|
"Permission system",
|
|
257
267
|
...formatPermissionSystemSection(),
|
|
258
268
|
"",
|
|
@@ -11,6 +11,7 @@ import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute,
|
|
|
11
11
|
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
12
12
|
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
13
13
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
14
|
+
import { finalizeToolResult } from "./tool-result.ts";
|
|
14
15
|
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
15
16
|
import { type Details, type SubagentState } from "../shared/types.ts";
|
|
16
17
|
|
|
@@ -181,8 +182,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
181
182
|
"Mutating management actions (create, update, delete, eject, disable, enable, reset, grant-spawn-budget) are blocked in this mode.",
|
|
182
183
|
].join("\n"),
|
|
183
184
|
parameters: params,
|
|
184
|
-
execute(id, params, signal, onUpdate, ctx) {
|
|
185
|
-
return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
185
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
186
|
+
return finalizeToolResult(await executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
|
|
186
187
|
},
|
|
187
188
|
};
|
|
188
189
|
|
package/src/extension/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { registerMainWatchdog } from "../watchdog/register-main.ts";
|
|
|
43
43
|
import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
44
44
|
import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
|
|
45
45
|
import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
|
|
46
|
+
import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
|
|
46
47
|
import { registerSubagentRpcBridge } from "./rpc.ts";
|
|
47
48
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
48
49
|
import { inspectSubagentStatus } from "../runs/background/run-status.ts";
|
|
@@ -57,6 +58,7 @@ import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capabili
|
|
|
57
58
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
58
59
|
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
59
60
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
|
|
61
|
+
import { finalizeToolResult } from "./tool-result.ts";
|
|
60
62
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
|
|
61
63
|
import { restoreForegroundRunHistory } from "../runs/foreground/foreground-history.ts";
|
|
62
64
|
import { resolveMissionStoreLocation } from "../missions/store.ts";
|
|
@@ -435,6 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
435
437
|
grantHistory: [],
|
|
436
438
|
},
|
|
437
439
|
activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
|
|
440
|
+
herdrProjectPanes: new Map(),
|
|
438
441
|
asyncJobs: new Map(),
|
|
439
442
|
fleetJobs: new Map(),
|
|
440
443
|
foregroundRuns: new Map(),
|
|
@@ -500,7 +503,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
500
503
|
...all.user,
|
|
501
504
|
...all.project,
|
|
502
505
|
];
|
|
503
|
-
|
|
506
|
+
const merged = mergeRuntimeAgents(pi, discovered, configuredAgents);
|
|
507
|
+
if (discovered.maxThinking === undefined) return merged;
|
|
508
|
+
return {
|
|
509
|
+
...merged,
|
|
510
|
+
agents: merged.agents.map((agent) => agent.maxThinking === discovered.maxThinking ? agent : { ...agent, maxThinking: discovered.maxThinking }),
|
|
511
|
+
};
|
|
504
512
|
};
|
|
505
513
|
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
506
514
|
widgetEnabled: asyncWidgetEnabled,
|
|
@@ -517,7 +525,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
517
525
|
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
518
526
|
hasDeliveryDemand: hasResultDeliveryDemand,
|
|
519
527
|
deliverIntercomResults: config.intercomBridge?.resultDelivery === true,
|
|
520
|
-
resultScanLogging: config.resultScanLogging
|
|
528
|
+
resultScanLogging: config.resultScanLogging,
|
|
521
529
|
},
|
|
522
530
|
);
|
|
523
531
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
@@ -599,6 +607,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
599
607
|
const expandKey = keyText("app.tools.expand");
|
|
600
608
|
text += `\n ${theme.fg("dim", `${expandKey} full notification`)}`;
|
|
601
609
|
}
|
|
610
|
+
if (details.workflowRunId) {
|
|
611
|
+
text += `\n ${theme.fg("muted", `workflow: ${details.workflowRunId}`)}`;
|
|
612
|
+
}
|
|
613
|
+
if (details.childRuns?.length) {
|
|
614
|
+
text += `\n ${theme.fg("muted", `children: ${details.childRuns.map((child) => `${child.workflowKey ?? child.agent ?? "child"}=${child.runId}`).join(", ")}`)}`;
|
|
615
|
+
}
|
|
616
|
+
if (details.reconciledFromDetachedChild) {
|
|
617
|
+
text += `\n ${theme.fg("muted", `reconciled child: ${details.reconciledFromDetachedChild}`)}`;
|
|
618
|
+
}
|
|
602
619
|
if (details.sessionLabel && details.sessionValue) {
|
|
603
620
|
text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
|
|
604
621
|
}
|
|
@@ -658,8 +675,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
658
675
|
...buildSubagentToolPromptMetadata(config),
|
|
659
676
|
parameters,
|
|
660
677
|
|
|
661
|
-
execute(id, params, signal, onUpdate, ctx) {
|
|
662
|
-
return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
678
|
+
async execute(id, params, signal, onUpdate, ctx) {
|
|
679
|
+
return finalizeToolResult(await executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx));
|
|
663
680
|
},
|
|
664
681
|
|
|
665
682
|
renderCall(args, theme) {
|
|
@@ -731,6 +748,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
731
748
|
const herdrStatusBridge = registerHerdrStatusBridge({
|
|
732
749
|
events: pi.events,
|
|
733
750
|
getRuns: activeHerdrRuns,
|
|
751
|
+
getProjectPaneCount: () => [...(state.herdrProjectPanes?.values() ?? [])].filter((pane) => pane.state === "open").length,
|
|
734
752
|
async runHerdr(args) {
|
|
735
753
|
await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
|
|
736
754
|
},
|
|
@@ -839,6 +857,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
839
857
|
granted: 0,
|
|
840
858
|
grantHistory: [],
|
|
841
859
|
};
|
|
860
|
+
const projectPaneOwnerRoot = path.resolve(ctx.cwd);
|
|
861
|
+
restoreHerdrProjectPaneSnapshots(state, [...new Set([...(state.herdrProjectPanes?.keys() ?? []), ...listHerdrProjectPaneRoots(projectPaneOwnerRoot), projectPaneOwnerRoot])]);
|
|
842
862
|
// Set PI_SUBAGENT_PARENT_SESSION for permission-system forwarding.
|
|
843
863
|
// Only set in the root session (the interactive UI session), not in
|
|
844
864
|
// child subagent processes — children inherit the parent's value
|
|
@@ -17,6 +17,12 @@ export interface PublicSubagentExecutionParams {
|
|
|
17
17
|
output?: unknown;
|
|
18
18
|
resume?: unknown;
|
|
19
19
|
clarify?: unknown;
|
|
20
|
+
workflowParentRunId?: unknown;
|
|
21
|
+
workflowKey?: unknown;
|
|
22
|
+
workflowChildAsyncId?: unknown;
|
|
23
|
+
workflowAwaitAsync?: unknown;
|
|
24
|
+
workflowParentDeadlineAt?: unknown;
|
|
25
|
+
suppressRoutineResultIntercom?: unknown;
|
|
20
26
|
runFanoutBudget?: unknown;
|
|
21
27
|
runFanoutAdmitted?: unknown;
|
|
22
28
|
}
|
|
@@ -46,6 +52,9 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
46
52
|
if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
|
|
47
53
|
return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
|
|
48
54
|
}
|
|
55
|
+
if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
|
|
56
|
+
return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
|
|
57
|
+
}
|
|
49
58
|
const action = params.action;
|
|
50
59
|
if (action !== undefined && (typeof action !== "string" || !action.trim())) {
|
|
51
60
|
return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
|
|
@@ -111,18 +120,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
111
120
|
if (params.task !== undefined && typeof params.task !== "string") {
|
|
112
121
|
return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
|
|
113
122
|
}
|
|
114
|
-
const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
|
|
115
|
-
const child = {
|
|
116
|
-
agent: params.agent.trim(),
|
|
117
|
-
...(params.task !== undefined ? { task: params.task } : {}),
|
|
118
|
-
output: output === undefined ? true : output,
|
|
119
|
-
};
|
|
120
123
|
return {
|
|
121
124
|
ok: true,
|
|
122
125
|
params: {
|
|
123
|
-
...
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
...params,
|
|
127
|
+
agent: params.agent.trim(),
|
|
128
|
+
output: params.output === undefined ? true : params.output,
|
|
126
129
|
} as T,
|
|
127
130
|
};
|
|
128
131
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -13,14 +13,17 @@ import {
|
|
|
13
13
|
type SubagentState,
|
|
14
14
|
DIRS,
|
|
15
15
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
16
|
+
SUBAGENT_CHILD_STATUS_EVENT,
|
|
16
17
|
SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
17
18
|
SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
19
|
+
type SubagentChildStatusEvent,
|
|
18
20
|
} from "../shared/types.ts";
|
|
19
21
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
20
22
|
import { readStatus } from "../shared/utils.ts";
|
|
21
23
|
import { SubagentParams } from "./schemas.ts";
|
|
22
24
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
23
25
|
import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
|
|
26
|
+
import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
24
27
|
|
|
25
28
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
26
29
|
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
@@ -405,6 +408,7 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
405
408
|
request: SUBAGENT_RPC_REQUEST_EVENT,
|
|
406
409
|
replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
|
|
407
410
|
asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
411
|
+
childStatus: SUBAGENT_CHILD_STATUS_EVENT,
|
|
408
412
|
processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
|
|
409
413
|
},
|
|
410
414
|
session: sessionData(ctx),
|
|
@@ -500,8 +504,14 @@ function stopAsyncRun(
|
|
|
500
504
|
params: unknown,
|
|
501
505
|
options: RegisterSubagentRpcBridgeOptions,
|
|
502
506
|
ctx: ExtensionContext,
|
|
503
|
-
): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
|
|
504
|
-
const
|
|
507
|
+
): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string; childId?: string } {
|
|
508
|
+
const input = assertRecordParams(params, "stop");
|
|
509
|
+
const rawChildId = input.childId;
|
|
510
|
+
if (rawChildId !== undefined && (typeof rawChildId !== "string" || !rawChildId.trim() || /[\r\n]/.test(rawChildId) || rawChildId.length > 256)) {
|
|
511
|
+
throw new SubagentRpcError("invalid_params", "RPC stop childId must be a non-empty string without newlines and at most 256 characters.");
|
|
512
|
+
}
|
|
513
|
+
const childId = typeof rawChildId === "string" ? rawChildId : undefined;
|
|
514
|
+
const target = normalizeTargetParams(input, "stop");
|
|
505
515
|
assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
|
|
506
516
|
const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
|
|
507
517
|
const resultsDir = options.resultsDir ?? DIRS.results;
|
|
@@ -523,7 +533,60 @@ function stopAsyncRun(
|
|
|
523
533
|
throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
|
|
524
534
|
}
|
|
525
535
|
|
|
536
|
+
let child: ResolvedAsyncStatusChild | undefined;
|
|
537
|
+
const emitChildStopping = (runId: string, asyncDir: string, stoppedChild: ResolvedAsyncStatusChild, ts = options.now?.() ?? Date.now()): void => {
|
|
538
|
+
options.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
|
|
539
|
+
type: "subagent.child-status",
|
|
540
|
+
version: 1,
|
|
541
|
+
runId,
|
|
542
|
+
childId: stoppedChild.id,
|
|
543
|
+
status: "stopping",
|
|
544
|
+
ts,
|
|
545
|
+
reason: "user",
|
|
546
|
+
source: "rpc",
|
|
547
|
+
asyncDir,
|
|
548
|
+
stepIndex: stoppedChild.index,
|
|
549
|
+
agent: stoppedChild.step.agent,
|
|
550
|
+
...(stoppedChild.step.runId ? { childRunId: stoppedChild.step.runId } : {}),
|
|
551
|
+
...(stoppedChild.step.workflowKey ? { workflowKey: stoppedChild.step.workflowKey } : {}),
|
|
552
|
+
...(stoppedChild.step.phase ? { phase: stoppedChild.step.phase } : {}),
|
|
553
|
+
...(stoppedChild.step.label ? { label: stoppedChild.step.label } : {}),
|
|
554
|
+
} satisfies SubagentChildStatusEvent);
|
|
555
|
+
};
|
|
556
|
+
if (childId !== undefined) {
|
|
557
|
+
const resolution = resolveAsyncStatusChild(initialStatus, childId);
|
|
558
|
+
if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
|
|
559
|
+
child = resolution.child;
|
|
560
|
+
if (!isStoppableAsyncStatusStep(child.step)) {
|
|
561
|
+
throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${initialRunId}' is ${child.step.status}; stop only supports pending or running children.`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
526
564
|
if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
|
|
565
|
+
if (child) {
|
|
566
|
+
const stopChild = options.state?.workflowChildStops?.get(initialRunId);
|
|
567
|
+
if (!stopChild) throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; child stop is unavailable.`);
|
|
568
|
+
if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
|
|
569
|
+
emitChildStopping(initialRunId, location.asyncDir, child);
|
|
570
|
+
return {
|
|
571
|
+
runId: initialRunId,
|
|
572
|
+
asyncDir: location.asyncDir,
|
|
573
|
+
previousState: initialStatus.state,
|
|
574
|
+
state: "stopping",
|
|
575
|
+
childId: child.id,
|
|
576
|
+
message: `Stop requested for child ${child.id} in async run ${initialRunId}.`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const workflowController = options.state?.workflowControllers?.get(initialRunId);
|
|
580
|
+
if (workflowController) {
|
|
581
|
+
workflowController.abort(new Error("Workflow stopped by RPC."));
|
|
582
|
+
return {
|
|
583
|
+
runId: initialRunId,
|
|
584
|
+
asyncDir: location.asyncDir,
|
|
585
|
+
previousState: initialStatus.state,
|
|
586
|
+
state: "stopping",
|
|
587
|
+
message: `Stop requested for async run ${initialRunId}.`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
527
590
|
throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
|
|
528
591
|
}
|
|
529
592
|
|
|
@@ -541,6 +604,14 @@ function stopAsyncRun(
|
|
|
541
604
|
if (status.state !== "running") {
|
|
542
605
|
throw new SubagentRpcError("invalid_state", `Async run ${runId} is ${status.state}; stop only supports running async runs.`);
|
|
543
606
|
}
|
|
607
|
+
if (childId !== undefined) {
|
|
608
|
+
const resolution = resolveAsyncStatusChild(status, childId);
|
|
609
|
+
if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
|
|
610
|
+
child = resolution.child;
|
|
611
|
+
if (!isStoppableAsyncStatusStep(child.step)) {
|
|
612
|
+
throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${runId}' is ${child.step.status}; stop only supports pending or running children.`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
544
615
|
|
|
545
616
|
try {
|
|
546
617
|
deliverStopRequest({
|
|
@@ -549,17 +620,20 @@ function stopAsyncRun(
|
|
|
549
620
|
kill: options.kill,
|
|
550
621
|
now: options.now,
|
|
551
622
|
source: "rpc-stop",
|
|
623
|
+
...(child ? { targetIndex: child.index, childId: child.id } : {}),
|
|
552
624
|
});
|
|
553
625
|
} catch (error) {
|
|
554
626
|
throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
555
627
|
}
|
|
628
|
+
if (child) emitChildStopping(runId, location.asyncDir, child);
|
|
556
629
|
|
|
557
630
|
return {
|
|
558
631
|
runId,
|
|
559
632
|
asyncDir: location.asyncDir,
|
|
560
633
|
previousState: status.state,
|
|
561
634
|
state: "stopping",
|
|
562
|
-
|
|
635
|
+
...(child ? { childId: child.id } : {}),
|
|
636
|
+
message: child ? `Stop requested for child ${child.id} in async run ${runId}.` : `Stop requested for async run ${runId}.`,
|
|
563
637
|
};
|
|
564
638
|
}
|
|
565
639
|
|
package/src/extension/schemas.ts
CHANGED
|
@@ -271,6 +271,7 @@ const SubagentParamProperties = {
|
|
|
271
271
|
})),
|
|
272
272
|
handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
|
|
273
273
|
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
274
|
+
childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
|
|
274
275
|
view: Type.Optional(Type.String({
|
|
275
276
|
enum: ["fleet", "transcript"],
|
|
276
277
|
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.",
|
|
@@ -307,7 +308,7 @@ const SubagentParamProperties = {
|
|
|
307
308
|
],
|
|
308
309
|
description: "Agent config for create/update. Object or JSON string."
|
|
309
310
|
})),
|
|
310
|
-
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]);
|
|
311
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. 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. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
311
312
|
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
|
|
312
313
|
isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
|
|
313
314
|
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
@@ -6,7 +6,7 @@ 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
|
|
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 read its ordered array result with indexes, destructuring, or .map(...), not by key property. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
10
10
|
|
|
11
11
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
12
12
|
|
|
@@ -14,8 +14,9 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
|
14
14
|
"Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
|
|
15
15
|
"Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
|
|
16
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}, ...]);
|
|
17
|
+
"For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. 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
18
|
"Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
|
|
19
|
+
"To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids.",
|
|
19
20
|
"Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
|
|
20
21
|
];
|
|
21
22
|
|
|
@@ -32,8 +33,9 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
|
|
|
32
33
|
|
|
33
34
|
EXECUTION:
|
|
34
35
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
35
|
-
•
|
|
36
|
-
•
|
|
36
|
+
• When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids.
|
|
37
|
+
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action or workflowScript.
|
|
38
|
+
• 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. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. 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.
|
|
37
39
|
• 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" }
|
|
38
40
|
• 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}" }
|
|
39
41
|
• Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork 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.
|
|
@@ -50,8 +52,9 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
|
|
|
50
52
|
|
|
51
53
|
EXECUTE:
|
|
52
54
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
53
|
-
•
|
|
54
|
-
•
|
|
55
|
+
• Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids.
|
|
56
|
+
• SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action or workflowScript.
|
|
57
|
+
• 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. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. 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.
|
|
55
58
|
• 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]"}
|
|
56
59
|
• context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork 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.
|
|
57
60
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert pi-subagents' internal logical-error result into the rejection Pi's
|
|
5
|
+
* public tool boundary uses to emit a canonical errored ToolResult.
|
|
6
|
+
*
|
|
7
|
+
* Keep this at registered tool boundaries. Internal workflows intentionally
|
|
8
|
+
* retain their return-based error handling.
|
|
9
|
+
*/
|
|
10
|
+
export function finalizeToolResult<T>(result: AgentToolResult<T>): AgentToolResult<T> {
|
|
11
|
+
if (result.isError !== true) return result;
|
|
12
|
+
|
|
13
|
+
const message = result.content
|
|
14
|
+
.flatMap((item) => item.type === "text" && typeof item.text === "string" ? [item.text] : [])
|
|
15
|
+
.join("\n")
|
|
16
|
+
.trim();
|
|
17
|
+
|
|
18
|
+
throw new Error(message || "pi-subagents reported a logical tool failure.");
|
|
19
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
export type HerdrErrorCode =
|
|
4
4
|
| "HERDR_UNAVAILABLE"
|
|
@@ -16,7 +16,7 @@ export interface HerdrClient {
|
|
|
16
16
|
run<T = unknown>(args: string[], options?: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean }): Promise<HerdrResult<T>>;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) =>
|
|
19
|
+
type SpawnHerdr = (command: string, args: readonly string[], options: { shell: false; windowsHide: true; env: NodeJS.ProcessEnv }) => ReturnType<typeof spawn>;
|
|
20
20
|
|
|
21
21
|
function error(code: HerdrErrorCode, message: string, details?: unknown): HerdrResult<never> {
|
|
22
22
|
return { ok: false, error: { code, message, ...(details !== undefined ? { details } : {}) } };
|
|
@@ -46,7 +46,7 @@ export function createHerdrClient(options: { bin?: string; spawn?: SpawnHerdr }
|
|
|
46
46
|
return {
|
|
47
47
|
run<T>(args: string[], runOptions: { timeoutMs?: number; signal?: AbortSignal; textOk?: boolean } = {}): Promise<HerdrResult<T>> {
|
|
48
48
|
return new Promise((resolve) => {
|
|
49
|
-
let child:
|
|
49
|
+
let child: ReturnType<typeof spawn>;
|
|
50
50
|
try {
|
|
51
51
|
child = spawnImpl(bin, args, { shell: false, windowsHide: true, env: process.env });
|
|
52
52
|
} catch (cause) {
|