Implement support for output schemas (#411)

* tweak examples

* tweak prompt

* Implement support for output schemas

* fix: add example for structured output with zod schema

* tweak

* remove redundant cast

* fix input name

* strip $schema

* hack around vendor requirement

* clarify required result output

---------

Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
This commit is contained in:
Mateusz Burzyński
2026-03-05 23:07:03 +00:00
committed by pullfrog[bot]
parent fafe930c77
commit 5684cbef77
11 changed files with 5981 additions and 89 deletions
+29 -1
View File
@@ -1,4 +1,5 @@
// changes to tool permissions should be reflected in wiki/granular-tools.md
import * as core from "@actions/core";
import { initToolState, startMcpHttpServer, type ToolState } from "./mcp/server.ts";
import { computeModes } from "./modes.ts";
import {
@@ -37,6 +38,23 @@ export interface MainResult {
result?: string | undefined;
}
function resolveOutputSchema(): Record<string, unknown> | undefined {
const raw = core.getInput("output_schema");
if (!raw) return undefined;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
throw new Error(`invalid output_schema: not valid JSON`);
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`invalid output_schema: must be a JSON object`);
}
log.info("» structured output schema provided — output will be required");
return parsed as Record<string, unknown>;
}
async function writeJobSummary(toolState: ToolState): Promise<void> {
const usageSummary = formatUsageSummary(toolState.usageEntries);
const summaryParts = [toolState.lastProgressBody, usageSummary].filter(Boolean);
@@ -151,6 +169,8 @@ export async function main(): Promise<MainResult> {
const modes = [...computeModes(), ...runContext.repoSettings.modes];
const outputSchema = resolveOutputSchema();
// mcpServerUrl and tmpdir are set after server starts — delegate tool reads them at call time
const toolContext = {
repo: runContext.repo,
@@ -169,7 +189,7 @@ export async function main(): Promise<MainResult> {
mcpServerUrl: "",
tmpdir,
};
await using mcpHttpServer = await startMcpHttpServer(toolContext);
await using mcpHttpServer = await startMcpHttpServer(toolContext, { outputSchema });
toolContext.mcpServerUrl = mcpHttpServer.url;
log.info(`» MCP server started at ${mcpHttpServer.url}`);
timer.checkpoint("mcpServer");
@@ -178,6 +198,7 @@ export async function main(): Promise<MainResult> {
payload,
repo: runContext.repo,
modes,
outputSchema,
});
// log instructions as soon as they are fully resolved
const logParts = [
@@ -236,6 +257,13 @@ export async function main(): Promise<MainResult> {
toolState.usageEntries.push(result.usage);
}
// validate this before writing job summary to avoid masking the error
if (outputSchema && !toolState.output) {
throw new Error(
"output_schema was provided but agent did not call set_output — structured output is required"
);
}
await writeJobSummary(toolState);
// emit structured output marker for test validation