mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
fix(runner): read saved API responses in bounded text pages
Keep large response artifacts inspectable through the existing authenticated API route. Preserve company and run authority, UTF-8 boundaries, and transfer limits. Reproduce the artifact loop through real HTTP and add an explicit full-stack fixture. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
6131a5858f
commit
c2744f77b4
16 files changed
+249
-12
No files matched your search
@@ -1042,3 +1042,15 @@ keys with low request limits. It can only lower the configured campaign limit.
|
||||
Keep subscription qualification separate from API-key results.
|
||||
|
||||
The explicit-only eight-cell [continuation accounting baseline](CONTINUATION-ACCOUNTING.md) tests productive work, bounded repair, restart and late gates with real providers.
|
||||
|
||||
### Bounded API response reading
|
||||
|
||||
`api-response-reading` is an explicit-only, two-cell native Codex suite (local
|
||||
and Daytona). Each cell creates synthetic diagnostic evidence over 24 KiB via
|
||||
the public API, outside the agent assignment. The browser starts one ordinary
|
||||
task which must read the saved response in bounded text pages and persist the
|
||||
exact hidden evidence code. The oracle also requires completed API tool events;
|
||||
missing events or a narrative completion cannot pass. Existing run, copyback,
|
||||
screenshot, billing and environment cleanup checks apply. Use
|
||||
`--id api-response-reading.runner-codex.daytona.saved-text-pages` with an
|
||||
immutable Daytona image; no private hooks or fixture database writes are used.
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { apiResponseReadingTask, responseEvidenceCode, responseEvidenceDescription, successfulApiReadCount } from "./api-response-reading.js";
|
||||
|
||||
describe("large API response evidence fixture", () => {
|
||||
it("keeps evidence beyond both preview and inline limits and out of the assignment", () => {
|
||||
const source = responseEvidenceDescription("fixture");
|
||||
const code = responseEvidenceCode("fixture");
|
||||
expect(Buffer.byteLength(source)).toBeGreaterThan(24 * 1024);
|
||||
expect(source.indexOf(code)).toBeGreaterThan(24 * 1024);
|
||||
expect(apiResponseReadingTask.buildPrompt("fixture")).not.toContain(code);
|
||||
expect(apiResponseReadingTask.buildPrompt("fixture")).toContain("responseText.nextOffsetBytes");
|
||||
});
|
||||
it("counts only real completed API tool events, never narrative claims or missing evidence", () => {
|
||||
const event = { eventType: "tool.execution.completed", payload: { prpEvent: { payload: { name: "call_api", status: "completed" } } } };
|
||||
expect(successfulApiReadCount([event])).toBe(1);
|
||||
expect(successfulApiReadCount([])).toBe(0);
|
||||
expect(successfulApiReadCount([{ ...event, eventType: "item.delta" }, { ...event, payload: null }, { ...event, payload: { prpEvent: { payload: { name: "call_api", status: "failed" } } } }])).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { RunnerTaskFixture } from "./types.js";
|
||||
|
||||
export function responseEvidenceCode(nonce: string) {
|
||||
return createHash("sha256").update(`bounded-response-evidence:${nonce}`).digest("hex");
|
||||
}
|
||||
export function responseEvidenceDescription(nonce: string) {
|
||||
return `${"Synthetic diagnostic padding.\n".repeat(1400)}\nEvidence code: ${responseEvidenceCode(nonce)}\n`;
|
||||
}
|
||||
|
||||
export function successfulApiReadCount(events: readonly { eventType?: string; payload?: unknown }[]) {
|
||||
return events.filter(event => {
|
||||
const outer = event.payload as { prpEvent?: { payload?: { name?: string; status?: string } } } | null;
|
||||
const payload = outer?.prpEvent?.payload;
|
||||
return event.eventType === "tool.execution.completed" && payload?.name === "call_api" && payload.status === "completed";
|
||||
}).length;
|
||||
}
|
||||
|
||||
export const apiResponseReadingTask: RunnerTaskFixture = {
|
||||
id: "saved-text-pages", label: "Read saved large API evidence", groups: [],
|
||||
workMode: "standard", flow: "single_turn", expectedRunCount: 1,
|
||||
attemptTimeoutMs: { local: 10 * 60_000, daytona: 10 * 60_000 },
|
||||
expectedTerminalState: { issue: "done", run: "succeeded" },
|
||||
buildTitle: nonce => `Read large API evidence ${nonce}`,
|
||||
buildVisibleMarker: nonce => `API_RESPONSE_READ_${nonce}`,
|
||||
buildPrompt: nonce => [
|
||||
"Inspect diagnostic evidence task {{API_RESPONSE_SOURCE_ID}} using the Paperclip API tools.",
|
||||
"Discover GET /api/issues/{id}, call it for that task, and retain the returned response artifact.",
|
||||
"Read the saved artifact through GET /api/assets/{assetId}/content with responseText: {offsetBytes:0,limitBytes:8192}.",
|
||||
"Continue using responseText.nextOffsetBytes until null. Extract the Evidence code at the end of its description.",
|
||||
"The large response must be read with bounded responseText pages. Do not use other tools or API routes to obtain the evidence; if bounded reading fails, report the failure instead of substituting a different reader.",
|
||||
"Write only that code followed by a newline to api-response-proof.txt in the current workspace. Do not edit the evidence task or create child tasks.",
|
||||
`Verify the proof file and finish this task with paperclip_finish, reportedWorkDisposition done, and summary API_RESPONSE_READ_${nonce}.`,
|
||||
].join("\n"),
|
||||
buildMatchers: (nonce, execution) => [
|
||||
{ kind: "file_exact", path: "api-response-proof.txt", expected: `${responseEvidenceCode(nonce)}\n` },
|
||||
{ kind: "message_contains", expected: `API_RESPONSE_READ_${nonce}` },
|
||||
{ kind: "issue_status", expected: "done" },
|
||||
{ kind: "run_status", expected: "succeeded" },
|
||||
{ kind: "runtime_mode", expected: execution.profile.expectedRuntimeMode },
|
||||
{ kind: "environment", expected: execution.environment.id },
|
||||
],
|
||||
};
|
||||
@@ -71,10 +71,10 @@ describe("runner E2E catalog", () => {
|
||||
expect(localIntegrityTasks).toHaveLength(2);
|
||||
expect(openRouterBreadthTasks).toHaveLength(3);
|
||||
expect(runnerSuites.map((suite) => suite.expectedMatrixSize)).toEqual([
|
||||
8, 46, 23, 47, 52, 28, 18, 6, 6, 42, 14, 10, 2,
|
||||
2, 8, 46, 23, 47, 52, 28, 18, 6, 6, 42, 14, 10, 2,
|
||||
]);
|
||||
expect(validateRunnerCatalog()).toHaveLength(302);
|
||||
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(302);
|
||||
expect(validateRunnerCatalog()).toHaveLength(304);
|
||||
expect(new Set(runnerMatrix.map((entry) => entry.id)).size).toBe(304);
|
||||
expect(
|
||||
runnerMatrix.filter((entry) => entry.suite.id === "core-compatibility"),
|
||||
).toHaveLength(42);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { apiResponseReadingTask } from "./api-response-reading.js";
|
||||
import { accountingTasks } from "./accounting-cases.js";
|
||||
import { continuationTasks } from "./continuation-cases.js";
|
||||
import { lifecycleLiveTasks, lifecycleLiveDefinitionDigest } from "./lifecycle-live-cases.js";
|
||||
@@ -911,6 +912,14 @@ const everydayProfiles = [
|
||||
].map(productionStoryProfile);
|
||||
|
||||
export const runnerSuites: readonly RunnerSuiteFixture[] = [
|
||||
{
|
||||
id: "api-response-reading", label: "Bounded API response reading", manualOnly: true,
|
||||
description: "Retrieve evidence beyond a saved API preview through authorized bounded text windows.",
|
||||
groups: [], environments: runnerEnvironments,
|
||||
profiles: runnerProfiles.filter(profile => profile.id === "runner-codex"),
|
||||
tasks: [apiResponseReadingTask], expectedMatrixSize: 2,
|
||||
definitionMetadata: { version: 1, grading: "hidden-evidence-exact-copy-and-api-tool-events", scheduling: "explicit-only" },
|
||||
},
|
||||
{
|
||||
id: "continuation-accounting", label: "Continuation accounting baseline", manualOnly: true,
|
||||
description: "Structured productive steps, bounded repair, restart and late gates; comments cannot buy more attempts.",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { responseEvidenceDescription, successfulApiReadCount } from "./api-response-reading.js";
|
||||
import { observeBrowserBootstrap } from "./browser-bootstrap-diagnostics.js";
|
||||
import { runAccountingFlow } from "./accounting-flow.js";
|
||||
import type { Issue } from "../../packages/shared/src/types/issue.js";
|
||||
@@ -794,6 +795,14 @@ for (const execution of executions) {
|
||||
daytonaImage: process.env.PAPERCLIP_E2E_DAYTONA_IMAGE,
|
||||
});
|
||||
|
||||
if (execution.suite.id === "api-response-reading") {
|
||||
const source = await api.post<{ id: string }>(`/api/companies/${fixtures.company.id}/issues`, {
|
||||
title: `Synthetic diagnostic evidence ${nonce}`,
|
||||
description: responseEvidenceDescription(nonce), status: "backlog",
|
||||
});
|
||||
prompt = prompt.replaceAll("{{API_RESPONSE_SOURCE_ID}}", source.id);
|
||||
}
|
||||
|
||||
if (execution.suite.id === "lifecycle-baseline" && lifecycleLiveCase(execution.task.id)?.family === "blocker") {
|
||||
const prerequisite = await api.post<{ id: string }>(`/api/companies/${fixtures.company.id}/issues`, {
|
||||
title: `Supply dataset ${nonce}`,
|
||||
@@ -1885,6 +1894,11 @@ for (const execution of executions) {
|
||||
}
|
||||
}
|
||||
|
||||
if (execution.suite.id === "api-response-reading") {
|
||||
const completedReads = successfulApiReadCount(runEventsByRun.flatMap(captured => captured.events));
|
||||
if (completedReads < 3) invariantFailures.push(`Expected source read and multiple bounded artifact reads; observed ${completedReads} completed API calls`);
|
||||
}
|
||||
|
||||
const context = record(run.contextSnapshot);
|
||||
const environmentContext = record(context.paperclipEnvironment);
|
||||
const workspaceContext = record(context.paperclipWorkspace);
|
||||
|
||||
Reference in new issue
Block a user