/** * `submit_result`: the host-side way a run hands back its structured result. * * A run whose task is bound to the prompt-slot role `context` is refused every * sandbox tool under the enforcing modes, so it cannot write the structured * result file itself. The tool takes the value as its input, validates it, and * the host writes the file the file-based path would have left. */ import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { expect } from "@std/expect"; import { join } from "@std/path"; import { normalize } from "@std/path/posix"; import { createCfHarnessCliCapabilities } from "../src/cli.ts"; import { parseCfHarnessCliArgs, runCfHarnessCli } from "./support/on-linux.ts"; import { CfHarnessEngine } from "../src/engine.ts"; import { CfHarnessPromptLoop, promptLoopModelTurnsFromError, } from "../src/prompt-loop.ts"; import { CAPABILITY_PROBE_SENTINEL } from "../src/diagnostics.ts"; import { CFC_PROMPT_SLOT_BOUND_ATOM_TYPE, type PromptSlotBinding, } from "../src/contracts/prompt-slot.ts"; import { parentToolIdsForBacking, withheldToolIds, } from "../src/contracts/tool-descriptor.ts"; import { createHarnessHandleTable, mintAddressHandle, } from "../src/handle-table.ts"; import { createToolOutputId } from "../src/contracts/tool-result.ts"; import { submitResultTool } from "../src/tools/submit-result.ts"; import type { HarnessToolContext } from "../src/tools/types.ts"; import type { SandboxCommandRequest, SandboxCommandResult, SandboxRuntime, SandboxRuntimeDescription, SandboxShellRequest, } from "../src/sandbox/types.ts"; import { chatViewOfRequest, responsesBodyFromChatFixture, } from "./support/responses-fixture.ts"; // A pattern's task text: context for the run, not an operator's command. const contextPromptSlotBinding: PromptSlotBinding = { type: CFC_PROMPT_SLOT_BOUND_ATOM_TYPE, source: { type: "test.prompt-slot", subject: "submit-result" }, role: "context", kernelName: "cf-harness", surface: "test", subject: "submit-result", eventId: "event-submit-result", }; const RESULT_SCHEMA = { type: "object", properties: { answer: { type: "string" }, source: { type: "string" }, }, required: ["answer"], additionalProperties: false, } as const; const NO_BACKING = { fabricSessionAvailable: false, patternIndexAvailable: false, skillsShSearchAvailable: false, skillsShAcquisitionAvailable: false, skillRegistryAvailable: false, docsCorpusAvailable: false, }; class FakeSandboxRuntime implements SandboxRuntime { describe(): SandboxRuntimeDescription { return { kind: "runsc-cfc", defaultWorkingDirectory: this.defaultWorkingDirectory(), cfc: { runtimeRequested: true, workspaceMountPath: "/workspace" }, }; } resolvePath(path: string, cwd = this.defaultWorkingDirectory()): string { return normalize(path.startsWith("/") ? path : `${cwd}/${path}`); } isPathWithinWorkspace(path: string): boolean { return path === "/workspace" || path.startsWith("/workspace/"); } isPathWithinAllowedRoots(path: string): boolean { return this.isPathWithinWorkspace(path); } defaultWorkingDirectory(): string { return "/workspace"; } run(_request: SandboxCommandRequest): Promise { return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }); } runShell(request: SandboxShellRequest): Promise { if (request.command.includes(CAPABILITY_PROBE_SENTINEL)) { return Promise.resolve({ stdout: "bash\tpresent\t/bin/bash\tGNU bash, version 5.2.26(1)-release", stderr: "", exitCode: 0, }); } return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 }); } } const toolCallTurn = ( id: string, name: string, argumentsText: string, ) => ({ choices: [{ index: 0, message: { role: "assistant", content: "", tool_calls: [{ id, type: "function", function: { name, arguments: argumentsText }, }], }, }], }); const finalTurn = (content: string) => ({ choices: [{ index: 0, message: { role: "assistant", content } }], }); const scriptedFetch = ( payloads: readonly unknown[], requestCount: { value: number }, requests: ReturnType[] = [], ): typeof fetch => async (_input, init) => { requests.push(chatViewOfRequest(JSON.parse(String(init?.body ?? "{}")))); const payload = payloads[requestCount.value]; requestCount.value += 1; if (payload === undefined) { throw new Error("scripted fetch ran out of payloads"); } return await Promise.resolve( new Response(JSON.stringify(responsesBodyFromChatFixture(payload)), { status: 200, }), ); }; /** Every tool message's parsed content, in order. */ const toolOutputs = ( transcript: readonly { role: string; content?: string }[], ): Record[] => transcript.filter((message) => message.role === "tool").map((message) => JSON.parse(message.content ?? "{}") ); describe("submit_result", () => { let dir: string; let resultPath: string; beforeEach(async () => { dir = await Deno.makeTempDir({ prefix: "submit-result-test-" }); resultPath = join(dir, "workspace", "result.json"); }); afterEach(async () => { await Deno.remove(dir, { recursive: true }); }); /** Runs `turns` under the `context` role at the default enforcement mode. */ const run = async ( runId: string, turns: readonly unknown[], options: { schema?: boolean; allowedToolIds?: string[]; stopOnStructuredResult?: boolean; maxModelTurns?: number; finalizeOnTurnLimit?: boolean; } = {}, ) => { const requests: ReturnType[] = []; const engine = new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), runId, model: "gpt-5.4", ...(options.schema === false ? {} : { structuredResult: { schema: RESULT_SCHEMA, path: resultPath }, }), }); const loop = new CfHarnessPromptLoop({ apiKey: "test-key", engine, ...(options.stopOnStructuredResult !== undefined ? { stopOnStructuredResult: options.stopOnStructuredResult } : {}), ...(options.allowedToolIds !== undefined // deno-lint-ignore no-explicit-any ? { allowedToolIds: options.allowedToolIds as any } : {}), ...(options.maxModelTurns !== undefined ? { maxModelTurns: options.maxModelTurns } : {}), ...(options.finalizeOnTurnLimit !== undefined ? { finalizeOnTurnLimit: options.finalizeOnTurnLimit } : {}), fetchFn: scriptedFetch(turns, { value: 0 }, requests), }); const result = await loop.runPrompt({ prompt: "Recommend a book.", promptSlotBinding: contextPromptSlotBinding, }); const offeredTools = requests.map((request) => request.tools); return { result, requests, offeredTools, engine }; }; const submit = (id: string, result: unknown) => toolCallTurn(id, "submit_result", JSON.stringify({ result })); it("ends a host-configured job on a valid final-turn submission without another model call", async () => { const { result, offeredTools } = await run("last-turn-submit", [ submit("call-1", { answer: "Hyperion" }), ], { stopOnStructuredResult: true, maxModelTurns: 1 }); expect(result.modelTurns).toBe(1); expect(offeredTools).toHaveLength(1); expect(result.runState.status).toBe("completed"); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("does not end a job on an invalid final-turn submission", async () => { let failure: unknown; try { await run("last-turn-invalid", [submit("call-1", { invalid: true })], { stopOnStructuredResult: true, maxModelTurns: 1, }); } catch (error) { failure = error; } expect(failure).toBeInstanceOf(Error); expect(promptLoopModelTurnsFromError(failure)).toBe(1); await expect(Deno.stat(resultPath)).rejects.toThrow(Deno.errors.NotFound); }); for (const stopOnStructuredResult of [false, true]) { it(`requires a standalone submission only for a job that stops on its result (${stopOnStructuredResult})`, async () => { const mixed = submit("call-1", { answer: "Premature" }); mixed.choices[0].message.tool_calls.push({ id: "call-other", type: "function", function: { name: "describe_handle", arguments: "{}" }, }); const { result } = await run("mixed-submit", [ mixed, submit("call-2", { answer: "Complete" }), finalTurn("Done."), ], { stopOnStructuredResult }); // Without the rule, the accepted submission ends the run on its turn. expect(result.modelTurns).toBe(stopOnStructuredResult ? 2 : 1); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: stopOnStructuredResult ? "Complete" : "Premature", }); const first = toolOutputs(result.transcript)[0]; if (stopOnStructuredResult) { expect(first).toMatchObject({ reason: "invalid-argument", toolId: "submit_result", field: "toolCalls", expected: "submit_result as the only tool call in this model turn", }); } else { expect(first).toMatchObject({ status: "ok", replaced: false }); } }); } it("does not persist a final-turn result that shares its turn with another tool", async () => { const mixed = submit("call-1", { answer: "Premature" }); mixed.choices[0].message.tool_calls.push({ id: "call-other", type: "function", function: { name: "describe_handle", arguments: "{}" }, }); await expect(run("mixed-last-turn", [mixed], { stopOnStructuredResult: true, maxModelTurns: 1, })).rejects.toThrow("exceeded max model turns"); await expect(Deno.stat(resultPath)).rejects.toThrow(Deno.errors.NotFound); }); /** One `submit_result` call, as an assistant turn carries it. */ const submitCall = (id: string, result: unknown) => ({ id, type: "function", function: { name: "submit_result", arguments: JSON.stringify({ result }) }, }); /** An assistant turn writing `content` beside `calls`. */ const submitting = ( content: string, ...calls: ReturnType[] ) => ({ choices: [{ index: 0, message: { role: "assistant", content, tool_calls: calls }, }], }); it("is withheld from a run that configures no structured-result schema", async () => { expect(withheldToolIds(NO_BACKING).has("submit_result")).toBe(true); expect(parentToolIdsForBacking(NO_BACKING)).not.toContain("submit_result"); expect( parentToolIdsForBacking({ ...NO_BACKING, structuredResultAvailable: true, }), ).toContain("submit_result"); const { result, offeredTools } = await run("run-no-schema", [ submit("call-1", { answer: "Hyperion" }), finalTurn("Done."), ], { schema: false, allowedToolIds: ["submit_result", "describe_handle"] }); expect(offeredTools[0]).not.toContain("submit_result"); // A tool outside the run's surface is refused as not allowed. expect(toolOutputs(result.transcript)[0]).toMatchObject({ type: "cf-harness.observation-denied", }); await expect(Deno.stat(resultPath)).rejects.toThrow(Deno.errors.NotFound); }); it("offers the tool when a schema is configured, under an explicit allowlist too", async () => { const byDefault = await run("run-offered", [finalTurn("Done.")]); const allowlisted = await run("run-offered-allowlist", [ finalTurn("Done."), ], { allowedToolIds: ["describe_handle", "submit_result"] }); expect(byDefault.offeredTools[0]).toContain("submit_result"); expect(allowlisted.offeredTools[0]).toEqual([ "describe_handle", "submit_result", ]); }); it("records a valid submission as the structured result, under the `context` role at `enforce-strict`", async () => { const { result, engine } = await run("run-valid", [ submit("call-1", { answer: "Hyperion" }), finalTurn("Done."), ]); expect(engine.getRunState().cfcEnforcementMode).toBe("enforce-strict"); expect(toolOutputs(result.transcript)[0]).toMatchObject({ status: "ok", replaced: false, }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("still refuses the same run a sandbox tool", async () => { const { result } = await run("run-bash-refused", [ toolCallTurn("call-1", "bash", JSON.stringify({ command: "true" })), finalTurn("Done."), ]); expect(toolOutputs(result.transcript)[0]).toMatchObject({ type: "cf-harness.observation-denied", reason: "not-authorized", }); }); it("returns a typed validation error, writes nothing, and takes a corrected resubmission", async () => { const { result } = await run("run-invalid-then-valid", [ submit("call-1", { answer: 7 }), submit("call-2", { answer: "Hyperion" }), finalTurn("Done."), ]); const [refused, accepted] = toolOutputs(result.transcript); expect(refused).toMatchObject({ status: "error", code: "invalid_result" }); expect(typeof refused.message).toBe("string"); expect(accepted).toMatchObject({ status: "ok", replaced: false }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("completes the run on an accepted submission and asks the model for no further turn", async () => { // The turn queued after the submission is the empty one a model gives // when it has nothing left to say, which the run used to fail on. const { result, offeredTools } = await run("run-ends-on-result", [ submit("call-1", { answer: "Hyperion" }), finalTurn(""), ]); expect(offeredTools).toHaveLength(1); expect(result.runState.status).toBe("completed"); expect(result.runState.terminalReason).toBe("assistant_completed"); expect(result.runState.failureRecords ?? []).toEqual([]); expect(result.finalAssistantText).toBe(""); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("completes with the accepted submission where a further model turn would fail", async () => { // The script holds no turn after the submission, so asking the provider // for one throws. const { result, offeredTools } = await run("run-result-then-failure", [ submit("call-1", { answer: "Hyperion" }), ]); expect(offeredTools).toHaveLength(1); expect(result.runState.status).toBe("completed"); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("returns the words written beside an accepted submission as the final answer", async () => { const { result } = await run("run-result-with-words", [ submitting( "Hyperion it is.", submitCall("call-1", { answer: "Hyperion" }), ), ]); expect(result.finalAssistantText).toBe("Hyperion it is."); expect(result.modelTurns).toBe(1); }); it("answers a first blank turn with a notice and completes on the submission after it", async () => { const { result, requests } = await run("run-blank-then-submit", [ finalTurn(""), submit("call-1", { answer: "Hyperion" }), ]); expect(requests).toHaveLength(2); expect(result.runState.status).toBe("completed"); expect(requests[1].messages.at(-1)).toMatchObject({ role: "user", content: "Host completion check: the last turn carried no words and no tool call. Call submit_result with the answer, or with why it cannot be given.", }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", }); }); it("fails the run on a second blank turn", async () => { await expect( run("run-blank-twice", [finalTurn(""), finalTurn(" \n")]), ).rejects.toMatchObject({ name: "HarnessControlError", code: "provider-unavailable", message: "The model returned an empty assistant response with no tool calls", }); }); it("fails a blank turn at once where the turn after it is the reserved final one", async () => { // The finalizing turn offers no tools, so a notice to call submit_result // would ask for what that turn cannot do. await expect( run("run-blank-before-final", [finalTurn(""), finalTurn("Partial.")], { maxModelTurns: 2, finalizeOnTurnLimit: true, }), ).rejects.toMatchObject({ code: "provider-unavailable" }); }); it("fails a blank turn at once where it is the last turn of the budget", async () => { await expect( run("run-blank-last-turn", [finalTurn("")], { maxModelTurns: 1 }), ).rejects.toMatchObject({ code: "provider-unavailable" }); }); it("fails a blank turn at once where the run takes no result", async () => { await expect( run("run-blank-no-schema", [finalTurn(""), finalTurn("Done.")], { schema: false, }), ).rejects.toMatchObject({ code: "provider-unavailable" }); }); it("gives the model another turn after a refused submission", async () => { const { result, offeredTools } = await run("run-refused-continues", [ submit("call-1", { answer: 7 }), finalTurn("I could not form the result."), ]); expect(offeredTools).toHaveLength(2); expect(result.finalAssistantText).toBe("I could not form the result."); await expect(Deno.stat(resultPath)).rejects.toThrow(Deno.errors.NotFound); }); it("leaves no file behind a submission that was only ever invalid", async () => { await run("run-only-invalid", [ submit("call-1", { wrong: true }), finalTurn("Done."), ]); await expect(Deno.stat(resultPath)).rejects.toThrow(Deno.errors.NotFound); }); it("replaces an earlier submission with a later valid one in the same turn", async () => { const { result } = await run("run-replace", [ submitting( "", submitCall("call-1", { answer: "Hyperion" }), submitCall("call-2", { answer: "Ubik" }), ), ]); expect(toolOutputs(result.transcript)[1]).toMatchObject({ status: "ok", replaced: true, }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Ubik", }); }); it("restores result configuration when a resumed run overwrites an existing result", async () => { const first = await run("run-resumed-result", [ submit("call-1", { answer: "Hyperion" }), finalTurn("Done."), ]); const resumed = new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), runState: first.engine.getRunState(), }); const { output } = await resumed.invokeBuiltinTool("submit_result", { result: { answer: "Ubik" }, }); expect(output).toMatchObject({ status: "ok", replaced: true }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Ubik", }); }); it("refuses a conflicting result configuration on resume", async () => { const first = await run("run-conflicting-resumed-result", [ finalTurn("Done."), ]); expect(() => new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), runState: first.engine.getRunState(), structuredResult: { schema: RESULT_SCHEMA, path: `${resultPath}.different`, }, }) ).toThrow( "resumed run structured-result configuration does not match the recorded configuration", ); }); it("keeps a handle token in the result a token", async () => { const engine = new CfHarnessEngine({ sandboxRuntime: new FakeSandboxRuntime(), runId: "run-token", model: "gpt-5.4", structuredResult: { schema: RESULT_SCHEMA, path: resultPath }, }); const minted = await mintAddressHandle( createHarnessHandleTable("run-token"), "/of:fid1:rszomt4Ti8MwDNpMJLZCjcENRtAH6MFBxgf0wYVNCs8/title", ); await engine.recordHandleTable(minted.table); const token = minted.token; const loop = new CfHarnessPromptLoop({ apiKey: "test-key", engine, fetchFn: scriptedFetch([ submit("call-1", { answer: "Hyperion", source: token }), finalTurn("Done."), ], { value: 0 }), }); await loop.runPrompt({ prompt: "Recommend a book.", promptSlotBinding: contextPromptSlotBinding, }); expect(JSON.parse(await Deno.readTextFile(resultPath))).toEqual({ answer: "Hyperion", source: token, }); }); it("accepts the result tool in the CLI allowlist and capability description", async () => { const parsed = await parseCfHarnessCliArgs([ "--workspace", dir, "--prompt", "Recommend a book.", "--allow-tool", "describe_handle", "--allow-tool", "submit_result", "--structured-result-schema", JSON.stringify(RESULT_SCHEMA), "--structured-result-path", "result.json", ], { env: {} }); expect(parsed).toMatchObject({ allowedToolIds: ["describe_handle", "submit_result"], }); expect(createCfHarnessCliCapabilities().parentToolIds).toContain( "submit_result", ); }); it("restores and validates the result contract when the CLI resumes a run", async () => { const runId = "run-resumed-result"; const runRoot = join(dir, "artifacts", runId); const runState = { runId, status: "failed" as const, createdAt: "2026-09-20T12:00:00.000Z", updatedAt: "2026-09-20T12:00:01.000Z", cfcEnforcementMode: "enforce-strict" as const, currentDir: "/workspace", policyEvents: [], toolOutputs: [], structuredResult: { schema: RESULT_SCHEMA, path: resultPath }, }; const transcript = [{ role: "user" as const, content: "Recommend a book.", }]; const errors: string[] = []; const reads: string[] = []; let resultAvailable: boolean | undefined; await Deno.mkdir(join(dir, "workspace")); await Deno.writeTextFile( resultPath, JSON.stringify({ answer: "Hyperion" }), ); const exitCode = await runCfHarnessCli([ "--resume-run", runRoot, "--workspace", join(dir, "workspace"), "--gateway-auth-mode", "none", ], { env: {}, io: { stdout: () => {}, stderr: (text) => errors.push(text) }, readTextFile: (path) => { reads.push(path); return Deno.readTextFile(path); }, readRunArtifacts: () => Promise.resolve({ runRoot, runStatePath: join(runRoot, "run-state.json"), transcriptPath: join(runRoot, "transcript.json"), runState, transcript, }), createPromptLoop: ({ engine }) => { resultAvailable = engine?.structuredResultAvailable; return { runPrompt: () => Promise.reject(new Error("unexpected fresh run")), runTranscript: ({ transcript: resumed }) => { expect(resumed).toEqual(transcript); return Promise.resolve({ model: "gpt-5.4", modelTurns: 1, finalAssistantText: "Done.", transcript: [...resumed], runState: { ...runState, status: "completed" }, }); }, }; }, }); expect(errors).toEqual([]); expect(exitCode).toBe(0); expect(resultAvailable).toBe(true); expect(reads).toEqual([await Deno.realPath(resultPath)]); }); it("returns `not_configured` when invoked outside a run that takes a result", async () => { const output = await submitResultTool.invoke( { nextOutputId: (toolId: string) => createToolOutputId("run-direct", toolId, 1), } as unknown as HarnessToolContext, { result: { answer: "Hyperion" } }, ); expect(output).toMatchObject({ status: "error", code: "not_configured" }); }); });