mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
feat(runner): add live eval session entrypoint
This commit is contained in:
1 parent
dcefccc876
commit
eac93e3af2
5 files changed
+107
No files matched your search
@@ -4,6 +4,9 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"paperclip-runner-eval-session": "./dist/cli/eval-session.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -21,6 +24,10 @@
|
||||
"types": "./dist/live/index.d.ts",
|
||||
"import": "./dist/live/index.js"
|
||||
},
|
||||
"./devtools": {
|
||||
"types": "./dist/devtools/index.d.ts",
|
||||
"import": "./dist/devtools/index.js"
|
||||
},
|
||||
"./browser": {
|
||||
"types": "./dist/browser/index.d.ts",
|
||||
"import": "./dist/browser/index.js"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { projectCapabilityDevtools } from "../devtools/index.js";
|
||||
import { PAPERCLIP_RUNNER_BUILD_METADATA } from "../evals/build-metadata.js";
|
||||
import {
|
||||
CapabilityLiveSessionService,
|
||||
reconcileCapabilityLiveUsage,
|
||||
type CreateCapabilityLiveSessionInput,
|
||||
} from "../live/live-session.js";
|
||||
|
||||
interface EvalSessionRequest {
|
||||
schema: "paperclip-runner/eval-session-request/v1";
|
||||
attemptId: string;
|
||||
prompt: string;
|
||||
model: string;
|
||||
runnerd: { path: string; sha256: string };
|
||||
limits: { turnTimeoutMs: number; maxProviderCalls: number; maxCostNanodollars: number };
|
||||
session: CreateCapabilityLiveSessionInput;
|
||||
}
|
||||
|
||||
function argument(name: string): string {
|
||||
const index = process.argv.indexOf(name);
|
||||
const value = index < 0 ? undefined : process.argv[index + 1];
|
||||
if (!value) throw new Error(`missing ${name}`);
|
||||
return resolve(value);
|
||||
}
|
||||
|
||||
async function sha256(path: string): Promise<string> {
|
||||
return createHash("sha256").update(await readFile(path)).digest("hex");
|
||||
}
|
||||
|
||||
const requestPath = argument("--request");
|
||||
const outputPath = argument("--output");
|
||||
const request = JSON.parse(await readFile(requestPath, "utf8")) as EvalSessionRequest;
|
||||
if (request.schema !== "paperclip-runner/eval-session-request/v1") throw new Error("unsupported request schema");
|
||||
const runnerdPath = resolve(request.runnerd.path);
|
||||
const actualDigest = await sha256(runnerdPath);
|
||||
if (actualDigest !== request.runnerd.sha256.replace(/^sha256:/, "")) {
|
||||
throw new Error(`runnerd digest mismatch: expected ${request.runnerd.sha256}, got sha256:${actualDigest}`);
|
||||
}
|
||||
|
||||
const service = new CapabilityLiveSessionService({ transportOptions: { runnerBinary: runnerdPath } });
|
||||
let sessionId: string | null = null;
|
||||
try {
|
||||
const session = await service.create({
|
||||
...request.session,
|
||||
attemptId: request.attemptId,
|
||||
runId: request.session.runId ?? request.attemptId,
|
||||
requestedModel: request.model,
|
||||
turnTimeoutMs: request.limits.turnTimeoutMs,
|
||||
});
|
||||
sessionId = session.id;
|
||||
const turn = await session.sendMessage(request.prompt);
|
||||
await session.completeAttempt(turn.status === "completed" ? "succeeded" : "failed", `turn_${turn.status}`);
|
||||
const snapshot = session.snapshot();
|
||||
const usage = reconcileCapabilityLiveUsage(snapshot);
|
||||
if (usage.providerCalls > request.limits.maxProviderCalls) throw new Error("provider call limit exceeded");
|
||||
if (usage.costNanodollars > request.limits.maxCostNanodollars) throw new Error("cost limit exceeded");
|
||||
await writeFile(outputPath, `${JSON.stringify({
|
||||
schema: "paperclip-runner/eval-session-artifact/v1",
|
||||
attemptId: request.attemptId,
|
||||
build: PAPERCLIP_RUNNER_BUILD_METADATA,
|
||||
runnerd: { path: "[withheld]", sha256: `sha256:${actualDigest}` },
|
||||
requestedModel: request.model,
|
||||
turn,
|
||||
snapshot,
|
||||
devtools: projectCapabilityDevtools(snapshot),
|
||||
usage,
|
||||
}, null, 2)}\n`);
|
||||
} catch (error) {
|
||||
await writeFile(outputPath, `${JSON.stringify({
|
||||
schema: "paperclip-runner/eval-session-artifact/v1",
|
||||
attemptId: request.attemptId,
|
||||
infrastructureError: error instanceof Error ? error.message : String(error),
|
||||
build: PAPERCLIP_RUNNER_BUILD_METADATA,
|
||||
runnerd: { path: "[withheld]", sha256: `sha256:${actualDigest}` },
|
||||
requestedModel: request.model,
|
||||
}, null, 2)}\n`);
|
||||
process.exitCode = 2;
|
||||
} finally {
|
||||
if (sessionId !== null) await service.shutdown(sessionId).catch(() => undefined);
|
||||
}
|
||||
@@ -272,6 +272,7 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
const session = await service.create({
|
||||
runId: "run-live-tool-loop",
|
||||
sessionId: "session-live-tool-loop",
|
||||
requestedModel: "gpt-5.4-mini-2026-03-17",
|
||||
});
|
||||
|
||||
const first = await session.sendMessage("Please report progress on this mock issue.");
|
||||
@@ -281,6 +282,10 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
expect(second.assistantText).toContain("stateRevision");
|
||||
expect(first.snapshot.providerThreadId).toBe(second.snapshot.providerThreadId);
|
||||
expect(first.snapshot.providerModel).toEqual({ id: "gpt-eval-test", provider: "openai" });
|
||||
expect(first.snapshot.config.requestedModel).toBe("gpt-5.4-mini-2026-03-17");
|
||||
expect(
|
||||
state.transports[0]?.requests.find((entry) => entry.method === "thread/start")?.params.model,
|
||||
).toBe("gpt-5.4-mini-2026-03-17");
|
||||
expect(session.mockState().comments.at(-1)?.body).toBe(
|
||||
"Progress persisted through the live Codex tool loop.",
|
||||
);
|
||||
|
||||
@@ -88,6 +88,8 @@ export interface CapabilityLiveAuthoritySnapshot {
|
||||
|
||||
export interface CapabilityLiveSessionConfigSnapshot {
|
||||
workingDirectory: string;
|
||||
/** Exact model requested by the caller; omitted only for legacy interactive sessions. */
|
||||
requestedModel?: string;
|
||||
scenario: CapabilitySemanticScenarioPolicy;
|
||||
capabilities: string[];
|
||||
explicitClaims: string[];
|
||||
@@ -184,6 +186,7 @@ export interface CapabilityLiveSessionSnapshot {
|
||||
export interface CreateCapabilityLiveSessionInput {
|
||||
seed?: CapabilityFixtureSeed | CapabilityFixtureState;
|
||||
workingDirectory?: string;
|
||||
requestedModel?: string;
|
||||
scenario?: CapabilitySemanticScenarioPolicy;
|
||||
capabilities?: string[];
|
||||
explicitClaims?: string[];
|
||||
@@ -592,6 +595,9 @@ export class CapabilityLiveSessionService {
|
||||
authority,
|
||||
config: {
|
||||
workingDirectory: input.workingDirectory ?? process.cwd(),
|
||||
...(input.requestedModel === undefined
|
||||
? {}
|
||||
: { requestedModel: requireNonEmpty(input.requestedModel, "requested_model") }),
|
||||
scenario,
|
||||
capabilities,
|
||||
explicitClaims: authority.explicitClaims,
|
||||
@@ -1342,6 +1348,7 @@ export class CapabilityLiveSession {
|
||||
}
|
||||
const resumed = await this.#transport.request("thread/resume", {
|
||||
threadId: this.#providerThreadId,
|
||||
...(this.#config.requestedModel === undefined ? {} : { model: this.#config.requestedModel }),
|
||||
cwd: this.#config.workingDirectory,
|
||||
config: createSkilllessCodexThreadConfig(this.#config.workingDirectory),
|
||||
permissions: CODEX_PERMISSION_PROFILE,
|
||||
@@ -1366,6 +1373,7 @@ export class CapabilityLiveSession {
|
||||
} else {
|
||||
const opened = await this.#transport.request("thread/start", {
|
||||
cwd: this.#config.workingDirectory,
|
||||
...(this.#config.requestedModel === undefined ? {} : { model: this.#config.requestedModel }),
|
||||
config: createSkilllessCodexThreadConfig(this.#config.workingDirectory),
|
||||
permissions: CODEX_PERMISSION_PROFILE,
|
||||
runtimeWorkspaceRoots: [this.#config.workingDirectory],
|
||||
@@ -1389,6 +1397,7 @@ export class CapabilityLiveSession {
|
||||
providerSessionId: this.#providerSessionId,
|
||||
mode: "live_codex",
|
||||
runner: "paperclip-runnerd",
|
||||
requestedModel: this.#config.requestedModel ?? null,
|
||||
controlPlane: "mock",
|
||||
});
|
||||
this.#pump = this.#pumpNotifications();
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from "./mock-core/capability-mock-control-plane-adapter.js";
|
||||
export * from "./protocol/conformance-fixture.js";
|
||||
export * from "./live/live-session.js";
|
||||
export * from "./live/durable-live-session-store.js";
|
||||
export * from "./devtools/index.js";
|
||||
Reference in new issue
Block a user