fix(agent): steered override-schema subagents

- Marked eval agent schema calls as caller overrides so subagent prompts can revoke native output/yield instructions.\n- Added override-schema prompt guidance telling agents to ignore conflicting native output labels and terminal-yield the caller schema object.\n- Added prompt coverage for the override notice.\n\nRefs #3926
This commit is contained in:
roboomp
2026-06-30 22:30:15 +00:00
parent ca3acaa61a
commit 00ef58f843
5 changed files with 36 additions and 0 deletions
@@ -235,10 +235,12 @@ describe("runEvalAgent", () => {
expect(firstOptions.signal).toBe(abortController.signal);
expect(firstOptions.parentActiveModelPattern).toBe("p/current");
expect(firstOptions.outputSchema).toBe(schema);
expect(firstOptions.outputSchemaOverridesAgent).toBe(true);
expect(firstOptions.assignment).toBe("hello");
expect(firstOptions.description).toBe("My Agent");
expect(firstOptions.modelOverride).toEqual(["p/override"]);
expect(secondOptions.outputSchema).toBeUndefined();
expect(secondOptions.outputSchemaOverridesAgent).toBeUndefined();
});
it("forces LSP off for bridge subagents even when task.enableLsp is on", async () => {
@@ -371,6 +371,7 @@ export async function runEvalAgent(args: unknown, options: EvalAgentBridgeOption
parentActiveModelPattern,
thinkingLevel: effectiveAgent.thinkingLevel,
outputSchema: structured ? parsed.schema : undefined,
outputSchemaOverridesAgent: structured,
sessionFile,
persistArtifacts: Boolean(sessionFile),
artifactsDir,
@@ -61,6 +61,10 @@ Yield protocol:
This is your only way to return a final result. For structured results, you NEVER put JSON in plain text or substitute a text summary for `result.data`.
{{#if outputSchemaOverridesAgent}}
Caller schema overrides agent-native output instructions. Ignore ROLE-provided output/yield labels, field names, examples, and procedures that conflict with the interface below. Use ONLY labels/fields from the caller schema; safest path: omit `type` and terminal-yield the full `result.data` object.
{{/if}}
{{#if outputSchema}}
Your result MUST match this TypeScript interface:
```ts
@@ -295,6 +295,11 @@ export interface ExecutorOptions {
parentActiveModelPattern?: string;
thinkingLevel?: ThinkingLevel;
outputSchema?: unknown;
/**
* Caller supplied a schema that supersedes the agent's native output prompt.
* Eval `agent(..., schema=...)` sets this so built-in agents ignore stale yield labels.
*/
outputSchemaOverridesAgent?: boolean;
/** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
taskDepth?: number;
/**
@@ -2141,6 +2146,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
planReferencePath: options.planReference?.path ?? "",
worktree: worktree ?? "",
outputSchema: normalizedOutputSchema,
outputSchemaOverridesAgent: options.outputSchemaOverridesAgent === true,
ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
ircSelfId: ircEnabled ? id : "",
});
@@ -0,0 +1,23 @@
import { describe, expect, it } from "bun:test";
import { prompt } from "@oh-my-pi/pi-utils";
import "../../src/config/prompt-templates";
import subagentSystemPromptTemplate from "../../src/prompts/system/subagent-system-prompt.md" with { type: "text" };
describe("subagent system prompt", () => {
it("revokes native output labels when caller schema overrides the agent", () => {
const out = prompt.render(subagentSystemPromptTemplate, {
agent: 'Use incremental yield with type: ["findings"].',
outputSchemaOverridesAgent: true,
outputSchema: {
properties: {
issue_key: { type: "string" },
verdict: { enum: ["clean", "blockers"] },
},
},
});
expect(out).toContain("Caller schema overrides agent-native output instructions");
expect(out).toContain("Ignore ROLE-provided output/yield labels");
expect(out).toContain("omit `type` and terminal-yield the full `result.data` object");
});
});