Skip to content

Commit 2c909e6

Browse files
Copilotgh-aw-bot
andauthored
Merge remote-tracking branch 'origin/main' into actions/update-2026-10-08-9b18ffb208f0a0ee
# Conflicts: # .github/workflows/smoke-pi-sub-agents.lock.yml Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
2 parents 634be70 + 0697ad2 commit 2c909e6

14 files changed

Lines changed: 295 additions & 29 deletions

‎.changeset/fix-pi-smoke-completion.md‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.github/workflows/smoke-pi-sub-agents.lock.yml‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎.github/workflows/smoke-pi-sub-agents.md‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ This is the Pi variant of `smoke-copilot-sub-agents.md`. Keep output short.
4040
1. Use the managed `subagent` tool exactly once per declared agent, sequentially:
4141
- `{"agent":"haiku-whoami","task":"who am i?"}`
4242
- `{"agent":"mini-whoami","task":"who am i?"}`
43-
- `{"agent":"nano-whoami","task":"who am i?"}`
44-
2. Check the exact responses: `claude-haiku-4.5`, `gpt-5-mini`, and `gpt-5-nano`.
43+
- `{"agent":"compact-whoami","task":"who am i?"}`
44+
2. Check the exact responses: `claude-haiku-4.5`, `gpt-5-mini`, and `gpt-4o-mini`.
4545
3. Mark a missing tool, failed delegation, or unexpected response as FAIL. Do not
4646
answer on behalf of a sub-agent or substitute a different agent.
4747
4. Do not make other tool calls except the staged `create_issue` report below.
@@ -55,36 +55,40 @@ Create a staged issue titled **"Smoke Test: Pi Sub Agents - ${{ github.run_id }}
5555

5656
These fixed responses test delegation, not a model's ability to identify itself.
5757
The dispatch records and inference model usage provide model-selection evidence.
58+
Provider-qualified model IDs pin the exact models rather than resolving aliases.
5859

5960
## agent: `haiku-whoami`
6061
---
6162
description: Returns the Haiku model identity for smoke testing
62-
model: claude-haiku-4.5
63+
model: copilot/claude-haiku-4.5
6364
---
64-
When asked `who am i?`, reply with exactly:
65+
When asked `who am i?`, reply with this single plain-text line, without Markdown
66+
or backticks:
6567

66-
`claude-haiku-4.5`
68+
claude-haiku-4.5
6769

68-
No extra words, punctuation, or formatting.
70+
Do not call tools. No extra words, punctuation, or formatting.
6971

7072
## agent: `mini-whoami`
7173
---
7274
description: Returns the GPT-5 mini model identity for smoke testing
73-
model: gpt-5-mini
75+
model: copilot/gpt-5-mini
7476
---
75-
When asked `who am i?`, reply with exactly:
77+
When asked `who am i?`, reply with this single plain-text line, without Markdown
78+
or backticks:
7679

77-
`gpt-5-mini`
80+
gpt-5-mini
7881

79-
No extra words, punctuation, or formatting.
82+
Do not call tools. No extra words, punctuation, or formatting.
8083

81-
## agent: `nano-whoami`
84+
## agent: `compact-whoami`
8285
---
83-
description: Returns the GPT-5 nano model identity for smoke testing
84-
model: gpt-5-nano
86+
description: Returns the GPT-4o mini model identity for smoke testing
87+
model: copilot/gpt-4o-mini
8588
---
86-
When asked `who am i?`, reply with exactly:
89+
When asked `who am i?`, reply with this single plain-text line, without Markdown
90+
or backticks:
8791

88-
`gpt-5-nano`
92+
gpt-4o-mini
8993

90-
No extra words, punctuation, or formatting.
94+
Do not call tools. No extra words, punctuation, or formatting.

‎actions/setup/js/pi_steering_extension.cjs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ function piSteeringExtension(pi) {
6060
process.stderr.write(`[gh-aw/steering] Session started. timeout=${config.timeoutMinutes}min, warn<${config.timeWarningMinutes}min, critical<${config.timeCriticalMinutes}min\n`);
6161
});
6262

63-
pi.on("turn_end", async (/** @type {any} */ _event, /** @type {any} */ ctx) => {
64-
if (startTime === undefined) {
63+
pi.on("turn_end", async (/** @type {any} */ event, /** @type {any} */ ctx) => {
64+
if (startTime === undefined || event.message?.stopReason !== "toolUse") {
6565
return;
6666
}
6767

‎actions/setup/js/pi_steering_extension.test.cjs‎

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ describe("Pi timeout steering", () => {
4949
const { handlers, sendUserMessage } = harness();
5050
await handlers.agent_start();
5151
vi.advanceTimersByTime(6 * 60_000);
52-
await handlers.turn_end({}, {});
53-
await handlers.turn_end({}, {});
52+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
53+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
5454
expect(sendUserMessage).toHaveBeenCalledTimes(1);
5555
expect(sendUserMessage).toHaveBeenNthCalledWith(1, expect.stringContaining("wrap up"), { deliverAs: "steer" });
5656
vi.advanceTimersByTime(3 * 60_000);
57-
await handlers.turn_end({}, {});
58-
await handlers.turn_end({}, {});
57+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
58+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
5959
expect(sendUserMessage).toHaveBeenCalledTimes(2);
6060
expect(sendUserMessage).toHaveBeenNthCalledWith(2, expect.stringContaining("CRITICAL"), { deliverAs: "steer" });
6161
});
@@ -65,7 +65,24 @@ describe("Pi timeout steering", () => {
6565
await handlers.agent_start();
6666
vi.advanceTimersByTime(9 * 60_000);
6767
await handlers.agent_start();
68-
await handlers.turn_end({}, {});
68+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
69+
expect(sendUserMessage).toHaveBeenCalledOnce();
70+
expect(sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("CRITICAL"), { deliverAs: "steer" });
71+
});
72+
73+
it.each(["stop", "error", "aborted", "unknown", undefined])("does not steer terminal assistant turns with stopReason %s", async stopReason => {
74+
const { handlers, sendUserMessage } = harness();
75+
await handlers.agent_start();
76+
vi.advanceTimersByTime(9 * 60_000);
77+
await handlers.turn_end({ message: { role: "assistant", stopReason } }, {});
78+
expect(sendUserMessage).not.toHaveBeenCalled();
79+
});
80+
81+
it("steers only ongoing tool turns", async () => {
82+
const { handlers, sendUserMessage } = harness();
83+
await handlers.agent_start();
84+
vi.advanceTimersByTime(9 * 60_000);
85+
await handlers.turn_end({ message: { role: "assistant", stopReason: "toolUse" } }, {});
6986
expect(sendUserMessage).toHaveBeenCalledOnce();
7087
expect(sendUserMessage).toHaveBeenCalledWith(expect.stringContaining("CRITICAL"), { deliverAs: "steer" });
7188
});

‎actions/setup/js/pi_subagent_extension.cjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ async function runPiSubagent(agent, task, ctx, signal, launch = spawn) {
3838
const promptPath = path.join(dir, "system.txt");
3939
const systemPath = process.env.GH_AW_PI_SYSTEM_PROMPT;
4040
const system = systemPath ? fs.readFileSync(systemPath, "utf8") + "\n\n" : "";
41-
fs.writeFileSync(promptPath, system + agent.prompt, { mode: 0o600 });
41+
const delegationScope =
42+
"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. " +
43+
"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. " +
44+
"Safe-output actions required to perform the delegated task remain permitted.";
45+
fs.writeFileSync(promptPath, system + agent.prompt + "\n\n" + delegationScope, { mode: 0o600 });
4246
process.stdout.write(JSON.stringify({ type: "gh_aw_subagent_dispatch", agent: agent.name, requested_model: agent.declaredModel || agent.modelId, resolved_model: agent.modelId }) + "\n");
4347
return await new Promise((resolve, reject) => {
4448
const child = launch(process.env.GH_AW_PI_COMMAND || "pi", subagentArgs(agent, promptPath), {

‎actions/setup/js/pi_subagent_extension.test.cjs‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ describe("Pi managed delegation", () => {
9393
await expect(runPiSubagent(agent, "Read.", { cwd: dir }, undefined, launcher({ stopReason: "error", errorMessage: "Model unavailable" }))).rejects.toThrow("Model unavailable");
9494
});
9595

96+
it("inherits workflow controls without making the child finalize the workflow", async () => {
97+
const systemPath = path.join(dir, "workflow-system.txt");
98+
fs.writeFileSync(systemPath, "Workflow instructions and tool restrictions.");
99+
vi.stubEnv("GH_AW_PI_SYSTEM_PROMPT", systemPath);
100+
const launch = launcher();
101+
await runPiSubagent(agent, "Read.", { cwd: dir }, undefined, (...args) => {
102+
const prompt = fs.readFileSync(args[1].at(-1), "utf8");
103+
expect(prompt).toContain("Workflow instructions and tool restrictions.\n\nRead only.");
104+
expect(prompt).toContain("The parent is responsible for finalizing the workflow");
105+
expect(prompt).toContain("Do not emit noop safe outputs");
106+
expect(prompt).toContain("Safe-output actions required to perform the delegated task remain permitted");
107+
return launch(...args);
108+
});
109+
});
110+
96111
it("propagates nonzero child exits", async () => {
97112
await expect(runPiSubagent(agent, "Read.", { cwd: dir }, undefined, launcher({}, 1))).rejects.toThrow();
98113
});
@@ -171,13 +186,14 @@ process.stdin.on("end", () => {
171186
GH_AW_SUB_AGENT_EXT: ".md",
172187
GH_AW_PI_SUBAGENT_ARGS: originalEnv.GH_AW_PI_SUBAGENT_ARGS,
173188
GH_AW_PI_TOOL_POLICY: originalEnv.GH_AW_PI_TOOL_POLICY,
189+
GH_AW_PI_MODEL_ALIASES: JSON.stringify({ "gpt-5-mini": ["copilot/gpt-5*mini*"], "gpt-5-nano": ["copilot/gpt-5*nano*"] }),
174190
};
175191
vi.stubGlobal("core", { info: vi.fn() });
176192
const source = fs.readFileSync(new URL("../../../.github/workflows/smoke-pi-sub-agents.md", import.meta.url), "utf8");
177193
const parentPrompt = writeInlineSubAgents(source, dir, dir, "pi");
178194
expect(parentPrompt).not.toContain("## agent:");
179195
stagePiArtifacts(process.env.PI_CODING_AGENT_DIR);
180-
const expected = ["claude-haiku-4.5", "gpt-5-mini", "gpt-5-nano"];
196+
const expected = ["claude-haiku-4.5", "gpt-5-mini", "gpt-4o-mini"];
181197
const sdk = {
182198
parseFrontmatter: content => {
183199
const [, header, body] = content.split("---");
@@ -197,7 +213,7 @@ process.stdin.on("end", () => {
197213
agentDir: process.env.PI_CODING_AGENT_DIR,
198214
sdk,
199215
provider: "github-copilot",
200-
catalog: expected.map(model => `github-copilot/${model}`),
216+
catalog: [...expected, "gpt-5.4-mini"].map(model => `github-copilot/${model}`),
201217
gateway: true,
202218
parentModel: "github-copilot/gpt-5.3-codex",
203219
});
@@ -221,7 +237,7 @@ process.stdin.on("end", () => {
221237
process.env.GH_AW_PI_COMMAND = executable;
222238
for (const agent of agents) {
223239
const result = await runPiSubagent(agent, "who am i?", { cwd: dir });
224-
expect(result.content[0].text).toBe(agent.declaredModel);
240+
expect(result.content[0].text).toBe(agent.modelId);
225241
expect(result.details.model).toBe(agent.modelId);
226242
}
227243
} finally {

‎actions/setup/js/pi_tool_policy.cjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function piToolPolicy(pi) {
6868
callCount = reservePiBudget("calls", callCount, limit);
6969
let reason;
7070
if (limit !== undefined && callCount > limit) reason = `Workflow tool-call budget exhausted (${limit} calls)`;
71+
else if (process.env.GH_AW_PI_SUBAGENT_CHILD === "1" && /(?:^|[^a-z0-9])(?:noop|report_incomplete|create_report_incomplete_issue)$/i.test(event.toolName || "")) reason = "Workflow-completion reporting is owned by the parent agent";
7172
else if (event.toolName === "bash" && !isPiBashAllowed(event.input?.command, policy.bash)) reason = "Command is outside the workflow tools.bash allowlist";
7273
else if (policy.edit === false && ["edit", "write"].includes(event.toolName)) reason = "File editing is disabled by the workflow";
7374
if (!reason) return;

‎actions/setup/js/pi_tool_policy.test.cjs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ describe("Pi tool policy", () => {
102102
for (const toolName of ["edit", "write"]) expect(handlers.tool_call({ toolName }, {})).toMatchObject({ block: true });
103103
});
104104

105+
it("blocks child workflow-completion reporting without blocking task safe outputs", () => {
106+
vi.stubEnv("GH_AW_PI_SUBAGENT_CHILD", "1");
107+
const child = {};
108+
policyExtension({ on: (event, handler) => (child[event] = handler) });
109+
for (const toolName of ["noop", "safeoutputs__report_incomplete", "safeoutputs__create_report_incomplete_issue"]) {
110+
expect(child.tool_call({ toolName }, {})).toMatchObject({ block: true, reason: expect.stringContaining("parent agent") });
111+
}
112+
expect(child.tool_call({ toolName: "safeoutputs__create_issue" }, {})).toBeUndefined();
113+
114+
vi.stubEnv("GH_AW_PI_SUBAGENT_CHILD", "");
115+
const parent = {};
116+
policyExtension({ on: (event, handler) => (parent[event] = handler) });
117+
expect(parent.tool_call({ toolName: "safeoutputs__noop" }, {})).toBeUndefined();
118+
});
119+
105120
it("aborts inference at the configured repeated-denial threshold", () => {
106121
const originalExitCode = process.exitCode;
107122
vi.stubEnv("GH_AW_PI_TOOL_POLICY", '{"bash":false}');

‎docs/src/content/docs/engines/pi.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ An omitted model inherits the parent's configured model. Bare model IDs use the
143143

144144
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.
145145

146+
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.
147+
146148
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.
147149

148150
## Additional providers

0 commit comments

Comments
 (0)