pi-open-agents 0.1.29 → 0.1.31
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/README.md +30 -2
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/primary/executor.ts +53 -26
- package/src/subagent/executor.ts +79 -12
- package/src/subagent/prompt.ts +46 -1
- package/src/subagent/tool.ts +13 -1
package/README.md
CHANGED
|
@@ -167,6 +167,20 @@ active agent's rules *before it runs* — not just used to filter the toolset:
|
|
|
167
167
|
> blocks matching *individual calls* (patterns included), not just statically
|
|
168
168
|
> hiding whole tools. (#20)
|
|
169
169
|
|
|
170
|
+
> **⚠️ Behavior change (v0.1.31):** subagent results are now file + summary
|
|
171
|
+
> by default — full output goes to a temp archive, inline carries the child's
|
|
172
|
+
> closing outcome block (#32). If you parsed full inline output
|
|
173
|
+
> programmatically, read the archive path from the result instead.
|
|
174
|
+
|
|
175
|
+
> **⚠️ Behavior change (v0.1.30):** primary-agent static filtering now covers
|
|
176
|
+
> **all tool sources**, not just builtins (#18). If an agent declares
|
|
177
|
+
> `permission` or `tools:`, extension tools (web, LSP, MCP, …) not matched
|
|
178
|
+
> are hidden from the model — previously they were always kept. Only blanket
|
|
179
|
+
> `deny` rules hide a tool; argument-pattern denies and `ask` stay enforced
|
|
180
|
+
> by the runtime gate. If your allowlist drops `subagent`, the agent cannot
|
|
181
|
+
> delegate anymore — apply warns you explicitly. No restrictions = everything
|
|
182
|
+
> stays active, exactly as before.
|
|
183
|
+
|
|
170
184
|
**Inheritance down the delegation chain (#8).** A subagent can never exceed
|
|
171
185
|
the authority of the agent that delegated to it — in both directions of the
|
|
172
186
|
rule language:
|
|
@@ -199,12 +213,24 @@ If an agent has the `subagent` tool, the plugin automatically appends a
|
|
|
199
213
|
subagents and the correct call syntax:
|
|
200
214
|
|
|
201
215
|
```
|
|
202
|
-
subagent({ agent: "<name>", task: "<task>", cwd?, session?, model? })
|
|
216
|
+
subagent({ agent: "<name>", task: "<task>", cwd?, session?, model?, output? })
|
|
203
217
|
```
|
|
204
218
|
|
|
205
219
|
You never need to explain delegation mechanics in agent prompts. The plugin
|
|
206
220
|
handles it, the same way pi handles tool descriptions.
|
|
207
221
|
|
|
222
|
+
### Subagent output: file + summary (not full-inline)
|
|
223
|
+
|
|
224
|
+
Every run archives its verbatim output to a temp file and returns only a
|
|
225
|
+
short outcome inline — open the file only if the summary is not enough to
|
|
226
|
+
decide the next step. The child is instructed (via its system prompt) to end
|
|
227
|
+
with a `## Outcome` block; without it, the executor falls back to today's
|
|
228
|
+
truncated inline. Pass `output: "<path>"` when the deliverable itself is a
|
|
229
|
+
file: the child writes it there and cites it as `` `<path>` — <what it
|
|
230
|
+
contains> '' ›. On failure the error always comes back inline, never behind a
|
|
231
|
+
file reference. Inspired by @EduardoOrtiz89's fork (`output`/`outputMode`),
|
|
232
|
+
redesigned as the default instead of an opt-in (#32).
|
|
233
|
+
|
|
208
234
|
### Subagent execution engine
|
|
209
235
|
|
|
210
236
|
When an agent delegates, the plugin spawns a child pi process with the target
|
|
@@ -387,6 +413,8 @@ six sessions, zero chaos: that's the exact workstation setup documented in
|
|
|
387
413
|
- ~~**Parent permission inheritance** for subagents~~ — ✅ shipped in [0.1.27](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.27) (#8)
|
|
388
414
|
- ~~**Agent hot-reload**~~ — ✅ works via pi's built-in `/reload`, shipped in [0.1.27](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.27) (#9)
|
|
389
415
|
- ~~**Per-call model override** in `subagent()`~~ — ✅ shipped in [0.1.19](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.19) (#10)
|
|
416
|
+
- ~~**Primary tool filters incl. extension tools**~~ — ✅ shipped in [0.1.30](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.30) (#18)
|
|
417
|
+
- ~~**Subagent output: file + summary**~~ — ✅ shipped in [0.1.31](https://github.com/andrea-tomassi/pi-open-agents/releases/tag/v0.1.31) (#32)
|
|
390
418
|
- **Parallel subagent limit** — #11
|
|
391
419
|
|
|
392
420
|
---
|
|
@@ -398,7 +426,7 @@ opening an issue or pull request.
|
|
|
398
426
|
|
|
399
427
|
```bash
|
|
400
428
|
npm install
|
|
401
|
-
npm test #
|
|
429
|
+
npm test # 216 tests
|
|
402
430
|
npm run typecheck # tsc --noEmit
|
|
403
431
|
```
|
|
404
432
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -261,9 +261,9 @@ export { matchPattern, isWildcardPattern } from "./permission/matcher.ts";
|
|
|
261
261
|
export { evaluate, toolToPermission, getDisabledTools, filterDisabledTools } from "./permission/evaluator.ts";
|
|
262
262
|
|
|
263
263
|
// Subagent re-exports
|
|
264
|
-
export { runSubagent, resolveSubagentSession, resolvePiEntryPoint, isVirtualBunPath, resolveCompiledCommand } from "./subagent/executor.ts";
|
|
264
|
+
export { runSubagent, resolveSubagentSession, resolvePiEntryPoint, isVirtualBunPath, resolveCompiledCommand, extractOutcomeBlock, OUTCOME_MARKER } from "./subagent/executor.ts";
|
|
265
265
|
export type { AgentProgress, AgentResult, RunSubagentOptions, SubagentSessionInfo, SubagentSessionMode, HostProcessProbe } from "./subagent/executor.ts";
|
|
266
266
|
export { resolveSkills } from "./subagent/skills.ts";
|
|
267
267
|
export type { ResolvedSkill } from "./subagent/skills.ts";
|
|
268
|
-
export { buildSubagentPrompt } from "./subagent/prompt.ts";
|
|
268
|
+
export { buildSubagentPrompt, buildClosingContract } from "./subagent/prompt.ts";
|
|
269
269
|
export { formatResultLines, formatUsage } from "./subagent/render.ts";
|
package/src/primary/executor.ts
CHANGED
|
@@ -75,47 +75,72 @@ function applyThinking(agent: AgentDefinition, pi: ExtensionAPI): void {
|
|
|
75
75
|
*
|
|
76
76
|
* Logic:
|
|
77
77
|
* 1. If agent has permission rules → filter via permission engine
|
|
78
|
-
* 2. Else if agent has tools whitelist → filter to only those
|
|
78
|
+
* 2. Else if agent has tools whitelist → filter to only those
|
|
79
79
|
* 3. Otherwise → all tools active
|
|
80
80
|
*
|
|
81
|
-
*
|
|
81
|
+
* Filtering covers ALL registered tool sources (builtin, extension, MCP,
|
|
82
|
+
* SDK): a restricted profile can hide extension tools (web, LSP, …) from
|
|
83
|
+
* the model, not just block their calls. Only blanket `deny` rules hide a
|
|
84
|
+
* tool statically — argument-pattern denies and `ask` stay enforced by the
|
|
85
|
+
* runtime gate (#20), which also remains the backstop for tools registered
|
|
86
|
+
* after the agent started (issue #18).
|
|
87
|
+
*
|
|
88
|
+
* @returns The applied active tool names (for the subagent-hidden warning).
|
|
82
89
|
*/
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
export function computeActiveTools(
|
|
91
|
+
agent: AgentDefinition,
|
|
92
|
+
allTools: Array<{ name: string }>,
|
|
93
|
+
): string[] {
|
|
94
|
+
// Case 1: Permission-based filtering (all sources)
|
|
87
95
|
if (agent.permission) {
|
|
88
96
|
const rules = parsePermissionRules(agent.permission);
|
|
89
97
|
const disabled = getDisabledTools(
|
|
90
98
|
allTools.map((t) => t.name),
|
|
91
99
|
rules,
|
|
92
100
|
);
|
|
93
|
-
|
|
94
|
-
.filter((t) => {
|
|
95
|
-
// Always keep non-builtin tools (extensions, MCP)
|
|
96
|
-
if (t.sourceInfo?.source !== "builtin") return true;
|
|
97
|
-
return !disabled.has(t.name);
|
|
98
|
-
})
|
|
99
|
-
.map((t) => t.name);
|
|
100
|
-
pi.setActiveTools(filtered);
|
|
101
|
-
return;
|
|
101
|
+
return allTools.filter((t) => !disabled.has(t.name)).map((t) => t.name);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// Case 2: Simple tools whitelist (
|
|
104
|
+
// Case 2: Simple tools whitelist (all sources)
|
|
105
105
|
if (agent.tools && agent.tools.length > 0) {
|
|
106
106
|
const whitelist = new Set(agent.tools.map((t) => t.toLowerCase()));
|
|
107
|
-
|
|
108
|
-
.filter((t) =>
|
|
109
|
-
if (t.sourceInfo?.source !== "builtin") return true;
|
|
110
|
-
return whitelist.has(t.name.toLowerCase());
|
|
111
|
-
})
|
|
107
|
+
return allTools
|
|
108
|
+
.filter((t) => whitelist.has(t.name.toLowerCase()))
|
|
112
109
|
.map((t) => t.name);
|
|
113
|
-
pi.setActiveTools(filtered);
|
|
114
|
-
return;
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
// Case 3: No restrictions — keep all tools
|
|
118
|
-
|
|
113
|
+
return allTools.map((t) => t.name);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function applyTools(agent: AgentDefinition, pi: ExtensionAPI): string[] {
|
|
117
|
+
const active = computeActiveTools(agent, pi.getAllTools());
|
|
118
|
+
pi.setActiveTools(active);
|
|
119
|
+
return active;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Warn when an agent's static restrictions hide the `subagent` tool (#18).
|
|
124
|
+
*
|
|
125
|
+
* Without it the model cannot delegate at all — that may be exactly what a
|
|
126
|
+
* locked-down profile wants, but it must be a deliberate choice, never a
|
|
127
|
+
* surprise from an allowlist that simply forgot to name it.
|
|
128
|
+
*/
|
|
129
|
+
export function warnIfHidingSubagent(
|
|
130
|
+
agent: AgentDefinition,
|
|
131
|
+
allNames: string[],
|
|
132
|
+
activeNames: string[],
|
|
133
|
+
ui: ExtensionContext["ui"],
|
|
134
|
+
): void {
|
|
135
|
+
const restricted = agent.permission !== undefined || (agent.tools?.length ?? 0) > 0;
|
|
136
|
+
if (!restricted) return;
|
|
137
|
+
if (!allNames.includes("subagent")) return;
|
|
138
|
+
if (activeNames.includes("subagent")) return;
|
|
139
|
+
ui.notify(
|
|
140
|
+
`Agent "${agent.name}": tool restrictions hide the "subagent" tool — this agent cannot delegate. ` +
|
|
141
|
+
`Add "subagent" to the allowlist if delegation is intended.`,
|
|
142
|
+
"warning",
|
|
143
|
+
);
|
|
119
144
|
}
|
|
120
145
|
|
|
121
146
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
@@ -131,7 +156,9 @@ export async function applyAgentConfig(
|
|
|
131
156
|
): Promise<void> {
|
|
132
157
|
await applyModel(agent, pi, ctx);
|
|
133
158
|
applyThinking(agent, pi);
|
|
134
|
-
|
|
159
|
+
const before = pi.getAllTools().map((t) => t.name);
|
|
160
|
+
const active = applyTools(agent, pi);
|
|
161
|
+
warnIfHidingSubagent(agent, before, active, ctx.ui);
|
|
135
162
|
}
|
|
136
163
|
|
|
137
164
|
/**
|
|
@@ -174,7 +201,7 @@ export function buildSubagentGuidance(
|
|
|
174
201
|
.map((a) => `- ${a.name}: ${a.description ?? "Subagent for delegated tasks."}`)
|
|
175
202
|
.join("\n");
|
|
176
203
|
|
|
177
|
-
return `\n\n## Subagent Delegation\n\nYou have ${available.length} subagent(s) available. Delegate isolated tasks using the \`subagent\` tool:\n\nsubagent({ agent: "<name>", task: "<task description>" })\n\nAvailable subagents:\n${list}\n\nIMPORTANT: Subagent names are NOT bash commands. Always use the \`subagent\` tool to delegate — never write scripts that call agent names as CLI commands.`;
|
|
204
|
+
return `\n\n## Subagent Delegation\n\nYou have ${available.length} subagent(s) available. Delegate isolated tasks using the \`subagent\` tool:\n\nsubagent({ agent: "<name>", task: "<task description>" })\n\nAvailable subagents:\n${list}\n\nSubagent output: every run saves its full output to a temp file and returns only a short outcome summary — open the file only if the summary is not enough to decide your next step. Pass \`output: "<path>"\` when the deliverable itself is a file (report, patch, doc): the child writes it there and cites it in its summary. On failure the child always returns the error inline.\n\nIMPORTANT: Subagent names are NOT bash commands. Always use the \`subagent\` tool to delegate — never write scripts that call agent names as CLI commands.`;
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
/**
|
package/src/subagent/executor.ts
CHANGED
|
@@ -127,6 +127,13 @@ export interface RunSubagentOptions {
|
|
|
127
127
|
parentCeiling?: string[];
|
|
128
128
|
/** Parent's restrictive (deny/ask) permission rules, inherited into the child's gate via env. */
|
|
129
129
|
parentRules?: Array<{ permission: string; pattern: string; action: "deny" | "ask" }>;
|
|
130
|
+
/**
|
|
131
|
+
* Commissioned deliverable path (issue #32). When set, the child is
|
|
132
|
+
* instructed to write its main deliverable there (resolved against cwd
|
|
133
|
+
* when relative) and cite it in the outcome block. Independent from the
|
|
134
|
+
* automatic temp archive of the full transcript output.
|
|
135
|
+
*/
|
|
136
|
+
outputPath?: string;
|
|
130
137
|
resolvePi?: () => Promise<PiResolution> | PiResolution;
|
|
131
138
|
runner?: ProcessRunner;
|
|
132
139
|
fs?: ExecutorFs;
|
|
@@ -216,6 +223,31 @@ export function composeChildTools(
|
|
|
216
223
|
return result.length > 0 ? result : "none";
|
|
217
224
|
}
|
|
218
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Marker separating the child's full transcript from its closing outcome
|
|
228
|
+
* block (issue #32). The child is instructed (see buildClosingContract) to
|
|
229
|
+
* end its final message with a `## Outcome` line followed by 2-3 lines.
|
|
230
|
+
*/
|
|
231
|
+
export const OUTCOME_MARKER = "## Outcome";
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Split the child's closing outcome block off the full output.
|
|
235
|
+
*
|
|
236
|
+
* Returns the text after the LAST marker line (case-insensitive, surrounding
|
|
237
|
+
* whitespace ignored), or undefined when the child omitted the block —
|
|
238
|
+
* older agents and error paths fall back to today's truncated inline.
|
|
239
|
+
*/
|
|
240
|
+
export function extractOutcomeBlock(output: string): string | undefined {
|
|
241
|
+
const lines = output.split("\n");
|
|
242
|
+
let idx = -1;
|
|
243
|
+
lines.forEach((line, i) => {
|
|
244
|
+
if (line.trim().toLowerCase() === OUTCOME_MARKER.toLowerCase()) idx = i;
|
|
245
|
+
});
|
|
246
|
+
if (idx === -1) return undefined;
|
|
247
|
+
const rest = lines.slice(idx + 1).join("\n").trim();
|
|
248
|
+
return rest ? rest : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
export function subagentSessionDir(
|
|
220
252
|
cwd: string,
|
|
221
253
|
agentDir = path.join(os.homedir(), ".pi", "agent"),
|
|
@@ -620,12 +652,25 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
620
652
|
options.onProgress?.(progress(status));
|
|
621
653
|
|
|
622
654
|
try {
|
|
655
|
+
// ── Commissioned deliverable (issue #32) ──
|
|
656
|
+
// Resolved against the child cwd so the closing contract can name the
|
|
657
|
+
// exact path and the task can point at it.
|
|
658
|
+
const deliverablePath =
|
|
659
|
+
options.outputPath && options.outputPath.trim()
|
|
660
|
+
? path.resolve(options.cwd, options.outputPath)
|
|
661
|
+
: undefined;
|
|
662
|
+
const task =
|
|
663
|
+
deliverablePath !== undefined
|
|
664
|
+
? `${options.task}\n\nDeliverable: save your main deliverable to ${deliverablePath} and cite it in the outcome block.`
|
|
665
|
+
: options.task;
|
|
666
|
+
|
|
623
667
|
// ── Build system prompt ──
|
|
624
668
|
const promptFilePath = path.join(tempDir, "system-prompt.md");
|
|
625
669
|
const promptResult = await buildSubagentPrompt({
|
|
626
670
|
agent: options.agent,
|
|
627
671
|
cwd: options.cwd,
|
|
628
672
|
agentDir: options.agentDir,
|
|
673
|
+
outputPath: deliverablePath,
|
|
629
674
|
});
|
|
630
675
|
|
|
631
676
|
for (const source of promptResult.skills.skippedPackages) {
|
|
@@ -641,9 +686,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
641
686
|
|
|
642
687
|
// ── Write task to file if too long ──
|
|
643
688
|
let taskFilePath: string | undefined;
|
|
644
|
-
if (
|
|
689
|
+
if (task.length > TASK_FILE_THRESHOLD) {
|
|
645
690
|
taskFilePath = path.join(tempDir, "task.md");
|
|
646
|
-
await fileSystem.writeFile(taskFilePath,
|
|
691
|
+
await fileSystem.writeFile(taskFilePath, task);
|
|
647
692
|
}
|
|
648
693
|
|
|
649
694
|
// ── Resolve pi CLI entry point ──
|
|
@@ -701,7 +746,7 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
701
746
|
}
|
|
702
747
|
|
|
703
748
|
// Task
|
|
704
|
-
args.push(buildTaskArgument(
|
|
749
|
+
args.push(buildTaskArgument(task, taskFilePath));
|
|
705
750
|
|
|
706
751
|
// ── Build env vars ──
|
|
707
752
|
const envVars: SubagentEnvVars = {
|
|
@@ -810,16 +855,38 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
810
855
|
output = stderr || `Subagent exited with code ${exit.exitCode}`;
|
|
811
856
|
}
|
|
812
857
|
|
|
813
|
-
// ──
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
858
|
+
// ── Archive full output + derive the inline result (issue #32) ──
|
|
859
|
+
// Every run archives the verbatim output to a temp file; inline the
|
|
860
|
+
// child only sends its closing outcome block (file reference included).
|
|
861
|
+
// Without the block (older agents, error paths) fall back to today's
|
|
862
|
+
// truncated inline. A failed archive write must never break delegation:
|
|
863
|
+
// fall back to inline-only and warn.
|
|
864
|
+
const fullOutputPath = path.join(
|
|
865
|
+
options.outputArchiveDir ?? os.tmpdir(),
|
|
866
|
+
`${safeFilePart(options.agent.name)}-${startedAt}-output.md`,
|
|
867
|
+
);
|
|
868
|
+
let archived = false;
|
|
869
|
+
try {
|
|
870
|
+
await fileSystem.writeFile(fullOutputPath, output);
|
|
871
|
+
archived = true;
|
|
872
|
+
} catch (error) {
|
|
873
|
+
console.warn(
|
|
874
|
+
`[pi-open-agents] could not archive subagent output to ${fullOutputPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
820
875
|
);
|
|
821
|
-
|
|
822
|
-
|
|
876
|
+
}
|
|
877
|
+
const outcome = extractOutcomeBlock(output);
|
|
878
|
+
if (archived && outcome !== undefined) {
|
|
879
|
+
output = `${outcome}\n\nFull output: ${fullOutputPath}`;
|
|
880
|
+
} else if (archived) {
|
|
881
|
+
const truncated = truncateHeadContent(output, OUTPUT_MAX_BYTES, OUTPUT_MAX_LINES);
|
|
882
|
+
if (truncated !== undefined) {
|
|
883
|
+
output = `${truncated}\n\n[Output truncated: original ${output.split("\n").length} lines / ${byteLength(output)} bytes. Full output: ${fullOutputPath}]`;
|
|
884
|
+
}
|
|
885
|
+
} else {
|
|
886
|
+
const truncated = truncateHeadContent(output, OUTPUT_MAX_BYTES, OUTPUT_MAX_LINES);
|
|
887
|
+
if (truncated !== undefined) {
|
|
888
|
+
output = `${truncated}\n\n[Output truncated: original ${output.split("\n").length} lines / ${byteLength(output)} bytes. Archive write failed — full output unavailable.]`;
|
|
889
|
+
}
|
|
823
890
|
}
|
|
824
891
|
|
|
825
892
|
const result: AgentResult = {
|
package/src/subagent/prompt.ts
CHANGED
|
@@ -13,6 +13,12 @@ export interface BuildPromptOptions {
|
|
|
13
13
|
agent: AgentDefinition;
|
|
14
14
|
cwd: string;
|
|
15
15
|
agentDir?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Commissioned deliverable path (issue #32). When set, the child is told
|
|
18
|
+
* to write its main deliverable there and cite it in the outcome block.
|
|
19
|
+
* Already resolved against the child cwd by the caller.
|
|
20
|
+
*/
|
|
21
|
+
outputPath?: string;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
export interface BuildPromptResult {
|
|
@@ -48,10 +54,47 @@ function formatSkillsForPrompt(
|
|
|
48
54
|
return lines.join("\n");
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Child-side result contract (issue #32).
|
|
59
|
+
*
|
|
60
|
+
* Every run archives the full output to a temp file and returns only a
|
|
61
|
+
* short outcome inline — the child must therefore END its final message
|
|
62
|
+
* with a brief outcome block after a `## Outcome` marker line, cite
|
|
63
|
+
* commissioned files as `` `<path>` — <one-line description> '' ›, and keep
|
|
64
|
+
* errors inline (never behind a file reference).
|
|
65
|
+
*/
|
|
66
|
+
export function buildClosingContract(outputPath?: string): string {
|
|
67
|
+
const lines = [
|
|
68
|
+
"",
|
|
69
|
+
"",
|
|
70
|
+
"## Result Contract",
|
|
71
|
+
"",
|
|
72
|
+
"When your task is complete, end your final message with a short outcome block,",
|
|
73
|
+
"separated from the rest by a line containing exactly `## Outcome`:",
|
|
74
|
+
"",
|
|
75
|
+
"## Outcome",
|
|
76
|
+
"Outcome: <one line — done / failed + why>",
|
|
77
|
+
"Details: <what matters for the caller's next step, 2-3 lines>",
|
|
78
|
+
"",
|
|
79
|
+
"If you produced files the caller asked for, list each on its own line as",
|
|
80
|
+
"`<path>` — <one line describing the content>.",
|
|
81
|
+
"Keep the outcome block to 2-3 lines. Never paste the full output after the",
|
|
82
|
+
"marker when a file reference suffices; never hide an error behind a file reference.",
|
|
83
|
+
];
|
|
84
|
+
if (outputPath) {
|
|
85
|
+
lines.push(
|
|
86
|
+
"",
|
|
87
|
+
`The caller expects its deliverable at \`${outputPath}\` — write it there with your file tools`,
|
|
88
|
+
"and cite it in the outcome block.",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
51
93
|
/**
|
|
52
94
|
* Build the system prompt for a subagent.
|
|
53
95
|
*
|
|
54
|
-
* Starts with the agent body, appends skill injection if the agent has skills
|
|
96
|
+
* Starts with the agent body, appends skill injection if the agent has skills,
|
|
97
|
+
* then the result-closing contract (issue #32).
|
|
55
98
|
*/
|
|
56
99
|
export async function buildSubagentPrompt(options: BuildPromptOptions): Promise<BuildPromptResult> {
|
|
57
100
|
let prompt = options.agent.prompt;
|
|
@@ -75,5 +118,7 @@ export async function buildSubagentPrompt(options: BuildPromptOptions): Promise<
|
|
|
75
118
|
}
|
|
76
119
|
}
|
|
77
120
|
|
|
121
|
+
prompt = `${prompt}${buildClosingContract(options.outputPath)}`;
|
|
122
|
+
|
|
78
123
|
return { prompt, skills: skillsResult };
|
|
79
124
|
}
|
package/src/subagent/tool.ts
CHANGED
|
@@ -55,6 +55,12 @@ interface SubagentParams {
|
|
|
55
55
|
cwd?: string;
|
|
56
56
|
session?: SubagentSessionMode;
|
|
57
57
|
model?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Commissioned deliverable path (issue #32). The child writes its main
|
|
60
|
+
* deliverable there and cites it in the closing outcome; the full
|
|
61
|
+
* transcript is always archived to temp regardless.
|
|
62
|
+
*/
|
|
63
|
+
output?: string;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -279,7 +285,7 @@ export function registerSubagentTool(
|
|
|
279
285
|
name: "subagent",
|
|
280
286
|
label: "Subagent",
|
|
281
287
|
description: "Delegate a task to a named sub-agent running in an isolated pi process.",
|
|
282
|
-
promptSnippet: "Delegate isolated tasks with subagent({ agent, task, cwd?, session?, model? }).",
|
|
288
|
+
promptSnippet: "Delegate isolated tasks with subagent({ agent, task, cwd?, session?, model?, output? }).",
|
|
283
289
|
promptGuidelines,
|
|
284
290
|
parameters: Type.Object({
|
|
285
291
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
@@ -295,6 +301,11 @@ export function registerSubagentTool(
|
|
|
295
301
|
description: 'Per-call model override in "provider/model-id" format (e.g. "anthropic/claude-sonnet-4-5"). Model ids may contain slashes for aggregator providers ("openrouter/anthropic/claude-opus-5.5"). Takes precedence over the agent definition default.',
|
|
296
302
|
}),
|
|
297
303
|
),
|
|
304
|
+
output: Type.Optional(
|
|
305
|
+
Type.String({
|
|
306
|
+
description: "Deliverable file path. The child writes its main deliverable there (relative paths resolve against the child cwd) and cites it in the closing outcome. The full transcript is always archived to temp regardless.",
|
|
307
|
+
}),
|
|
308
|
+
),
|
|
298
309
|
}),
|
|
299
310
|
|
|
300
311
|
async execute(
|
|
@@ -356,6 +367,7 @@ export function registerSubagentTool(
|
|
|
356
367
|
agentDir: options.agentDir,
|
|
357
368
|
session,
|
|
358
369
|
modelOverride: params.model,
|
|
370
|
+
outputPath: params.output && params.output.trim() ? params.output : undefined,
|
|
359
371
|
parentCeiling,
|
|
360
372
|
parentRules: parentRules.length > 0 ? parentRules : undefined,
|
|
361
373
|
onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
|