pi-subagents 0.28.0 → 0.30.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 +31 -0
- package/README.md +26 -62
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +29 -35
- package/src/agents/agent-management.ts +29 -22
- package/src/agents/agent-selection.ts +2 -0
- package/src/agents/agent-serializer.ts +5 -10
- package/src/agents/agents.ts +339 -47
- package/src/agents/chain-serializer.ts +4 -9
- package/src/agents/proactive-skills.ts +191 -0
- package/src/extension/doctor.ts +4 -3
- package/src/extension/fanout-child.ts +1 -3
- package/src/extension/index.ts +6 -9
- package/src/extension/schemas.ts +63 -26
- package/src/intercom/intercom-bridge.ts +11 -1
- package/src/intercom/result-intercom.ts +0 -5
- package/src/runs/background/async-execution.ts +186 -74
- package/src/runs/background/async-resume.ts +53 -5
- package/src/runs/background/async-status.ts +4 -1
- package/src/runs/background/chain-append.ts +282 -0
- package/src/runs/background/chain-root-attachment.ts +161 -0
- package/src/runs/background/run-status.ts +2 -7
- package/src/runs/background/subagent-runner.ts +160 -219
- package/src/runs/foreground/chain-execution.ts +62 -58
- package/src/runs/foreground/execution.ts +39 -343
- package/src/runs/foreground/subagent-executor.ts +316 -111
- package/src/runs/shared/acceptance.ts +605 -22
- package/src/runs/shared/chain-outputs.ts +23 -8
- package/src/runs/shared/completion-guard.ts +3 -26
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +38 -0
- package/src/runs/shared/parallel-utils.ts +13 -10
- package/src/runs/shared/pi-args.ts +3 -2
- package/src/runs/shared/subagent-control.ts +8 -11
- package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
- package/src/runs/shared/workflow-graph.ts +2 -6
- package/src/shared/atomic-json.ts +68 -11
- package/src/shared/settings.ts +1 -0
- package/src/shared/types.ts +20 -49
- package/src/shared/utils.ts +2 -8
- package/src/slash/slash-bridge.ts +3 -1
- package/src/slash/slash-commands.ts +1 -1
- package/src/tui/render.ts +14 -29
- package/src/runs/shared/acceptance-contract.ts +0 -318
- package/src/runs/shared/acceptance-evaluation.ts +0 -221
- package/src/runs/shared/acceptance-finalization.ts +0 -173
- package/src/runs/shared/acceptance-reports.ts +0 -127
|
@@ -8,6 +8,11 @@ const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
|
8
8
|
|
|
9
9
|
export class ChainOutputValidationError extends Error {}
|
|
10
10
|
|
|
11
|
+
export interface ChainOutputValidationContext {
|
|
12
|
+
priorOutputNames?: Iterable<string>;
|
|
13
|
+
startStepIndex?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
function outputNamesForStep(step: ChainStep): string[] {
|
|
12
17
|
if (isParallelStep(step)) return step.parallel.map((task) => task.as).filter((name): name is string => Boolean(name));
|
|
13
18
|
if (isDynamicParallelStep(step)) return [step.collect.as];
|
|
@@ -22,27 +27,37 @@ function taskTemplatesForStep(step: ChainStep): string[] {
|
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
validateChainOutputBindingsWithContext(steps, dynamicFanoutConfig);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateChainOutputBindingsWithContext(
|
|
34
|
+
steps: ChainStep[],
|
|
35
|
+
dynamicFanoutConfig: DynamicFanoutConfig = {},
|
|
36
|
+
context: ChainOutputValidationContext = {},
|
|
37
|
+
): void {
|
|
38
|
+
const priorOutputNames = [...(context.priorOutputNames ?? [])];
|
|
39
|
+
const available = new Set<string>(priorOutputNames);
|
|
40
|
+
const seen = new Set<string>(priorOutputNames);
|
|
27
41
|
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
42
|
+
const displayStepIndex = (context.startStepIndex ?? 0) + stepIndex + 1;
|
|
28
43
|
const step = steps[stepIndex]!;
|
|
29
44
|
if (hasDynamicFanoutFields(step)) {
|
|
30
45
|
if (!isDynamicParallelStep(step)) {
|
|
31
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
46
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} requires expand, a single parallel template object, and collect; dynamic expand/collect cannot be mixed with static parallel arrays.`);
|
|
32
47
|
}
|
|
33
48
|
try {
|
|
34
|
-
validateDynamicStepShape(step,
|
|
49
|
+
validateDynamicStepShape(step, displayStepIndex - 1, dynamicFanoutConfig);
|
|
35
50
|
} catch (error) {
|
|
36
51
|
if (error instanceof DynamicFanoutError) throw new ChainOutputValidationError(error.message);
|
|
37
52
|
throw error;
|
|
38
53
|
}
|
|
39
54
|
if (!available.has(step.expand.from.output)) {
|
|
40
|
-
throw new ChainOutputValidationError(`Dynamic chain step ${
|
|
55
|
+
throw new ChainOutputValidationError(`Dynamic chain step ${displayStepIndex} references unknown output '${step.expand.from.output}'. Named outputs are only available after producing step/group completes.`);
|
|
41
56
|
}
|
|
42
57
|
}
|
|
43
58
|
for (const name of outputNamesForStep(step)) {
|
|
44
59
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
45
|
-
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${
|
|
60
|
+
throw new ChainOutputValidationError(`Invalid chain output name '${name}' at step ${displayStepIndex}. Use /^[A-Za-z_][A-Za-z0-9_]*$/.`);
|
|
46
61
|
}
|
|
47
62
|
if (seen.has(name)) {
|
|
48
63
|
throw new ChainOutputValidationError(`Duplicate chain output name '${name}'. Each as name must be unique.`);
|
|
@@ -54,10 +69,10 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
54
69
|
const rawReference = match[0];
|
|
55
70
|
const name = match[1]!;
|
|
56
71
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) {
|
|
57
|
-
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${
|
|
72
|
+
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${displayStepIndex}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
58
73
|
}
|
|
59
74
|
if (!available.has(name)) {
|
|
60
|
-
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${
|
|
75
|
+
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${displayStepIndex}. Named outputs are only available after producing step/group completes.`);
|
|
61
76
|
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
@@ -66,17 +66,6 @@ const READ_ONLY_BUILTIN_TOOLS = new Set([
|
|
|
66
66
|
"contact_supervisor",
|
|
67
67
|
]);
|
|
68
68
|
|
|
69
|
-
export type CompletionPolicy = "none" | "mutation-guard" | "acceptance-contract";
|
|
70
|
-
|
|
71
|
-
interface CompletionPolicyInput {
|
|
72
|
-
agent: string;
|
|
73
|
-
task: string;
|
|
74
|
-
completionGuardEnabled: boolean;
|
|
75
|
-
usesAcceptanceContract: boolean;
|
|
76
|
-
tools?: string[];
|
|
77
|
-
mcpDirectTools?: string[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
69
|
interface CompletionMutationGuardInput {
|
|
81
70
|
agent: string;
|
|
82
71
|
task: string;
|
|
@@ -145,22 +134,10 @@ export function hasMutationToolCall(messages: Message[]): boolean {
|
|
|
145
134
|
return false;
|
|
146
135
|
}
|
|
147
136
|
|
|
148
|
-
export function resolveCompletionPolicy(input: CompletionPolicyInput): CompletionPolicy {
|
|
149
|
-
if (input.usesAcceptanceContract) return "acceptance-contract";
|
|
150
|
-
if (!input.completionGuardEnabled) return "none";
|
|
151
|
-
if (declaresOnlyReadOnlyTools(input.tools, input.mcpDirectTools)) return "none";
|
|
152
|
-
return expectsImplementationMutation(input.agent, input.task) ? "mutation-guard" : "none";
|
|
153
|
-
}
|
|
154
|
-
|
|
155
137
|
export function evaluateCompletionMutationGuard(input: CompletionMutationGuardInput): CompletionMutationGuardResult {
|
|
156
|
-
const expectedMutation =
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
completionGuardEnabled: true,
|
|
160
|
-
usesAcceptanceContract: false,
|
|
161
|
-
tools: input.tools,
|
|
162
|
-
mcpDirectTools: input.mcpDirectTools,
|
|
163
|
-
}) === "mutation-guard";
|
|
138
|
+
const expectedMutation = declaresOnlyReadOnlyTools(input.tools, input.mcpDirectTools)
|
|
139
|
+
? false
|
|
140
|
+
: expectsImplementationMutation(input.agent, input.task);
|
|
164
141
|
const attemptedMutation = hasMutationToolCall(input.messages);
|
|
165
142
|
return {
|
|
166
143
|
expectedMutation,
|
|
@@ -48,7 +48,7 @@ const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outpu
|
|
|
48
48
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
49
49
|
...DYNAMIC_PARALLEL_KEYS,
|
|
50
50
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "maxSubagentDepth",
|
|
51
|
-
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
51
|
+
"structuredOutput", "structuredOutputSchema", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "completionGuard", "systemPrompt",
|
|
52
52
|
"systemPromptMode", "thinking", "modelCandidates", "sessionFile", "effectiveAcceptance",
|
|
53
53
|
]);
|
|
54
54
|
const DYNAMIC_COLLECT_KEYS = new Set(["as", "outputSchema"]);
|
|
@@ -20,6 +20,44 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Sentinel model value requesting that a subagent inherit the parent session's model. */
|
|
24
|
+
export const INHERIT_MODEL = "inherit";
|
|
25
|
+
|
|
26
|
+
/** Minimal shape of the parent session's in-memory model (`ctx.model`). */
|
|
27
|
+
export interface ParentModel {
|
|
28
|
+
provider: string;
|
|
29
|
+
id: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the `--model` override passed to a spawned subagent.
|
|
34
|
+
*
|
|
35
|
+
* When no model is requested (`undefined`, `false`, empty, or the `"inherit"`
|
|
36
|
+
* sentinel), the child must inherit the parent session's *in-memory* model
|
|
37
|
+
* (`provider/id`) instead of being left to resolve its own model. Without an
|
|
38
|
+
* explicit `provider/id`, the child falls back to the global
|
|
39
|
+
* `~/.pi/agent/settings.json` default, which is shared across every open PI
|
|
40
|
+
* session — so a different session that last changed its model in the TUI would
|
|
41
|
+
* silently contaminate this session's subagents (see issue #266). Passing an
|
|
42
|
+
* explicit `provider/id` keeps each session's children isolated to that
|
|
43
|
+
* session's model.
|
|
44
|
+
*
|
|
45
|
+
* An explicitly requested model string is resolved via {@link resolveModelCandidate}.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSubagentModelOverride(
|
|
48
|
+
requestedModel: string | boolean | undefined,
|
|
49
|
+
parentModel: ParentModel | undefined,
|
|
50
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
51
|
+
preferredProvider?: string,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
54
|
+
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
55
|
+
if (explicit === undefined) {
|
|
56
|
+
return parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
57
|
+
}
|
|
58
|
+
return resolveModelCandidate(explicit, availableModels, preferredProvider);
|
|
59
|
+
}
|
|
60
|
+
|
|
23
61
|
export function resolveModelCandidate(
|
|
24
62
|
model: string | undefined,
|
|
25
63
|
availableModels: AvailableModelInfo[] | undefined,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type { DynamicCollectSpec, DynamicExpandSpec } from "../../shared/settings.ts";
|
|
2
|
-
import type { JsonSchemaObject, ResolvedAcceptanceConfig } from "../../shared/types.ts";
|
|
3
|
-
|
|
4
1
|
export interface RunnerSubagentStep {
|
|
5
2
|
agent: string;
|
|
6
3
|
task: string;
|
|
4
|
+
importAsyncRoot?: {
|
|
5
|
+
runId: string;
|
|
6
|
+
asyncDir: string;
|
|
7
|
+
resultPath: string;
|
|
8
|
+
index: number;
|
|
9
|
+
};
|
|
7
10
|
phase?: string;
|
|
8
11
|
label?: string;
|
|
9
12
|
outputName?: string;
|
|
@@ -14,6 +17,7 @@ export interface RunnerSubagentStep {
|
|
|
14
17
|
modelCandidates?: string[];
|
|
15
18
|
tools?: string[];
|
|
16
19
|
extensions?: string[];
|
|
20
|
+
subagentOnlyExtensions?: string[];
|
|
17
21
|
mcpDirectTools?: string[];
|
|
18
22
|
completionGuard?: boolean;
|
|
19
23
|
systemPrompt?: string | null;
|
|
@@ -25,15 +29,13 @@ export interface RunnerSubagentStep {
|
|
|
25
29
|
outputMode?: "inline" | "file-only";
|
|
26
30
|
sessionFile?: string;
|
|
27
31
|
maxSubagentDepth?: number;
|
|
28
|
-
maxExecutionTimeMs?: number;
|
|
29
|
-
maxTokens?: number;
|
|
30
32
|
structuredOutput?: {
|
|
31
|
-
schema: JsonSchemaObject;
|
|
33
|
+
schema: import("../../shared/types.ts").JsonSchemaObject;
|
|
32
34
|
schemaPath: string;
|
|
33
35
|
outputPath: string;
|
|
34
36
|
};
|
|
35
|
-
structuredOutputSchema?: JsonSchemaObject;
|
|
36
|
-
effectiveAcceptance?: ResolvedAcceptanceConfig;
|
|
37
|
+
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
38
|
+
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface ParallelStepGroup {
|
|
@@ -44,13 +46,14 @@ export interface ParallelStepGroup {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export interface DynamicRunnerGroup {
|
|
47
|
-
expand: DynamicExpandSpec;
|
|
49
|
+
expand: import("../../shared/settings.ts").DynamicExpandSpec;
|
|
48
50
|
parallel: RunnerSubagentStep;
|
|
49
|
-
collect: DynamicCollectSpec;
|
|
51
|
+
collect: import("../../shared/settings.ts").DynamicCollectSpec;
|
|
50
52
|
concurrency?: number;
|
|
51
53
|
failFast?: boolean;
|
|
52
54
|
phase?: string;
|
|
53
55
|
label?: string;
|
|
56
|
+
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
@@ -39,6 +39,7 @@ interface BuildPiArgsInput {
|
|
|
39
39
|
inheritSkills: boolean;
|
|
40
40
|
tools?: string[];
|
|
41
41
|
extensions?: string[];
|
|
42
|
+
subagentOnlyExtensions?: string[];
|
|
42
43
|
systemPrompt?: string | null;
|
|
43
44
|
mcpDirectTools?: string[];
|
|
44
45
|
cwd?: string;
|
|
@@ -120,11 +121,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
120
121
|
: [PROMPT_RUNTIME_EXTENSION_PATH];
|
|
121
122
|
if (input.extensions !== undefined) {
|
|
122
123
|
args.push("--no-extensions");
|
|
123
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
|
|
124
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
124
125
|
args.push("--extension", extPath);
|
|
125
126
|
}
|
|
126
127
|
} else {
|
|
127
|
-
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
|
|
128
|
+
for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...(input.subagentOnlyExtensions ?? [])])]) {
|
|
128
129
|
args.push("--extension", extPath);
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -173,9 +173,8 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
173
173
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
: undefined;
|
|
176
|
+
const nudgeMessage = "What are you blocked on? Reply with the smallest next step or ask for a decision.";
|
|
177
|
+
const nudgeCommand = `subagent({ action: "resume", id: "${runTarget}", ${event.index !== undefined ? `index: ${event.index}, ` : ""}message: "${nudgeMessage}" })`;
|
|
179
178
|
if (event.type === "active_long_running") {
|
|
180
179
|
const facts = formatLongRunningFacts(event);
|
|
181
180
|
return [
|
|
@@ -183,10 +182,9 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
183
182
|
`Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
|
|
184
183
|
`Signal: ${event.message}`,
|
|
185
184
|
facts ? `Facts: ${facts}` : undefined,
|
|
186
|
-
"Hint: Inspect status, then nudge if the work seems stuck.",
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
: "Nudge: no child message route registered",
|
|
185
|
+
"Hint: Inspect status, then nudge if the work seems stuck. Live async nudges interrupt the child before sending the follow-up.",
|
|
186
|
+
`Nudge: ${nudgeCommand}`,
|
|
187
|
+
childIntercomTarget ? `Direct intercom target: ${childIntercomTarget}` : undefined,
|
|
190
188
|
`Status: subagent({ action: "status", id: "${runTarget}" })`,
|
|
191
189
|
`Interrupt: subagent({ action: "interrupt", id: "${runTarget}" })`,
|
|
192
190
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
@@ -197,10 +195,9 @@ export function formatControlNoticeMessage(event: ControlEvent, childIntercomTar
|
|
|
197
195
|
`Run: ${runTarget}${event.index !== undefined ? ` step ${event.index + 1}` : ""}`,
|
|
198
196
|
`Signal: ${event.message}`,
|
|
199
197
|
event.recentFailureSummary ? `Recent failures: ${event.recentFailureSummary}` : undefined,
|
|
200
|
-
"Hint: Inspect status first unless the run is clearly blocked.",
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
: "Nudge: no child message route registered",
|
|
198
|
+
"Hint: Inspect status first unless the run is clearly blocked. Live async nudges interrupt the child before sending the follow-up.",
|
|
199
|
+
`Nudge: ${nudgeCommand}`,
|
|
200
|
+
childIntercomTarget ? `Direct intercom target: ${childIntercomTarget}` : undefined,
|
|
204
201
|
`Status: subagent({ action: "status", id: "${runTarget}" })`,
|
|
205
202
|
`Interrupt: subagent({ action: "interrupt", id: "${runTarget}" })`,
|
|
206
203
|
].filter((line): line is string => Boolean(line)).join("\n");
|
|
@@ -135,14 +135,15 @@ function stripAssistantSubagentToolCallBlocks(message: unknown): unknown | undef
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[] {
|
|
138
|
+
const preserveCurrentFanoutToolHistory = process.env[SUBAGENT_FANOUT_CHILD_ENV] === "1";
|
|
138
139
|
let changed = false;
|
|
139
140
|
const filtered: unknown[] = [];
|
|
140
141
|
for (const message of messages) {
|
|
141
|
-
if (isParentOnlySubagentMessage(message) || isSubagentToolResultMessage(message)) {
|
|
142
|
+
if (isParentOnlySubagentMessage(message) || (!preserveCurrentFanoutToolHistory && isSubagentToolResultMessage(message))) {
|
|
142
143
|
changed = true;
|
|
143
144
|
continue;
|
|
144
145
|
}
|
|
145
|
-
const stripped = stripAssistantSubagentToolCallBlocks(message);
|
|
146
|
+
const stripped = preserveCurrentFanoutToolHistory ? message : stripAssistantSubagentToolCallBlocks(message);
|
|
146
147
|
if (stripped === undefined) {
|
|
147
148
|
changed = true;
|
|
148
149
|
continue;
|
|
@@ -5,7 +5,7 @@ export interface WorkflowGraphBuildInput {
|
|
|
5
5
|
runId: string;
|
|
6
6
|
mode?: SubagentRunMode;
|
|
7
7
|
steps: ChainStep[];
|
|
8
|
-
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "
|
|
8
|
+
results?: Array<Pick<SingleResult, "exitCode" | "detached" | "interrupted" | "error" | "acceptance">>;
|
|
9
9
|
currentFlatIndex?: number;
|
|
10
10
|
currentStepIndex?: number;
|
|
11
11
|
stepStatuses?: Array<{ status?: string; error?: string }>;
|
|
@@ -26,8 +26,6 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
26
26
|
return "paused";
|
|
27
27
|
case "detached":
|
|
28
28
|
return "detached";
|
|
29
|
-
case "timed-out":
|
|
30
|
-
return "timed-out";
|
|
31
29
|
case "pending":
|
|
32
30
|
return "pending";
|
|
33
31
|
default:
|
|
@@ -35,10 +33,9 @@ function normalizeStatus(status: string | undefined): WorkflowNodeStatus | undef
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"
|
|
36
|
+
function resultStatus(result: Pick<SingleResult, "exitCode" | "detached" | "interrupted"> | undefined): WorkflowNodeStatus | undefined {
|
|
39
37
|
if (!result) return undefined;
|
|
40
38
|
if (result.detached) return "detached";
|
|
41
|
-
if (result.timedOut) return "timed-out";
|
|
42
39
|
if (result.interrupted) return "paused";
|
|
43
40
|
return result.exitCode === 0 ? "completed" : "failed";
|
|
44
41
|
}
|
|
@@ -66,7 +63,6 @@ function seqLabel(step: SequentialStep, stepIndex: number): string {
|
|
|
66
63
|
function summarizeParallelStatuses(statuses: WorkflowNodeStatus[]): WorkflowNodeStatus {
|
|
67
64
|
if (statuses.some((status) => status === "running")) return "running";
|
|
68
65
|
if (statuses.some((status) => status === "failed")) return "failed";
|
|
69
|
-
if (statuses.some((status) => status === "timed-out")) return "timed-out";
|
|
70
66
|
if (statuses.some((status) => status === "paused")) return "paused";
|
|
71
67
|
if (statuses.some((status) => status === "detached")) return "detached";
|
|
72
68
|
if (statuses.length > 0 && statuses.every((status) => status === "completed")) return "completed";
|
|
@@ -1,16 +1,73 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
4
|
+
type AtomicJsonFs = Pick<typeof fs, "mkdirSync" | "writeFileSync" | "renameSync" | "rmSync">;
|
|
5
|
+
|
|
6
|
+
type AtomicJsonWriterOptions = {
|
|
7
|
+
fs?: AtomicJsonFs;
|
|
8
|
+
now?: () => number;
|
|
9
|
+
pid?: number;
|
|
10
|
+
random?: () => number;
|
|
11
|
+
retryRenameErrors?: boolean;
|
|
12
|
+
retryDelaysMs?: readonly number[];
|
|
13
|
+
wait?: (delayMs: number) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_RENAME_RETRY_DELAYS_MS = [10, 25, 50, 100, 200] as const;
|
|
17
|
+
const RETRYABLE_RENAME_ERROR_CODES = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
18
|
+
|
|
19
|
+
function waitSync(delayMs: number): void {
|
|
20
|
+
const end = Date.now() + delayMs;
|
|
21
|
+
while (Date.now() < end) {
|
|
22
|
+
// writeAtomicJson is synchronous because callers often update status from sync callbacks.
|
|
15
23
|
}
|
|
16
24
|
}
|
|
25
|
+
|
|
26
|
+
function isRetryableRenameError(error: unknown): boolean {
|
|
27
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
28
|
+
return typeof code === "string" && RETRYABLE_RENAME_ERROR_CODES.has(code);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function renameWithRetry(
|
|
32
|
+
fsImpl: AtomicJsonFs,
|
|
33
|
+
sourcePath: string,
|
|
34
|
+
targetPath: string,
|
|
35
|
+
retryDelaysMs: readonly number[],
|
|
36
|
+
wait: (delayMs: number) => void,
|
|
37
|
+
): void {
|
|
38
|
+
for (let attempt = 0; ; attempt++) {
|
|
39
|
+
try {
|
|
40
|
+
fsImpl.renameSync(sourcePath, targetPath);
|
|
41
|
+
return;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const delayMs = retryDelaysMs[attempt];
|
|
44
|
+
if (delayMs === undefined || !isRetryableRenameError(error)) throw error;
|
|
45
|
+
wait(delayMs);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createAtomicJsonWriter(options: AtomicJsonWriterOptions = {}): (filePath: string, payload: object) => void {
|
|
51
|
+
const fsImpl = options.fs ?? fs;
|
|
52
|
+
const now = options.now ?? Date.now;
|
|
53
|
+
const pid = options.pid ?? process.pid;
|
|
54
|
+
const random = options.random ?? Math.random;
|
|
55
|
+
const retryRenameErrors = options.retryRenameErrors ?? process.platform === "win32";
|
|
56
|
+
const retryDelaysMs = retryRenameErrors ? options.retryDelaysMs ?? DEFAULT_RENAME_RETRY_DELAYS_MS : [];
|
|
57
|
+
const wait = options.wait ?? waitSync;
|
|
58
|
+
return (filePath: string, payload: object): void => {
|
|
59
|
+
fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
60
|
+
const tempPath = path.join(
|
|
61
|
+
path.dirname(filePath),
|
|
62
|
+
`.${path.basename(filePath)}.${pid}.${now()}.${random().toString(36).slice(2)}.tmp`,
|
|
63
|
+
);
|
|
64
|
+
try {
|
|
65
|
+
fsImpl.writeFileSync(tempPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
66
|
+
renameWithRetry(fsImpl, tempPath, filePath, retryDelaysMs, wait);
|
|
67
|
+
} finally {
|
|
68
|
+
fsImpl.rmSync(tempPath, { force: true });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const writeAtomicJson = createAtomicJsonWriter();
|
package/src/shared/settings.ts
CHANGED
package/src/shared/types.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ChainOutputMapEntry {
|
|
|
30
30
|
|
|
31
31
|
export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
|
|
32
32
|
|
|
33
|
-
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached"
|
|
33
|
+
export type WorkflowNodeStatus = "pending" | "running" | "completed" | "failed" | "paused" | "detached";
|
|
34
34
|
|
|
35
35
|
export interface WorkflowGraphNode {
|
|
36
36
|
id: string;
|
|
@@ -142,7 +142,7 @@ export interface ControlEvent {
|
|
|
142
142
|
recentFailureSummary?: string;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached"
|
|
145
|
+
export type SubagentResultStatus = "completed" | "failed" | "paused" | "detached";
|
|
146
146
|
export type SubagentRunMode = "single" | "parallel" | "chain";
|
|
147
147
|
|
|
148
148
|
export type PublicNestedStepSummary = Pick<
|
|
@@ -239,7 +239,7 @@ export interface ModelAttempt {
|
|
|
239
239
|
usage?: Usage;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
export type
|
|
242
|
+
export type AcceptanceLevel = "auto" | "none" | "attested" | "checked" | "verified" | "reviewed";
|
|
243
243
|
|
|
244
244
|
export type AcceptanceEvidenceKind =
|
|
245
245
|
| "changed-files"
|
|
@@ -275,15 +275,16 @@ export interface AcceptanceReviewGate {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
export interface AcceptanceConfig {
|
|
278
|
+
level?: AcceptanceLevel;
|
|
278
279
|
criteria?: Array<string | AcceptanceGate>;
|
|
279
280
|
evidence?: AcceptanceEvidenceKind[];
|
|
280
281
|
verify?: AcceptanceVerifyCommand[];
|
|
281
|
-
review?: AcceptanceReviewGate;
|
|
282
|
+
review?: AcceptanceReviewGate | false;
|
|
282
283
|
stopRules?: string[];
|
|
283
|
-
|
|
284
|
+
reason?: string;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
export type AcceptanceInput = AcceptanceConfig;
|
|
287
|
+
export type AcceptanceInput = AcceptanceLevel | false | AcceptanceConfig;
|
|
287
288
|
|
|
288
289
|
export interface ResolvedAcceptanceGate extends AcceptanceGate {
|
|
289
290
|
id: string;
|
|
@@ -293,18 +294,15 @@ export interface ResolvedAcceptanceGate extends AcceptanceGate {
|
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
export interface ResolvedAcceptanceConfig {
|
|
296
|
-
level:
|
|
297
|
+
level: Exclude<AcceptanceLevel, "auto">;
|
|
297
298
|
explicit: boolean;
|
|
298
299
|
inferredReason: string[];
|
|
299
300
|
criteria: ResolvedAcceptanceGate[];
|
|
300
301
|
evidence: AcceptanceEvidenceKind[];
|
|
301
302
|
verify: AcceptanceVerifyCommand[];
|
|
302
|
-
review?: AcceptanceReviewGate;
|
|
303
|
+
review?: AcceptanceReviewGate | false;
|
|
303
304
|
stopRules: string[];
|
|
304
|
-
|
|
305
|
-
mode: "none" | "self-review-loop";
|
|
306
|
-
maxTurns: number;
|
|
307
|
-
};
|
|
305
|
+
reason?: string;
|
|
308
306
|
}
|
|
309
307
|
|
|
310
308
|
export interface AcceptanceReport {
|
|
@@ -368,25 +366,6 @@ export type AcceptanceLedgerStatus =
|
|
|
368
366
|
| "accepted"
|
|
369
367
|
| "rejected";
|
|
370
368
|
|
|
371
|
-
export interface AcceptanceFinalizationTurn {
|
|
372
|
-
turn: number;
|
|
373
|
-
prompt: string;
|
|
374
|
-
status: AcceptanceLedgerStatus;
|
|
375
|
-
rawOutput?: string;
|
|
376
|
-
report?: AcceptanceReport;
|
|
377
|
-
parseError?: string;
|
|
378
|
-
runtimeChecks: AcceptanceRuntimeCheck[];
|
|
379
|
-
verifyRuns: AcceptanceVerifyResult[];
|
|
380
|
-
failureMessage?: string;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
export interface AcceptanceFinalizationLedger {
|
|
384
|
-
mode: "self-review-loop";
|
|
385
|
-
status: "not-run" | "completed" | "failed";
|
|
386
|
-
maxTurns: number;
|
|
387
|
-
turns: AcceptanceFinalizationTurn[];
|
|
388
|
-
}
|
|
389
|
-
|
|
390
369
|
export interface AcceptanceLedger {
|
|
391
370
|
status: AcceptanceLedgerStatus;
|
|
392
371
|
explicit: boolean;
|
|
@@ -395,12 +374,9 @@ export interface AcceptanceLedger {
|
|
|
395
374
|
criteria: ResolvedAcceptanceGate[];
|
|
396
375
|
childReport?: AcceptanceReport;
|
|
397
376
|
childReportParseError?: string;
|
|
398
|
-
initialChildReport?: AcceptanceReport;
|
|
399
|
-
initialChildReportParseError?: string;
|
|
400
377
|
runtimeChecks: AcceptanceRuntimeCheck[];
|
|
401
378
|
verifyRuns: AcceptanceVerifyResult[];
|
|
402
379
|
reviewResult?: AcceptanceReviewResult;
|
|
403
|
-
finalization?: AcceptanceFinalizationLedger;
|
|
404
380
|
parentDecision?: {
|
|
405
381
|
status: "accepted" | "rejected";
|
|
406
382
|
at: string;
|
|
@@ -408,13 +384,6 @@ export interface AcceptanceLedger {
|
|
|
408
384
|
};
|
|
409
385
|
}
|
|
410
386
|
|
|
411
|
-
export interface ResourceLimitExceeded {
|
|
412
|
-
kind: "maxExecutionTimeMs" | "maxTokens";
|
|
413
|
-
limit: number;
|
|
414
|
-
observed?: number;
|
|
415
|
-
message: string;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
387
|
export interface SingleResult {
|
|
419
388
|
agent: string;
|
|
420
389
|
task: string;
|
|
@@ -422,8 +391,6 @@ export interface SingleResult {
|
|
|
422
391
|
detached?: boolean;
|
|
423
392
|
detachedReason?: string;
|
|
424
393
|
interrupted?: boolean;
|
|
425
|
-
timedOut?: boolean;
|
|
426
|
-
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
427
394
|
messages?: Message[];
|
|
428
395
|
usage: Usage;
|
|
429
396
|
model?: string;
|
|
@@ -612,6 +579,7 @@ export interface AsyncStatus {
|
|
|
612
579
|
cwd?: string;
|
|
613
580
|
currentStep?: number;
|
|
614
581
|
chainStepCount?: number;
|
|
582
|
+
pendingAppends?: number;
|
|
615
583
|
parallelGroups?: AsyncParallelGroupStatus[];
|
|
616
584
|
workflowGraph?: WorkflowGraphSnapshot;
|
|
617
585
|
steps?: Array<{
|
|
@@ -648,7 +616,6 @@ export interface AsyncStatus {
|
|
|
648
616
|
structuredOutputPath?: string;
|
|
649
617
|
structuredOutputSchemaPath?: string;
|
|
650
618
|
acceptance?: AcceptanceLedger;
|
|
651
|
-
resourceLimitExceeded?: ResourceLimitExceeded;
|
|
652
619
|
}>;
|
|
653
620
|
sessionDir?: string;
|
|
654
621
|
outputFile?: string;
|
|
@@ -790,8 +757,6 @@ export interface RunSyncOptions {
|
|
|
790
757
|
cwd?: string;
|
|
791
758
|
signal?: AbortSignal;
|
|
792
759
|
interruptSignal?: AbortSignal;
|
|
793
|
-
timeoutMs?: number;
|
|
794
|
-
timeoutAt?: number;
|
|
795
760
|
allowIntercomDetach?: boolean;
|
|
796
761
|
intercomEvents?: IntercomEventBus;
|
|
797
762
|
onUpdate?: (r: import("@earendil-works/pi-agent-core").AgentToolResult<Details>) => void;
|
|
@@ -810,8 +775,6 @@ export interface RunSyncOptions {
|
|
|
810
775
|
outputPath?: string;
|
|
811
776
|
outputMode?: OutputMode;
|
|
812
777
|
maxSubagentDepth?: number;
|
|
813
|
-
maxExecutionTimeMs?: number;
|
|
814
|
-
maxTokens?: number;
|
|
815
778
|
nestedRoute?: NestedRouteInfo;
|
|
816
779
|
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
817
780
|
modelOverride?: string;
|
|
@@ -853,6 +816,13 @@ interface ExtensionChainConfig {
|
|
|
853
816
|
};
|
|
854
817
|
}
|
|
855
818
|
|
|
819
|
+
export interface ProactiveSkillSubagentsConfig {
|
|
820
|
+
enabled?: boolean;
|
|
821
|
+
minReferences?: number;
|
|
822
|
+
maxRecommendations?: number;
|
|
823
|
+
preferredAgent?: string;
|
|
824
|
+
}
|
|
825
|
+
|
|
856
826
|
export interface ExtensionConfig {
|
|
857
827
|
asyncByDefault?: boolean;
|
|
858
828
|
forceTopLevelAsync?: boolean;
|
|
@@ -864,6 +834,7 @@ export interface ExtensionConfig {
|
|
|
864
834
|
worktreeSetupHook?: string;
|
|
865
835
|
worktreeSetupHookTimeoutMs?: number;
|
|
866
836
|
intercomBridge?: IntercomBridgeConfig;
|
|
837
|
+
proactiveSkillSubagents?: ProactiveSkillSubagentsConfig | false;
|
|
867
838
|
}
|
|
868
839
|
|
|
869
840
|
// ============================================================================
|
|
@@ -954,7 +925,7 @@ export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
|
954
925
|
export const POLL_INTERVAL_MS = 250;
|
|
955
926
|
export const MAX_WIDGET_JOBS = 4;
|
|
956
927
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
957
|
-
export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "resume", "doctor"] as const;
|
|
928
|
+
export const SUBAGENT_ACTIONS = ["list", "get", "create", "update", "delete", "status", "interrupt", "resume", "append-step", "doctor"] as const;
|
|
958
929
|
|
|
959
930
|
export const DEFAULT_FORK_PREAMBLE =
|
|
960
931
|
"You are a delegated subagent running from a fork of the parent session. " +
|
package/src/shared/utils.ts
CHANGED
|
@@ -192,7 +192,8 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
192
192
|
const hasAssistantError = ("errorMessage" in msg && typeof msg.errorMessage === "string" && msg.errorMessage.length > 0)
|
|
193
193
|
|| ("stopReason" in msg && msg.stopReason === "error");
|
|
194
194
|
if (hasAssistantError) continue;
|
|
195
|
-
for (
|
|
195
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
196
|
+
const part = msg.content[j];
|
|
196
197
|
if (part.type === "text" && part.text.trim().length > 0) return part.text;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
@@ -204,13 +205,6 @@ export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" |
|
|
|
204
205
|
return result.finalOutput ?? getFinalOutput(result.messages ?? []);
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
export function formatResourceLimitExceeded(input: { agent: string; kind: "maxExecutionTimeMs" | "maxTokens"; limit: number; observed?: number }): string {
|
|
208
|
-
if (input.kind === "maxExecutionTimeMs") {
|
|
209
|
-
return `Resource limit exceeded for ${input.agent}: maxExecutionTimeMs ${input.limit}ms.`;
|
|
210
|
-
}
|
|
211
|
-
return `Resource limit exceeded for ${input.agent}: maxTokens ${input.limit}${input.observed !== undefined ? ` (observed ${input.observed})` : ""}.`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
208
|
/**
|
|
215
209
|
* Extract display items (text and tool calls) from messages
|
|
216
210
|
*/
|