Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Preserve completed Pi sub-agent answers
Do not reopen completed turns with time-pressure steering. Scope child reporting to delegated task results while retaining task-required safe outputs and inherited controls. Make smoke answers explicitly plain text and tool-free, with regressions and documentation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
pelikhan and Copilot committed Oct 8, 2026
commit fe36b03733b53daf92827e7516b32611badf3b31
7 changes: 7 additions & 0 deletions .changeset/fix-pi-smoke-completion.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .github/workflows/smoke-pi-sub-agents.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 12 additions & 9 deletions .github/workflows/smoke-pi-sub-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,30 +62,33 @@ Provider-qualified model IDs pin the exact models rather than resolving aliases.
description: Returns the Haiku model identity for smoke testing
model: copilot/claude-haiku-4.5
---
When asked `who am i?`, reply with exactly:
When asked `who am i?`, reply with this single plain-text line, without Markdown
or backticks:

`claude-haiku-4.5`
claude-haiku-4.5

No extra words, punctuation, or formatting.
Do not call tools. No extra words, punctuation, or formatting.

## agent: `mini-whoami`
---
description: Returns the GPT-5 mini model identity for smoke testing
model: copilot/gpt-5-mini
---
When asked `who am i?`, reply with exactly:
When asked `who am i?`, reply with this single plain-text line, without Markdown
or backticks:

`gpt-5-mini`
gpt-5-mini

No extra words, punctuation, or formatting.
Do not call tools. No extra words, punctuation, or formatting.

## agent: `compact-whoami`
---
description: Returns the GPT-4o mini model identity for smoke testing
model: copilot/gpt-4o-mini
---
When asked `who am i?`, reply with exactly:
When asked `who am i?`, reply with this single plain-text line, without Markdown
or backticks:

`gpt-4o-mini`
gpt-4o-mini

No extra words, punctuation, or formatting.
Do not call tools. No extra words, punctuation, or formatting.
4 changes: 2 additions & 2 deletions actions/setup/js/pi_steering_extension.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ function piSteeringExtension(pi) {
process.stderr.write(`[gh-aw/steering] Session started. timeout=${config.timeoutMinutes}min, warn<${config.timeWarningMinutes}min, critical<${config.timeCriticalMinutes}min\n`);
});

pi.on("turn_end", async (/** @type {any} */ _event, /** @type {any} */ ctx) => {
if (startTime === undefined) {
pi.on("turn_end", async (/** @type {any} */ event, /** @type {any} */ ctx) => {
if (startTime === undefined || event.message?.stopReason === "stop") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deadline steering now runs only when stopReason === "toolUse"; tests cover stop, error, aborted, unknown, and missing reasons. Pushed as 1994aa9.

return;
}

Expand Down
11 changes: 11 additions & 0 deletions actions/setup/js/pi_steering_extension.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,15 @@ describe("Pi timeout steering", () => {
expect(sendUserMessage).toHaveBeenCalledOnce();
expect(sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("CRITICAL"), { deliverAs: "steer" });
});

it("does not reopen a completed answer, but still steers subsequent tool turns", async () => {
const { handlers, sendUserMessage } = harness();
await handlers.agent_start();
vi.advanceTimersByTime(9 * 60_000);
await handlers.turn_end({ message: { role: "assistant", stopReason: "stop" } }, {});
expect(sendUserMessage).not.toHaveBeenCalled();
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
expect(sendUserMessage).toHaveBeenCalledOnce();
expect(sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("CRITICAL"), { deliverAs: "steer" });
});
});
6 changes: 5 additions & 1 deletion actions/setup/js/pi_subagent_extension.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ async function runPiSubagent(agent, task, ctx, signal, launch = spawn) {
const promptPath = path.join(dir, "system.txt");
const systemPath = process.env.GH_AW_PI_SYSTEM_PROMPT;
const system = systemPath ? fs.readFileSync(systemPath, "utf8") + "\n\n" : "";
fs.writeFileSync(promptPath, system + agent.prompt, { mode: 0o600 });
const delegationScope =
"You are a delegated sub-agent, not the workflow's main agent. Complete only the delegated task and return your answer to the parent, following the declared agent's output format exactly. " +
"The parent is responsible for finalizing the workflow and its required safe-output reporting. Do not emit noop safe outputs or workflow-completion reports for this child session. " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] This fix relies entirely on prompt text ("Do not emit noop safe outputs or workflow-completion reports") rather than structural enforcement. The codebase already has a working pattern for this exact scenario one line above: GH_AW_PI_SUBAGENT_CHILD env var gates piSubagentExtension registration structurally. A model can ignore prompt instructions.

💡 Why this matters and a possible structural fix

The root cause of the original bug ("staged report FAIL despite green Actions... aborted compact child") was that children could independently trigger workflow-completion/noop safe outputs. Pinning this down with a text instruction treats the symptom, not the mechanism. Since GH_AW_PI_SUBAGENT_CHILD=1 is already set in the child's env (line 52), consider filtering/disabling the noop/completion-reporting tool(s) from the child's MCP tool list when that env var is set, the same way piSubagentExtension itself is disabled for children. This would make the constraint unbypassable regardless of what the model decides to do with the extra prompt text.

If a structural fix is infeasible for this PR (e.g. tool registration happens in a separate process/extension not easily reached here), it would help to note in the PR description or a code comment why prompt-only mitigation was chosen, and whether gh aw audit/tests can detect a regression if a child does emit a completion report despite the instruction.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The child Pi tool policy now blocks noop and incomplete-reporting tools while leaving task safe outputs available. The existing prompt assertions remain, with added policy regression coverage. Pushed as 1994aa9.

"Safe-output actions required to perform the delegated task remain permitted.";
fs.writeFileSync(promptPath, system + agent.prompt + "\n\n" + delegationScope, { mode: 0o600 });
process.stdout.write(JSON.stringify({ type: "gh_aw_subagent_dispatch", agent: agent.name, requested_model: agent.declaredModel || agent.modelId, resolved_model: agent.modelId }) + "\n");
return await new Promise((resolve, reject) => {
const child = launch(process.env.GH_AW_PI_COMMAND || "pi", subagentArgs(agent, promptPath), {
Expand Down
15 changes: 15 additions & 0 deletions actions/setup/js/pi_subagent_extension.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ describe("Pi managed delegation", () => {
await expect(runPiSubagent(agent, "Read.", { cwd: dir }, undefined, launcher({ stopReason: "error", errorMessage: "Model unavailable" }))).rejects.toThrow("Model unavailable");
});

it("inherits workflow controls without making the child finalize the workflow", async () => {
const systemPath = path.join(dir, "workflow-system.txt");
fs.writeFileSync(systemPath, "Workflow instructions and tool restrictions.");
vi.stubEnv("GH_AW_PI_SYSTEM_PROMPT", systemPath);
const launch = launcher();
await runPiSubagent(agent, "Read.", { cwd: dir }, undefined, (...args) => {
const prompt = fs.readFileSync(args[1].at(-1), "utf8");
expect(prompt).toContain("Workflow instructions and tool restrictions.\n\nRead only.");
expect(prompt).toContain("The parent is responsible for finalizing the workflow");
expect(prompt).toContain("Do not emit noop safe outputs");
expect(prompt).toContain("Safe-output actions required to perform the delegated task remain permitted");
return launch(...args);
});
});

it("propagates nonzero child exits", async () => {
await expect(runPiSubagent(agent, "Read.", { cwd: dir }, undefined, launcher({}, 1))).rejects.toThrow();
});
Expand Down
2 changes: 2 additions & 0 deletions docs/src/content/docs/engines/pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ An omitted model inherits the parent's configured model. Bare model IDs use the

Children retain the workflow's inference gateway, MCP configuration, steering, bash allowlist, and edit restrictions. Tool-call and denial budgets are shared with the parent. Children use compiler-managed execution arguments rather than inheriting `engine.args` overrides. Optional automatically discovered extensions are disabled in children; only the managed infrastructure extensions are loaded. Custom drivers do not support this delegation path.

Children return task results to the parent instead of emitting workflow-completion reports or `noop` safe outputs. Task-required safe-output actions remain available. Time-pressure steering applies to ongoing tool turns, not completed answers, so it does not reopen a finished child session.

Dispatch logs record both the requested alias and resolved model. `gh aw audit` reports `subagent_model_not_observed` when usage evidence exists but no agent inference matches a declared model. This means the agent may not have run or its model was not honored; model presence alone does not prove delegation. Detection requests are not treated as agent-model evidence.

## Additional providers
Expand Down
Loading