feat: enable permission-aware subagents

This commit is contained in:
叶林立
2026-08-26 10:49:26 +08:00
parent d3bf562189
commit 7571ba6dd9
185 changed files with 48365 additions and 26 deletions
@@ -0,0 +1,108 @@
/**
* subagent-error-status-e2e.test.ts — regression for issue #144: a subagent
* whose final assistant turn is a provider error must be reported as a
* failure, not as "completed" with an empty (or stale) result.
*
* Full-stack: real pi loader + real extension + real runAgent + real child
* sessions on a faux model. Faux is the point, not a shortcut — the scenario is
* a provider error with zero content, which no live model will produce on
* request. Each run pins `live: false` so the pre-publish smoke's global
* `PI_E2E_LIVE=1` can't swap a real model in and turn this suite red.
*/
import { fauxAssistantMessage, fauxText, fauxToolCall } from "@earendil-works/pi-ai";
import type { AgentSession } from "@earendil-works/pi-coding-agent";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
agentCall,
type PrintModeRun,
routeBySession,
runPrintMode,
} from "./helpers/print-mode-runner.js";
/** Text of the parent's Agent tool result — what the orchestrator LLM sees. */
function agentToolResult(session: AgentSession): string {
const msg = [...session.messages].reverse().find(
(m) => m.role === "toolResult" && (m as { toolName?: string }).toolName === "Agent",
);
return ((msg?.content ?? []) as Array<{ text?: string }>).map((b) => b.text ?? "").join("");
}
vi.setConfig({ testTimeout: 30_000 });
// Not matched by pi's transient-error patterns → no auto-retry, deterministic.
const FATAL = "invalid request: provider rejected the prompt";
describe("issue #144 — empty-error final turns must not be 'completed'", () => {
let run: PrintModeRun | undefined;
afterEach(async () => {
await run?.dispose();
run = undefined;
});
it("a run whose ONLY turn errors with no output is a failure, not an empty success", async () => {
run = await runPrintMode({
prompt: "Delegate.",
respond: routeBySession({
parentInitial: agentCall({ run_in_background: false, description: "doomed", prompt: "Do work." }),
parentFinal: "parent done",
// The child's one and only turn: provider error, zero content.
subagent: () => fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL }),
}),
live: false,
});
// DESIRED: the orchestrator sees a failure naming the provider error —
// not a clean success reading "No output.".
const toolResult = agentToolResult(run.parentSession);
expect(toolResult).toContain(FATAL);
expect(toolResult).not.toContain("No output.");
});
it("an earlier turn's text must not mask a failed final turn as a fresh success", async () => {
run = await runPrintMode({
prompt: "Delegate.",
respond: routeBySession({
parentInitial: agentCall({ run_in_background: false, description: "masked", prompt: "Do work." }),
parentFinal: "parent done",
subagent: (ctx) => {
const hasToolResult = ctx.messages.some((m) => m.role === "toolResult");
// Turn 1: real text + a tool call. Turn 2 (after the tool result):
// provider error with zero content.
return hasToolResult
? fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL })
: fauxAssistantMessage([
fauxText("EARLIER-PARTIAL-TEXT"),
fauxToolCall("bash", { command: "echo hi" }),
]);
},
}),
live: false,
});
// The orchestrator sees the failure (not the earlier text as a clean
// answer), AND the partial output is salvaged, clearly labeled as
// pre-failure so it can't be mistaken for the final answer.
const toolResult = agentToolResult(run.parentSession);
expect(toolResult).toContain(FATAL);
expect(toolResult).toContain("Partial output before the failure:");
expect(toolResult).toContain("EARLIER-PARTIAL-TEXT");
// The failure headline comes before the salvaged partial output.
expect(toolResult.indexOf(FATAL)).toBeLessThan(toolResult.indexOf("EARLIER-PARTIAL-TEXT"));
});
it("a pure empty-error run shows no 'partial output' section", async () => {
run = await runPrintMode({
prompt: "Delegate.",
respond: routeBySession({
parentInitial: agentCall({ run_in_background: false, description: "empty", prompt: "Do work." }),
parentFinal: "parent done",
subagent: () => fauxAssistantMessage([], { stopReason: "error", errorMessage: FATAL }),
}),
live: false,
});
const toolResult = agentToolResult(run.parentSession);
expect(toolResult).toContain(FATAL);
expect(toolResult).not.toContain("Partial output before the failure:");
});
});