@selesai/code 0.5.29 → 0.6.1
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 +24 -0
- package/README.md +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +18 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/system-prompt.test.d.ts +2 -0
- package/dist/core/system-prompt.test.d.ts.map +1 -0
- package/dist/core/system-prompt.test.js +89 -0
- package/dist/core/system-prompt.test.js.map +1 -0
- package/dist/defaults/models.json +13 -45
- package/dist/defaults/settings.json +1 -2
- package/dist/extensions/copy-turn.test.ts +131 -0
- package/dist/extensions/copy-turn.ts +6 -1
- package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/dist/extensions/package.json +0 -1
- package/dist/extensions/pi-subagents/CHANGELOG.md +3 -0
- package/dist/extensions/pi-subagents/README.md +27 -32
- package/dist/extensions/pi-subagents/agents/architect.md +4 -4
- package/dist/extensions/pi-subagents/agents/builder.md +5 -4
- package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
- package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
- package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
- package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +10 -9
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +12 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +10 -11
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +56 -9
- package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +1 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +24 -7
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +23 -5
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +27 -1
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +64 -6
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +16 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +72 -18
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +19 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +127 -31
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +4 -6
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +41 -2
- package/dist/extensions/pi-subagents/src/shared/utils.ts +29 -1
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -1
- package/dist/extensions/pi-subagents/src/tui/render.ts +28 -6
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +111 -6
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +74 -43
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +36 -21
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +5 -3
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -8
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +81 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +49 -10
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
- package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +29 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
- package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +23 -1
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +60 -9
- package/dist/skills/ponytail/SKILL.md +1 -3
- package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
- package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
- package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
- package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
- package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
- package/package.json +2 -2
- package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
- package/dist/extensions/caveman/index.js +0 -118
- package/dist/extensions/caveman/package.json +0 -8
- package/dist/extensions/caveman/test/extension.test.js +0 -203
- package/dist/extensions/caveman/test/helpers.test.js +0 -58
- package/dist/skills/caveman/SKILL.md +0 -50
|
@@ -28,13 +28,14 @@ import { discoverAvailableSkills, resolveSkills } from "./skills.ts";
|
|
|
28
28
|
import {
|
|
29
29
|
buildProactiveSkillSubagentRecommendationLines,
|
|
30
30
|
} from "./proactive-skills.ts";
|
|
31
|
+
import { formatTaskAwareAgentRecommendation, recommendTaskAwareAgent } from "./task-aware-routing.ts";
|
|
31
32
|
import { parseFrontmatter } from "./frontmatter.ts";
|
|
32
33
|
import { toModelInfo } from "../shared/model-info.ts";
|
|
33
34
|
import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
|
|
34
35
|
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
|
|
35
36
|
import { resolveTurnBudgetConfig } from "../runs/shared/turn-budget.ts";
|
|
36
37
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
37
|
-
import type { AcceptanceInput, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
|
|
38
|
+
import type { AcceptanceInput, CatalogAgentMetadata, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
|
|
38
39
|
import { getProjectConfigDir } from "../shared/utils.ts";
|
|
39
40
|
import { capabilityCeilingAgentRestrictionSources, isAgentAllowedByCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
40
41
|
|
|
@@ -48,10 +49,12 @@ interface ManagementParams {
|
|
|
48
49
|
chainName?: string;
|
|
49
50
|
agentScope?: string;
|
|
50
51
|
config?: unknown;
|
|
52
|
+
/** Optional advisory intent for action:'list' only; never launches work. */
|
|
53
|
+
task?: string;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
function result(text: string, isError = false): AgentToolResult<Details> {
|
|
54
|
-
return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [] } };
|
|
56
|
+
function result(text: string, isError = false, extraDetails: Partial<Details> = {}): AgentToolResult<Details> {
|
|
57
|
+
return { content: [{ type: "text", text }], isError, details: { mode: "management", results: [], ...extraDetails } };
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function parseCsv(value: string): string[] {
|
|
@@ -585,8 +588,13 @@ function renamePath(
|
|
|
585
588
|
return { filePath };
|
|
586
589
|
}
|
|
587
590
|
|
|
591
|
+
/** Effective declared tools: normal tools plus mcp:-prefixed direct MCP tools. */
|
|
592
|
+
function effectiveAgentTools(agent: AgentConfig): string[] {
|
|
593
|
+
return [...(agent.tools ?? []), ...(agent.mcpDirectTools ?? []).map((t) => `mcp:${t}`)];
|
|
594
|
+
}
|
|
595
|
+
|
|
588
596
|
function formatAgentDetail(agent: AgentConfig): string {
|
|
589
|
-
const tools =
|
|
597
|
+
const tools = effectiveAgentTools(agent);
|
|
590
598
|
const lines: string[] = [`Agent: ${agent.name} (${agent.source})`, `Path: ${agent.filePath}`, `Description: ${agent.description}`];
|
|
591
599
|
if (agent.packageName) {
|
|
592
600
|
lines.push(`Local name: ${frontmatterNameForConfig(agent)}`);
|
|
@@ -672,6 +680,33 @@ function formatChainDetail(chain: ChainConfig): string {
|
|
|
672
680
|
return lines.join("\n");
|
|
673
681
|
}
|
|
674
682
|
|
|
683
|
+
function formatCatalogAgentLine(agent: AgentConfig): string {
|
|
684
|
+
const parts: string[] = [agent.source, `context: ${agent.defaultContext ?? "fresh"}`];
|
|
685
|
+
if (agent.acceptanceRole) parts.push(`role: ${agent.acceptanceRole}`);
|
|
686
|
+
if (agent.aliases?.length) parts.push(`aliases: ${agent.aliases.join(", ")}`);
|
|
687
|
+
const tools = effectiveAgentTools(agent);
|
|
688
|
+
if (tools.length) parts.push(`tools: ${tools.join(", ")}`);
|
|
689
|
+
return `- ${agent.name} (${parts.join(", ")}): ${agent.description}`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function catalogAgentMetadata(
|
|
693
|
+
agent: AgentConfig,
|
|
694
|
+
executable: boolean,
|
|
695
|
+
restrictionSources: string[] | undefined,
|
|
696
|
+
): CatalogAgentMetadata {
|
|
697
|
+
return {
|
|
698
|
+
name: agent.name,
|
|
699
|
+
source: agent.source,
|
|
700
|
+
description: agent.description,
|
|
701
|
+
executable,
|
|
702
|
+
...(restrictionSources?.length ? { restrictionSources } : {}),
|
|
703
|
+
...(agent.aliases?.length ? { aliases: [...agent.aliases] } : {}),
|
|
704
|
+
defaultContext: agent.defaultContext ?? "fresh",
|
|
705
|
+
...(agent.acceptanceRole ? { acceptanceRole: agent.acceptanceRole } : {}),
|
|
706
|
+
tools: effectiveAgentTools(agent),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
675
710
|
export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
|
|
676
711
|
const scope = normalizeListScope(params.agentScope) ?? "both";
|
|
677
712
|
const d = discoverAgentsAll(ctx.cwd);
|
|
@@ -690,23 +725,35 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
690
725
|
config: ctx.config?.proactiveSkillSubagents,
|
|
691
726
|
discoverAvailableSkills: () => discoverAvailableSkills(ctx.cwd),
|
|
692
727
|
});
|
|
728
|
+
const taskAdvice = params.task?.trim()
|
|
729
|
+
? formatTaskAwareAgentRecommendation(recommendTaskAwareAgent({ task: params.task, agents, capabilityCeiling }))
|
|
730
|
+
: [];
|
|
693
731
|
const lines = [
|
|
694
732
|
"Executable agents:",
|
|
695
|
-
...(agents.length
|
|
696
|
-
? agents.map((a) => `- ${a.name} (${a.source}${a.defaultContext ? `, context: ${a.defaultContext}` : ""}${a.aliases?.length ? `, aliases: ${a.aliases.join(", ")}` : ""}): ${a.description}`)
|
|
697
|
-
: ["- (none)"]),
|
|
733
|
+
...(agents.length ? agents.map(formatCatalogAgentLine) : ["- (none)"]),
|
|
698
734
|
...(restrictedAgents.length ? [
|
|
699
735
|
"",
|
|
700
736
|
`Restricted agents (not executable in this session${restrictedSources?.length ? `; capability ceiling: ${restrictedSources.join(", ")}` : ""}):`,
|
|
701
|
-
...restrictedAgents.map(
|
|
737
|
+
...restrictedAgents.map(formatCatalogAgentLine),
|
|
702
738
|
] : []),
|
|
703
739
|
"",
|
|
704
740
|
"Chains:",
|
|
705
741
|
...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"]),
|
|
706
742
|
...(proactiveSuggestions.length ? ["", ...proactiveSuggestions] : []),
|
|
743
|
+
...(taskAdvice.length ? ["", ...taskAdvice] : []),
|
|
707
744
|
...(diagnostics.length ? ["", "Chain diagnostics:", ...diagnostics.map((entry) => `- ${entry.filePath}: ${entry.error}`)] : []),
|
|
708
745
|
];
|
|
709
|
-
return result(lines.join("\n")
|
|
746
|
+
return result(lines.join("\n"), false, {
|
|
747
|
+
catalog: {
|
|
748
|
+
version: 1,
|
|
749
|
+
agents: [
|
|
750
|
+
...agents.map((a) => catalogAgentMetadata(a, true, undefined)),
|
|
751
|
+
...restrictedAgents.map((a) => catalogAgentMetadata(a, false, restrictedSources)),
|
|
752
|
+
],
|
|
753
|
+
chains: chains.map((c) => ({ name: c.name, source: c.source, description: c.description })),
|
|
754
|
+
...(restrictedSources?.length ? { capabilityCeilingSources: restrictedSources } : {}),
|
|
755
|
+
},
|
|
756
|
+
});
|
|
710
757
|
}
|
|
711
758
|
|
|
712
759
|
function formatModelSource(agent: AgentConfig, currentModel: ParentModel | undefined): string {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure task-aware advisory agent recommender.
|
|
3
|
+
*
|
|
4
|
+
* Given an already-discovered, effective agent set and the current capability
|
|
5
|
+
* ceiling, deterministically selects at most one canonical agent to recommend
|
|
6
|
+
* for a task, or returns recovery guidance when intent is unknown or no safe
|
|
7
|
+
* candidate exists. This module is deliberately pure: it performs no
|
|
8
|
+
* filesystem/discovery access, no alias/chains/executor/RPC/preflight/settings
|
|
9
|
+
* access, no mutation, launch, scheduling, or persistence, and never writes to
|
|
10
|
+
* params. Launching stays explicit: the caller must make a separate execution
|
|
11
|
+
* call with the recommended canonical `agent.name`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AgentConfig, AgentSource } from "./agents.ts";
|
|
15
|
+
import { agentHasWriteTools } from "./agent-memory.ts";
|
|
16
|
+
import { classifyTaskMutationIntent, resolveAgentRoutingRole } from "../runs/shared/task-intent.ts";
|
|
17
|
+
import { isAgentAllowedByCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
18
|
+
|
|
19
|
+
/** Core tools that make an implementation agent able to write (matches agent-memory). */
|
|
20
|
+
const WRITER_TOOLS = new Set(["edit", "write", "bash"]);
|
|
21
|
+
|
|
22
|
+
/** Source precedence for deterministic ordering: project > user > package > builtin. */
|
|
23
|
+
const AGENT_SOURCE_PRECEDENCE: Record<AgentSource, number> = { builtin: 0, package: 1, user: 2, project: 3 };
|
|
24
|
+
|
|
25
|
+
export type TaskAwareAgentIntent = "implementation" | "read-only" | "unknown";
|
|
26
|
+
|
|
27
|
+
export interface TaskAwareAgentRecommendationAgent {
|
|
28
|
+
/** Canonical runtime agent name accepted by execution; never an alias. */
|
|
29
|
+
name: string;
|
|
30
|
+
source: AgentSource;
|
|
31
|
+
role: "writer" | "read-only";
|
|
32
|
+
roleBasis: "declared" | "inferred";
|
|
33
|
+
reason: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TaskAwareAgentRecommendation {
|
|
37
|
+
intent: TaskAwareAgentIntent;
|
|
38
|
+
agent?: TaskAwareAgentRecommendationAgent;
|
|
39
|
+
/** Recovery guidance when no safe recommendation exists (unknown intent or no candidate). */
|
|
40
|
+
next?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Recommend a canonical agent for a trimmed non-empty task, or `undefined` for
|
|
45
|
+
* an empty/whitespace task (no-op). Never recommends disabled or
|
|
46
|
+
* capability-disallowed agents, never guesses for unknown intent, and never
|
|
47
|
+
* outputs an alias or a role-incompatible agent.
|
|
48
|
+
*/
|
|
49
|
+
export function recommendTaskAwareAgent(input: {
|
|
50
|
+
task: string;
|
|
51
|
+
agents: AgentConfig[];
|
|
52
|
+
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
53
|
+
}): TaskAwareAgentRecommendation | undefined {
|
|
54
|
+
const task = input.task.trim();
|
|
55
|
+
if (!task) return undefined;
|
|
56
|
+
|
|
57
|
+
const intent = classifyTaskMutationIntent("builder", task).kind;
|
|
58
|
+
if (intent === "unknown") {
|
|
59
|
+
return {
|
|
60
|
+
intent,
|
|
61
|
+
next: "Clarify whether the task is read-only analysis/review or implementation allowed to edit files.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const candidates: TaskAwareAgentRecommendationAgent[] = [];
|
|
66
|
+
for (const agent of input.agents) {
|
|
67
|
+
if (agent.disabled) continue;
|
|
68
|
+
if (!isAgentAllowedByCapabilityCeiling(agent.name, input.capabilityCeiling)) continue;
|
|
69
|
+
const role = resolveAgentRoutingRole(agent.name, agent.acceptanceRole);
|
|
70
|
+
if (!role) continue;
|
|
71
|
+
const hasWriteTools = agentHasWriteTools(agent);
|
|
72
|
+
if (intent === "implementation") {
|
|
73
|
+
if (role !== "writer" || !hasWriteTools) continue;
|
|
74
|
+
const allowedTools = input.capabilityCeiling?.allowedTools;
|
|
75
|
+
if (allowedTools !== undefined && !allowedTools.some((tool) => WRITER_TOOLS.has(tool))) continue;
|
|
76
|
+
} else {
|
|
77
|
+
if (role !== "read-only" || hasWriteTools) continue;
|
|
78
|
+
}
|
|
79
|
+
const roleBasis = agent.acceptanceRole !== undefined ? "declared" : "inferred";
|
|
80
|
+
candidates.push({
|
|
81
|
+
name: agent.name,
|
|
82
|
+
source: agent.source,
|
|
83
|
+
role,
|
|
84
|
+
roleBasis,
|
|
85
|
+
reason: intent === "implementation"
|
|
86
|
+
? `${roleBasis} writer role with write tools`
|
|
87
|
+
: `${roleBasis} read-only role without known write tools`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
candidates.sort((a, b) => {
|
|
92
|
+
const sourceOrder = AGENT_SOURCE_PRECEDENCE[b.source] - AGENT_SOURCE_PRECEDENCE[a.source];
|
|
93
|
+
if (sourceOrder !== 0) return sourceOrder;
|
|
94
|
+
const declaredOrder = (a.roleBasis === "declared" ? 0 : 1) - (b.roleBasis === "declared" ? 0 : 1);
|
|
95
|
+
if (declaredOrder !== 0) return declaredOrder;
|
|
96
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const agent = candidates[0];
|
|
100
|
+
if (!agent) {
|
|
101
|
+
return {
|
|
102
|
+
intent,
|
|
103
|
+
next: intent === "implementation"
|
|
104
|
+
? "No executable agent has a writer role with write tools for this task. Check the executable/restricted sections above or adjust the capability ceiling."
|
|
105
|
+
: "No executable agent has a read-only role without known write tools for this task. Check the executable/restricted sections above.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { intent, agent };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Text-only advisory lines; `undefined` (empty-task no-op) renders nothing. */
|
|
112
|
+
export function formatTaskAwareAgentRecommendation(recommendation: TaskAwareAgentRecommendation | undefined): string[] {
|
|
113
|
+
if (!recommendation) return [];
|
|
114
|
+
const lines = ["Task-aware advisory routing:", `- Intent: ${recommendation.intent}`];
|
|
115
|
+
if (recommendation.agent) {
|
|
116
|
+
lines.push(`- Recommended: ${recommendation.agent.name} (${recommendation.agent.source})`);
|
|
117
|
+
lines.push(`- Reason: ${recommendation.agent.reason}`);
|
|
118
|
+
lines.push("- Advisory only: no subagent was launched. To proceed, explicitly call subagent with this canonical agent name and the task.");
|
|
119
|
+
} else {
|
|
120
|
+
lines.push("- Recommendation: none");
|
|
121
|
+
lines.push(`- Next: ${recommendation.next ?? "Refine the task wording and retry."}`);
|
|
122
|
+
lines.push("- Advisory only: no subagent was launched.");
|
|
123
|
+
}
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
@@ -284,7 +284,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
284
284
|
diagnostics.push({ code: "denied_required_tool", severity: "error", message });
|
|
285
285
|
return { ok: false, code: "denied_required_tool", message, diagnostics };
|
|
286
286
|
}
|
|
287
|
-
const artifactsEnabled = input.artifacts
|
|
287
|
+
const artifactsEnabled = input.artifacts === true;
|
|
288
288
|
const artifactsDir = artifactsEnabled ? getArtifactsDir(input.parentSessionFile ?? null, effectiveCwd, input.artifactDir ?? "project") : undefined;
|
|
289
289
|
const artifactPaths = artifactsDir ? getArtifactPaths(artifactsDir, runId, agent.name, 0) : undefined;
|
|
290
290
|
const outputPath = resolveSingleOutputPath(behavior.output, effectiveCwd, effectiveCwd, artifactsDir ? path.join(artifactsDir, "outputs", runId) : undefined);
|
|
@@ -51,7 +51,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
|
|
|
51
51
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
52
52
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
53
53
|
import { loadConfig } from "./config.ts";
|
|
54
|
-
import { buildSubagentToolDescription } from "./tool-description.ts";
|
|
54
|
+
import { buildSubagentToolDescription, SUBAGENT_PARENT_ROUTING_GUIDANCE } from "./tool-description.ts";
|
|
55
55
|
import {
|
|
56
56
|
type Details,
|
|
57
57
|
type SubagentState,
|
|
@@ -405,6 +405,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
405
405
|
name: "subagent",
|
|
406
406
|
label: "Subagent",
|
|
407
407
|
description: buildSubagentToolDescription(config),
|
|
408
|
+
// Always-visible active-tool path: rendered into the main system prompt by
|
|
409
|
+
// buildSystemPrompt (default and custom-prompt branches). Parent-only; the
|
|
410
|
+
// child-safe fanout registration must not receive this guidance.
|
|
411
|
+
promptGuidelines: [SUBAGENT_PARENT_ROUTING_GUIDANCE],
|
|
408
412
|
parameters: SubagentParams,
|
|
409
413
|
|
|
410
414
|
prepareArguments(args) {
|
|
@@ -262,7 +262,7 @@ const ControlOverrides = Type.Object({
|
|
|
262
262
|
|
|
263
263
|
const SubagentParamsSchema = Type.Object({
|
|
264
264
|
agent: Type.Optional(Type.String({ description: "Agent name (SINGLE mode) or target for management get/update/delete" })),
|
|
265
|
-
task: Type.Optional(Type.String({ description: "Task
|
|
265
|
+
task: Type.Optional(Type.String({ description: "Task for SINGLE-mode execution, or an optional advisory intent for action:'list' (list only appends a task-aware recommendation and never launches work; explicitly call subagent with the recommended canonical agent name and the task to execute)" })),
|
|
266
266
|
// Management action (when present, tool operates in management mode)
|
|
267
267
|
action: Type.Optional(Type.String({
|
|
268
268
|
description: "Optional management/control action. Omit this field entirely for execution/delegation ({agent, task}, {tasks}, or {chain}); use it only for management/control actions."
|
|
@@ -321,7 +321,7 @@ const SubagentParamsSchema = Type.Object({
|
|
|
321
321
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
322
322
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
323
323
|
cwd: Type.Optional(Type.String()),
|
|
324
|
-
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default:
|
|
324
|
+
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: false)" })),
|
|
325
325
|
includeProgress: Type.Optional(Type.Boolean({ description: "Include full progress in result (default: false)" })),
|
|
326
326
|
share: Type.Optional(Type.Boolean({ description: "Upload session to GitHub Gist for sharing (default: false)" })),
|
|
327
327
|
sessionDir: Type.Optional(
|
|
@@ -6,13 +6,27 @@ 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
|
+
/**
|
|
10
|
+
* Generic parent-only delegation routing contract. Deliberately contains no
|
|
11
|
+
* bundled agent names: agents are selected from runtime catalog metadata only.
|
|
12
|
+
* Attached to the parent `subagent` tool as promptGuidelines and embedded in
|
|
13
|
+
* the mandatory safety guidance so custom descriptions cannot remove it.
|
|
14
|
+
*/
|
|
15
|
+
export const SUBAGENT_PARENT_ROUTING_GUIDANCE = `PARENT-ONLY SUBAGENT ROUTING:
|
|
16
|
+
• Before any execution, call { action: "list" } and select only an executable entry using its current role, context, and tool metadata.
|
|
17
|
+
• Keep tiny targeted reads and simple answers local with the parent.
|
|
18
|
+
• Send broad local investigation, external research, and mutation/implementation work to a capable delegated agent.
|
|
19
|
+
• The parent remains the decision-maker and normally the sole writer.`;
|
|
20
|
+
|
|
9
21
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
22
|
• Use { action: "list" } before execution and only run executable/non-disabled agents or chains.
|
|
11
23
|
• Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/grant-spawn-budget/interrupt/stop/resume/steer/append-step/approve-checkpoint/reject-checkpoint/doctor.
|
|
12
24
|
• Async/background runs: launch with async:true only when work can proceed independently. Do not sleep or poll status just to wait. In an interactive session, normally return control and let Pi wake you; do not call subagent_wait merely to wait. Override that default and call subagent_wait when the current request is run-to-completion — for example, the user asked you to report results back before continuing or a skill must finish in one turn. Headless sessions auto-drain current-session work at agent_end; use subagent_wait when this turn must receive results before it ends.
|
|
13
25
|
• Child-safety boundary: ordinary child subagents are not orchestrators and must not run subagents. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
26
|
• Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
|
|
15
|
-
• Artifacts/status essentials: chain outputs live under {chain_dir}; 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
|
|
27
|
+
• Artifacts/status essentials: chain outputs live under {chain_dir}; async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Normal delegated results are reference-first: completion returns saved-output references plus lifecycle/status information, not child prose — inspect full output through the saved output path, async status/transcript, or resume. Include output paths and residual risks when reporting results.
|
|
28
|
+
|
|
29
|
+
${SUBAGENT_PARENT_ROUTING_GUIDANCE}`;
|
|
16
30
|
|
|
17
31
|
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `To delegate work, call with { agent, task }, { tasks }, or { chain }; omit action. Use action only for management/control actions listed below.
|
|
18
32
|
|
|
@@ -37,8 +51,9 @@ CHAIN EXAMPLES (quick reference for the nested schema):
|
|
|
37
51
|
• Parallel fan-out: { chain: [{parallel: [{agent:"agent-a", task:"Check part of {task}", count: 3}]}] }
|
|
38
52
|
• 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
53
|
|
|
40
|
-
MANAGEMENT (use action field
|
|
54
|
+
MANAGEMENT (use action field; omit agent/chain/tasks — the sole field exception is list's optional advisory task):
|
|
41
55
|
• { action: "list" } - discover executable agents/chains
|
|
56
|
+
• { action: "list", task: "..." } - same, plus optional task-aware advisory routing: recommends one canonical agent (implementation or read-only) for the task, or explains why none is safe. It only recommends and never launches; to proceed, explicitly call subagent with the recommended canonical agent name and the task.
|
|
42
57
|
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
43
58
|
• { action: "models", agent?: "name" } - show the runtime-loaded builtin subagent model mapping, optionally filtered to one builtin
|
|
44
59
|
• { action: "watchdog.status" | "watchdog.check" | "watchdog.recommend-model" } - inspect the opt-in subagent watchdog and its strong complementary model recommendation
|
|
@@ -47,10 +62,10 @@ MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
|
47
62
|
• 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
63
|
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
49
64
|
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
50
|
-
• { action: "eject", agent: "
|
|
51
|
-
• { action: "disable", agent: "
|
|
52
|
-
• { action: "enable", agent: "
|
|
53
|
-
• { action: "reset", agent: "
|
|
65
|
+
• { action: "eject", agent: "agent-name", agentScope?: "user" | "project" } - copy a bundled/package agent to user/project scope as an editable custom file that shadows the original (default scope: user)
|
|
66
|
+
• { action: "disable", agent: "agent-name", agentScope?: "user" | "project" } - hide any agent from runtime discovery via a reversible settings override (default scope: user)
|
|
67
|
+
• { action: "enable", agent: "agent-name", agentScope?: "user" | "project" } - remove a disabled override and restore discovery
|
|
68
|
+
• { action: "reset", agent: "agent-name", agentScope?: "user" | "project" } - delete the scope's custom agent file and/or settings override, restoring the bundled default
|
|
54
69
|
• { 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
70
|
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
56
71
|
|
|
@@ -88,7 +103,8 @@ EXECUTE:
|
|
|
88
103
|
• If list shows proactive skill subagent suggestions, use a small fresh-context fanout only when the task is broad enough.
|
|
89
104
|
|
|
90
105
|
MANAGE / CONTROL:
|
|
91
|
-
• Use action without execution fields: list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
|
|
106
|
+
• Use action without execution fields (list may add an optional advisory task): list, get, models, create, update, delete, eject, disable, enable, reset, grant-spawn-budget, doctor, watchdog.status, watchdog.check, watchdog.recommend-model, watchdog.configure.
|
|
107
|
+
• { action: "list", task: "..." } optionally appends task-aware advisory routing that recommends a canonical agent but never launches; execute the recommended agent explicitly in a separate call.
|
|
92
108
|
• Agent acceptanceRole (read-only or writer) affects inferred acceptance only, never tools. Explicit task intent wins; omission keeps name heuristics. Update with false or an empty string to clear it.
|
|
93
109
|
• Async control actions: status, interrupt, stop, resume, steer, append-step, approve-checkpoint, reject-checkpoint. Use stop with an id for current-session top-level async runs. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, steer for acknowledged top-level live async guidance, and resume for paused/completed/failed revival or a routed nested follow-up. Stopped runs are non-resumable. Steering delivery means Pi accepted the correlated user input, not model compliance; use index for a specific child.
|
|
94
110
|
• Opt-in schedule actions: schedule, schedule-list, schedule-status, schedule-cancel. Schedule only explicit delayed runs the user asked for.
|
|
@@ -98,6 +114,7 @@ ASYNC / WAIT:
|
|
|
98
114
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, session files, and { action:"status", id:"..." }.
|
|
99
115
|
|
|
100
116
|
SAFETY:
|
|
117
|
+
• Parent-only routing: call { action: "list" } before any execution and select only an executable entry using its current role, context, and tool metadata. Keep tiny targeted reads and simple answers local; send broad local investigation, external research, and mutation/implementation work to a capable delegated agent. The parent remains the decision-maker and normally the sole writer.
|
|
101
118
|
• 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
119
|
• 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.`;
|
|
103
120
|
|
|
@@ -686,9 +686,19 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
686
686
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
687
687
|
if (behavior.progress) progressInstructionCreated = true;
|
|
688
688
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
689
|
-
const
|
|
689
|
+
const explicitOutput = typeof behavior.output === "string" ? behavior.output : undefined;
|
|
690
|
+
// Omitted output generates a collision-safe per-child durable path
|
|
691
|
+
// `<asyncDir>/outputs/<flat-index>-<agent>.md`; omitted outputMode resolves
|
|
692
|
+
// to file-only whenever an output path is active. Explicit paths,
|
|
693
|
+
// `output: false`, and explicit `outputMode: "inline"` are preserved.
|
|
694
|
+
const outputPath = explicitOutput
|
|
695
|
+
? resolveSingleOutputPath(explicitOutput, ctx.cwd, instructionCwd, outputBaseDir)
|
|
696
|
+
: behavior.output === false && s.output !== false && s.output !== "false" && flatIndex !== undefined
|
|
697
|
+
? path.join(asyncDir, "outputs", `${flatIndex}-${s.agent}.md`)
|
|
698
|
+
: undefined;
|
|
690
699
|
if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
|
|
691
|
-
const
|
|
700
|
+
const effectiveOutputMode = s.outputMode ?? (outputPath ? "file-only" : "inline");
|
|
701
|
+
const validationError = validateFileOnlyOutputMode(effectiveOutputMode, outputPath, `Async step (${s.agent})`);
|
|
692
702
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
693
703
|
let taskTemplate = s.task ?? "{previous}";
|
|
694
704
|
taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
|
|
@@ -751,7 +761,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
751
761
|
skills: resolvedSkills.map((r) => r.name),
|
|
752
762
|
outputPath,
|
|
753
763
|
...(namespaceOutputPath ? { namespaceOutputPath: true } : {}),
|
|
754
|
-
outputMode:
|
|
764
|
+
outputMode: effectiveOutputMode,
|
|
755
765
|
sessionFile,
|
|
756
766
|
maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
|
|
757
767
|
waitToolEnabled: params.waitToolEnabled,
|
|
@@ -1228,9 +1238,17 @@ export function executeAsyncSingle(
|
|
|
1228
1238
|
}
|
|
1229
1239
|
|
|
1230
1240
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1231
|
-
|
|
1241
|
+
// Omitted output resolves to a generated per-run durable path
|
|
1242
|
+
// `<asyncDir>/outputs/0-<agent>.md`; omitted outputMode resolves to file-only
|
|
1243
|
+
// whenever an output path is active. Explicit paths, `output: false`, and
|
|
1244
|
+
// explicit `outputMode: "inline"` are preserved exactly.
|
|
1245
|
+
const outputPath = typeof effectiveOutput === "string"
|
|
1246
|
+
? resolveSingleOutputPath(effectiveOutput, ctx.cwd, runnerCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined))
|
|
1247
|
+
: effectiveOutput === false
|
|
1248
|
+
? undefined
|
|
1249
|
+
: path.join(asyncDir, "outputs", `0-${agent}.md`);
|
|
1232
1250
|
systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, agentConfig);
|
|
1233
|
-
const outputMode = params.outputMode ?? "inline";
|
|
1251
|
+
const outputMode = params.outputMode ?? (outputPath ? "file-only" : "inline");
|
|
1234
1252
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1235
1253
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
1236
1254
|
const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath, agentConfig);
|
|
@@ -192,6 +192,32 @@ function completionBatchKey(result: CompletionNotification): string {
|
|
|
192
192
|
return cwd ? `cwd:${cwd}` : "unknown";
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Reference-first notification preview: compact child summaries built by the
|
|
197
|
+
* result watcher from each child's authoritative output path/log. Never uses
|
|
198
|
+
* `results[].output` or the run-level `summary` as parent-facing text; the run
|
|
199
|
+
* summary remains only a defensive fallback for legacy payloads without results.
|
|
200
|
+
*/
|
|
201
|
+
function buildReferenceFirstPreview(result: CompletionNotification): string {
|
|
202
|
+
const children = Array.isArray(result.results) ? result.results : [];
|
|
203
|
+
if (children.length === 0) {
|
|
204
|
+
return typeof result.summary === "string" ? result.summary : "";
|
|
205
|
+
}
|
|
206
|
+
if (children.length === 1) {
|
|
207
|
+
const childSummary = typeof children[0]?.summary === "string" && children[0].summary.trim()
|
|
208
|
+
? children[0].summary
|
|
209
|
+
: "(no output)";
|
|
210
|
+
return childSummary;
|
|
211
|
+
}
|
|
212
|
+
return children.map((child, index) => {
|
|
213
|
+
const agentLabel = typeof child?.agent === "string" ? child.agent : `step-${index + 1}`;
|
|
214
|
+
const childSummary = typeof child?.summary === "string" && child.summary.trim()
|
|
215
|
+
? child.summary
|
|
216
|
+
: "(no output)";
|
|
217
|
+
return `${index + 1}. ${agentLabel}\n${childSummary}`;
|
|
218
|
+
}).join("\n\n");
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
export function buildCompletionDetails(result: CompletionNotification): SubagentNotifyDetails {
|
|
196
222
|
const agent = result.agent ?? "unknown";
|
|
197
223
|
const summary = typeof result.summary === "string" ? result.summary : "";
|
|
@@ -229,7 +255,7 @@ export function buildCompletionDetails(result: CompletionNotification): Subagent
|
|
|
229
255
|
status,
|
|
230
256
|
...(result.source ? { source: result.source } : {}),
|
|
231
257
|
...(taskInfo ? { taskInfo } : {}),
|
|
232
|
-
resultPreview:
|
|
258
|
+
resultPreview: buildReferenceFirstPreview(result),
|
|
233
259
|
...(typeof result.durationMs === "number" ? { durationMs: result.durationMs } : {}),
|
|
234
260
|
...(handoffPath ? { handoffPath } : {}),
|
|
235
261
|
...(session ? { sessionLabel: session.label, sessionValue: session.value } : {}),
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "../../intercom/result-intercom.ts";
|
|
21
21
|
import { projectNestedRegistryForRoot, sanitizeSummary } from "../shared/nested-events.ts";
|
|
22
22
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
|
+
import { formatBoundedPersistenceFallback, formatSavedOutputReference } from "../shared/single-output.ts";
|
|
23
24
|
import type { CompletionNotifier, CompletionNotification } from "./notify.ts";
|
|
24
25
|
|
|
25
26
|
const WATCHER_RESTART_DELAY_MS = 3000;
|
|
@@ -46,10 +47,14 @@ type ResultWatcherDeps = {
|
|
|
46
47
|
type ResultFileChild = {
|
|
47
48
|
agent?: string;
|
|
48
49
|
output?: string;
|
|
50
|
+
/** Authoritative durable output location: explicit/generated saved path or async output log. */
|
|
51
|
+
outputPath?: string;
|
|
52
|
+
savedOutputPath?: string;
|
|
49
53
|
outputState?: SubagentOutputState;
|
|
50
54
|
error?: string;
|
|
51
55
|
success?: boolean;
|
|
52
56
|
state?: string;
|
|
57
|
+
exitCode?: number | null;
|
|
53
58
|
interrupted?: boolean;
|
|
54
59
|
timedOut?: boolean;
|
|
55
60
|
stopped?: boolean;
|
|
@@ -92,6 +97,57 @@ function isNotFound(error: unknown): boolean {
|
|
|
92
97
|
return errorCode(error) === "ENOENT";
|
|
93
98
|
}
|
|
94
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Resolve the authoritative output location for one async child: explicit/generated
|
|
102
|
+
* saved output path first, else the persisted per-child output log
|
|
103
|
+
* (`asyncDir/output-<flat-index>.log`). Legacy result files without saved-path
|
|
104
|
+
* fields fall back to the output log when the run directory is known.
|
|
105
|
+
*/
|
|
106
|
+
function childOutputPath(data: ResultFileData, result: ResultFileChild | undefined, index: number): string | undefined {
|
|
107
|
+
if (typeof result?.outputPath === "string" && result.outputPath.trim()) return result.outputPath;
|
|
108
|
+
if (typeof result?.savedOutputPath === "string" && result.savedOutputPath.trim()) return result.savedOutputPath;
|
|
109
|
+
if (typeof data.asyncDir === "string" && data.asyncDir.trim()) return path.join(data.asyncDir, `output-${index}.log`);
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reference-only child summary for async delivery: the saved-output reference
|
|
115
|
+
* (or output-log reference) plus process error/status for failed children. Never
|
|
116
|
+
* emits `results[].output` or the run `summary` as parent-facing text; when the
|
|
117
|
+
* output location is absent or unreadable, a bounded fallback is used instead.
|
|
118
|
+
*/
|
|
119
|
+
function buildChildReferenceSummary(fsApi: ResultWatcherFs, input: {
|
|
120
|
+
outputPath: string | undefined;
|
|
121
|
+
outputText: string;
|
|
122
|
+
exitCode: number;
|
|
123
|
+
error?: string;
|
|
124
|
+
}): string {
|
|
125
|
+
const statusError = input.exitCode !== 0 && input.error ? input.error : undefined;
|
|
126
|
+
if (input.outputPath) {
|
|
127
|
+
try {
|
|
128
|
+
const content = fsApi.readFileSync(input.outputPath, "utf-8");
|
|
129
|
+
const ref = formatSavedOutputReference(input.outputPath, content);
|
|
130
|
+
return statusError ? `${statusError}\n\n${ref.message}` : ref.message;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
const fallback = formatBoundedPersistenceFallback({
|
|
133
|
+
error: `Failed to read saved output: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
outputPath: input.outputPath,
|
|
135
|
+
fullOutput: input.outputText,
|
|
136
|
+
exitCode: input.exitCode,
|
|
137
|
+
processError: input.error,
|
|
138
|
+
});
|
|
139
|
+
return statusError ? `${statusError}\n\n${fallback}` : fallback;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const fallback = formatBoundedPersistenceFallback({
|
|
143
|
+
error: "No saved output path is available for this child.",
|
|
144
|
+
fullOutput: input.outputText,
|
|
145
|
+
exitCode: input.exitCode,
|
|
146
|
+
processError: input.error,
|
|
147
|
+
});
|
|
148
|
+
return statusError ? `${statusError}\n\n${fallback}` : fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
95
151
|
function shouldPoll(error: unknown): boolean {
|
|
96
152
|
const code = errorCode(error);
|
|
97
153
|
return code === "EMFILE" || code === "ENOSPC";
|
|
@@ -180,14 +236,16 @@ export function createResultWatcher(
|
|
|
180
236
|
const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
|
|
181
237
|
const resultChildren: ResultFileChild[] = hasResultChildren
|
|
182
238
|
? data.results!
|
|
183
|
-
: [{ agent: data.agent ?? undefined,
|
|
239
|
+
: [{ agent: data.agent ?? undefined, outputPath: childOutputPath(data, undefined, 0), outputState: "unknown", success: data.success }];
|
|
184
240
|
const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
|
|
185
241
|
const baseOutput = hasResultChildren ? result.output : result.output ?? data.summary;
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
:
|
|
242
|
+
const outputText = typeof baseOutput === "string" ? baseOutput : "";
|
|
243
|
+
const summary = buildChildReferenceSummary(fsApi, {
|
|
244
|
+
outputPath: childOutputPath(data, result, index),
|
|
245
|
+
outputText,
|
|
246
|
+
exitCode: result.success === false ? 1 : result.success === true ? 0 : typeof result.exitCode === "number" ? result.exitCode : data.success === false ? 1 : 0,
|
|
247
|
+
error: result.error ?? (result.success === false ? (typeof data.error === "string" ? data.error : undefined) : undefined),
|
|
248
|
+
});
|
|
191
249
|
const sessionPath = result.sessionFile ?? (resultChildren.length === 1 ? data.sessionFile : undefined);
|
|
192
250
|
const childNestedChildren = sanitizeNestedResultChildren(result.children, resultPath, `results[${index}].children`);
|
|
193
251
|
const childState = result.state === "paused" || result.state === "stopped"
|
|
@@ -1476,7 +1476,11 @@ async function runSingleStep(
|
|
|
1476
1476
|
|
|
1477
1477
|
const rawOutput = finalResult?.finalOutput ?? "";
|
|
1478
1478
|
const outputForPersistence = stripAcceptanceReport(rawOutput);
|
|
1479
|
-
|
|
1479
|
+
// Persist whenever an output path is configured and meaningful output exists,
|
|
1480
|
+
// not only for successful runs: failed children keep a readable result file
|
|
1481
|
+
// and a saved-output reference instead of raw inline output. Synthetic
|
|
1482
|
+
// failures without output stay "absent" and are not promoted to a reference.
|
|
1483
|
+
const resolvedOutput = step.outputPath && (outputForPersistence.trim() || (finalResult?.exitCode ?? 1) === 0)
|
|
1480
1484
|
? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
|
|
1481
1485
|
: { fullOutput: outputForPersistence };
|
|
1482
1486
|
const output = stripAcceptanceReport(resolvedOutput.fullOutput);
|
|
@@ -1515,6 +1519,9 @@ async function runSingleStep(
|
|
|
1515
1519
|
savedPath: resolvedOutput.savedPath,
|
|
1516
1520
|
outputReference,
|
|
1517
1521
|
saveError: resolvedOutput.saveError,
|
|
1522
|
+
error: finalResult?.error
|
|
1523
|
+
?? (finalResult?.stopped ? ctx.stopMessage ?? "Subagent stopped by user." : undefined)
|
|
1524
|
+
?? (finalResult?.timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : undefined),
|
|
1518
1525
|
});
|
|
1519
1526
|
outputForSummary = finalizedOutput.displayOutput;
|
|
1520
1527
|
const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
|
|
@@ -1596,6 +1603,10 @@ async function runSingleStep(
|
|
|
1596
1603
|
context: step.context,
|
|
1597
1604
|
...(step.agentContract ? { agentContract: step.agentContract } : {}),
|
|
1598
1605
|
launchContractDigest: actualLaunchContractDigest,
|
|
1606
|
+
// Authoritative durable output location for parent-facing delivery: the
|
|
1607
|
+
// explicit/generated saved output path when present, otherwise the async
|
|
1608
|
+
// per-child output log (`asyncDir/output-<flat-index>.log`).
|
|
1609
|
+
outputPath: step.outputPath ?? ctx.outputFile,
|
|
1599
1610
|
output: outputForSummary,
|
|
1600
1611
|
outputState,
|
|
1601
1612
|
exitCode: effectiveFinalExitCode,
|
|
@@ -3388,6 +3399,7 @@ async function runSubagent(
|
|
|
3388
3399
|
launchResolvedExtensions: pr.launchResolvedExtensions,
|
|
3389
3400
|
runtimeAcknowledgedExtensions: pr.runtimeAcknowledgedExtensions,
|
|
3390
3401
|
output: pr.output,
|
|
3402
|
+
outputPath: pr.outputPath,
|
|
3391
3403
|
outputState: pr.outputState,
|
|
3392
3404
|
error: pr.error,
|
|
3393
3405
|
protocolError: pr.protocolError,
|
|
@@ -3797,6 +3809,7 @@ async function runSubagent(
|
|
|
3797
3809
|
launchContractDigest: pr.launchContractDigest,
|
|
3798
3810
|
launchResolvedExtensions: pr.launchResolvedExtensions,
|
|
3799
3811
|
output: pr.output,
|
|
3812
|
+
outputPath: pr.outputPath,
|
|
3800
3813
|
outputState: pr.outputState,
|
|
3801
3814
|
error: pr.error,
|
|
3802
3815
|
protocolError: pr.protocolError,
|
|
@@ -3984,6 +3997,7 @@ async function runSubagent(
|
|
|
3984
3997
|
launchResolvedExtensions: singleResult.launchResolvedExtensions,
|
|
3985
3998
|
runtimeAcknowledgedExtensions: singleResult.runtimeAcknowledgedExtensions,
|
|
3986
3999
|
output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
|
|
4000
|
+
outputPath: singleResult.outputPath,
|
|
3987
4001
|
outputState: singleResult.outputState,
|
|
3988
4002
|
error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
|
|
3989
4003
|
protocolError: singleResult.protocolError,
|
|
@@ -4311,6 +4325,7 @@ async function runSubagent(
|
|
|
4311
4325
|
results: results.map((r) => ({
|
|
4312
4326
|
agent: r.agent,
|
|
4313
4327
|
context: r.context,
|
|
4328
|
+
outputPath: r.outputPath,
|
|
4314
4329
|
output: r.output,
|
|
4315
4330
|
outputState: r.outputState,
|
|
4316
4331
|
error: r.error,
|