Add the native runner session runtime (#12352)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Paperclip Runner package needs one provider-neutral session loop > - Native execution contracts now define the accepted input and output > - Backends still need bounded execution, recovery, and control-plane ports > - This pull request adds that package-local session runtime > - It does not change server runtime selection or start the experimental runner > - The benefit is a tested execution core for later provider and server layers ## Linked Issues or Issue Description **Subsystem affected** packages/paperclip-runner **Problem or motivation** The runner contracts do not yet have a shared session loop. Each backend would otherwise duplicate event handoff, terminal handling, recovery cursors, timeouts, and checkpoint behavior. **Proposed solution** Add the native session runtime, backend interfaces, control-plane port, harness driver contract, deterministic conformance helpers, and bounded tests. **Alternatives considered** The server could own this loop. That would mix provider process behavior with server persistence and authority logic. **Roadmap alignment** This is part of the existing experimental Paperclip Runner rollout. It does not enable a production adapter. ## What Changed - Add a provider-neutral native session execution loop. - Add recovery cursor reconciliation and checkpoint hooks. - Add bounded timeout and governed-wait behavior. - Add harness driver and control-plane conformance helpers. - Add deterministic backend and session runtime tests. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` - `pnpm --filter @paperclipai/paperclip-runner typecheck:typescript` - `pnpm -r typecheck` - `pnpm build` - Protocol generation and manifest checks pass. - The branch changes 14 files relative to its declared base. ## Risks The main risk is a session that hangs, duplicates an event, or restores the wrong cursor after reconnect. Tests cover terminal events, timeouts, durable handoff, duplicate receipts, recovery, continuity breaks, and checkpoint updates. The change remains package-local. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex, `gpt-5`, with agentic reasoning, tool use, and code execution. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change and contains no internal Paperclip ticket id - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
d8dfa27a1e
commit
d18e281c33
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"schema": "paperclip-runner/harness-driver-conformance-fixture/v1",
|
||||
"validConfig": {
|
||||
"schema": "paperclip-runner/deterministic-harness-driver-config/v1",
|
||||
"scenario": "semantic-tool-success"
|
||||
},
|
||||
"invalidConfig": {
|
||||
"schema": "paperclip-runner/deterministic-harness-driver-config/v1",
|
||||
"scenario": "unknown"
|
||||
},
|
||||
"completionMessage": "Run deterministic semantic-tool conformance.",
|
||||
"interruptMessage": "[conformance:interrupt] Keep the turn active until interrupted.",
|
||||
"requiredCapabilities": [
|
||||
"resume",
|
||||
"typedEvents",
|
||||
"interruption",
|
||||
"structuredResult",
|
||||
"read",
|
||||
"reconciliation",
|
||||
"usage",
|
||||
"dynamicTools"
|
||||
],
|
||||
"unsupportedFeatures": [
|
||||
"steering",
|
||||
"runtimeRequestResolution",
|
||||
"goals",
|
||||
"threadLineage"
|
||||
]
|
||||
}
|
||||
|
|
@ -121,6 +121,12 @@
|
|||
"expectation": "accept",
|
||||
"compatibilityCase": "cross-language-input"
|
||||
},
|
||||
{
|
||||
"path": "fixtures/evals/harness-driver-conformance.json",
|
||||
"sha256": "ce84eb64faa6af78e17a156cde352a951db7c108c8819a20109b732b23a5bec5",
|
||||
"expectation": "accept",
|
||||
"compatibilityCase": "canonical"
|
||||
},
|
||||
{
|
||||
"path": "fixtures/local-runner/scripts/duplicate-terminal.json",
|
||||
"sha256": "56928a21318ea4b390c65fb34f0d3114760619d79301f4d535391a8566b693ab",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,451 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { HarnessDriver, HarnessSession, PersistedHarnessSession } from "../contracts/harness-driver.js";
|
||||
import type { PrpEvent, PrpStructuredRunResult } from "../protocol/replay-contract.js";
|
||||
import { HarnessDriverBackend } from "./harness-driver-backend.js";
|
||||
|
||||
const result: PrpStructuredRunResult = {
|
||||
schema: "paperclip.run_result.v1",
|
||||
reportedWorkDisposition: "done",
|
||||
summary: "Backend adapter completed.",
|
||||
completionClaim: {
|
||||
contractRevision: "1",
|
||||
objectiveSatisfied: true,
|
||||
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [] }],
|
||||
remainingWork: [],
|
||||
},
|
||||
evidence: [],
|
||||
verification: [{ commandOrCheck: "fake", status: "passed" }],
|
||||
attentionRequests: [],
|
||||
artifacts: [],
|
||||
};
|
||||
|
||||
const providerIdentity = {
|
||||
kind: "acpx" as const,
|
||||
normalizedSessionId: "session-1",
|
||||
acpxRecordId: "driver-1",
|
||||
backendSessionId: "backend-1",
|
||||
agentSessionId: "provider-1",
|
||||
profileDigest: "sha256:profile",
|
||||
workspaceDigest: "sha256:workspace",
|
||||
requestedModel: "claude-sonnet-4-20250514",
|
||||
effectiveModel: "claude-sonnet-4-20250514",
|
||||
};
|
||||
|
||||
function prpEvent(sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent {
|
||||
return {
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: `fake:${sourceSeq}`,
|
||||
sourceSeq,
|
||||
sourceInstanceId: "fake",
|
||||
sourceKind: "runner",
|
||||
runId: "run-1",
|
||||
normalizedSessionId: "session-1",
|
||||
turnId: "turn-1",
|
||||
eventType,
|
||||
schemaVersion: 1,
|
||||
priority: 0,
|
||||
emittedAt: `2026-08-09T00:00:0${sourceSeq}.000Z`,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
const runtimeResolutions: unknown[] = [];
|
||||
|
||||
class FakeHarnessSession implements HarnessSession {
|
||||
ids() { return { driverSessionId: "driver-1", providerSessionId: "provider-1" }; }
|
||||
async *events() {
|
||||
yield prpEvent(1, "run.result.proposed", result);
|
||||
yield prpEvent(2, "turn.completed", { status: "completed" });
|
||||
}
|
||||
async startTurn() { return { turnId: "turn-1" }; }
|
||||
async resolveRuntimeRequest(input: unknown) {
|
||||
runtimeResolutions.push(structuredClone(input));
|
||||
}
|
||||
async snapshot(): Promise<PersistedHarnessSession> {
|
||||
return {
|
||||
driverKind: "fake",
|
||||
driverSessionId: "driver-1",
|
||||
providerSessionId: "provider-1",
|
||||
providerIdentity,
|
||||
providerRecoveryPolicy: "allow_replacement_after_governed_wait",
|
||||
semanticResult: { result, fingerprint: "fingerprint", turnId: "turn-1" },
|
||||
lastSourceSequence: 2,
|
||||
};
|
||||
}
|
||||
async close() {}
|
||||
}
|
||||
|
||||
const driver: HarnessDriver = {
|
||||
async descriptor() {
|
||||
return {
|
||||
kind: "fake",
|
||||
displayName: "Fake harness",
|
||||
version: "1",
|
||||
capabilities: {
|
||||
resume: false,
|
||||
typedEvents: true,
|
||||
steering: false,
|
||||
interruption: false,
|
||||
structuredResult: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
async openSession() { return new FakeHarnessSession(); },
|
||||
};
|
||||
|
||||
describe("HarnessDriverBackend", () => {
|
||||
it("rejects and closes a provider session without a durable provider identity", async () => {
|
||||
let closed = false;
|
||||
class MissingProviderIdentitySession extends FakeHarnessSession {
|
||||
override ids() {
|
||||
return {
|
||||
driverSessionId: "driver-missing-provider",
|
||||
providerSessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
override async close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
const incompleteDriver: HarnessDriver = {
|
||||
...driver,
|
||||
async openSession() {
|
||||
return new MissingProviderIdentitySession();
|
||||
},
|
||||
};
|
||||
const backend = new HarnessDriverBackend(incompleteDriver);
|
||||
|
||||
await expect(backend.openSession({
|
||||
identity: {
|
||||
runId: "run-incomplete",
|
||||
sessionId: "session-incomplete",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
agentId: "agent-1",
|
||||
},
|
||||
workingDirectory: "/workspace",
|
||||
})).rejects.toThrow(
|
||||
"provider_initialize_protocol_error: provider=fake stage=session.open missing durable provider session identity",
|
||||
);
|
||||
expect(closed).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects and closes a recovered session without a durable provider identity", async () => {
|
||||
let closed = false;
|
||||
class MissingRecoveredIdentitySession extends FakeHarnessSession {
|
||||
override ids() {
|
||||
return {
|
||||
driverSessionId: "driver-missing-recovered-provider",
|
||||
providerSessionId: null,
|
||||
};
|
||||
}
|
||||
|
||||
override async close() {
|
||||
closed = true;
|
||||
}
|
||||
}
|
||||
const incompleteDriver: HarnessDriver = {
|
||||
...driver,
|
||||
async recoverSession() {
|
||||
return {
|
||||
recovered: true,
|
||||
session: new MissingRecoveredIdentitySession(),
|
||||
};
|
||||
},
|
||||
};
|
||||
const backend = new HarnessDriverBackend(incompleteDriver);
|
||||
|
||||
await expect(backend.recoverSession({
|
||||
backendKind: "runner",
|
||||
driverKind: "fake",
|
||||
sessionId: "driver-1",
|
||||
providerSessionId: "provider-1",
|
||||
identity: {
|
||||
runId: "run-recover-incomplete",
|
||||
sessionId: "session-recover-incomplete",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
agentId: "agent-1",
|
||||
},
|
||||
cursor: "0",
|
||||
}, {
|
||||
signal: new AbortController().signal,
|
||||
})).rejects.toThrow(
|
||||
"provider_initialize_protocol_error: provider=fake stage=session.recover missing durable provider session identity",
|
||||
);
|
||||
expect(closed).toBe(true);
|
||||
});
|
||||
|
||||
it("normalizes harness events, result, terminal, and snapshot", async () => {
|
||||
const backend = new HarnessDriverBackend(driver);
|
||||
const session = await backend.openSession({
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
const events: PrpEvent[] = [];
|
||||
for await (const event of session.events()) events.push(event);
|
||||
expect(events).toHaveLength(2);
|
||||
await expect(session.result()).resolves.toMatchObject({ result, turnId: "turn-1", terminal: { runTerminalState: "succeeded" } });
|
||||
await expect(session.snapshot()).resolves.toMatchObject({
|
||||
driverKind: "fake",
|
||||
sessionId: "driver-1",
|
||||
providerSessionId: "provider-1",
|
||||
providerIdentity,
|
||||
providerRecoveryPolicy: "allow_replacement_after_governed_wait",
|
||||
});
|
||||
});
|
||||
|
||||
it("passes the persisted harness driver kind through recovery", async () => {
|
||||
let recoveredDriverKind: string | null = null;
|
||||
let recoveredProviderIdentity: PersistedHarnessSession["providerIdentity"];
|
||||
const recoveryDriver: HarnessDriver = {
|
||||
...driver,
|
||||
async recoverSession(snapshot) {
|
||||
recoveredDriverKind = snapshot.driverKind;
|
||||
recoveredProviderIdentity = snapshot.providerIdentity;
|
||||
return { recovered: true, session: new FakeHarnessSession() };
|
||||
},
|
||||
};
|
||||
const backend = new HarnessDriverBackend(recoveryDriver);
|
||||
const recovery = await backend.recoverSession({
|
||||
backendKind: "runner",
|
||||
driverKind: "fake",
|
||||
sessionId: "driver-1",
|
||||
providerSessionId: "provider-1",
|
||||
providerIdentity,
|
||||
providerRecoveryPolicy: "allow_replacement_after_governed_wait",
|
||||
identity: { runId: "run-2", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
}, {
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
expect(recovery.recovered).toBe(true);
|
||||
expect(recoveredDriverKind).toBe("fake");
|
||||
expect(recoveredProviderIdentity).toEqual(providerIdentity);
|
||||
});
|
||||
|
||||
it("forwards the native bootstrap signal to the harness driver", async () => {
|
||||
const controller = new AbortController();
|
||||
let receivedSignal: AbortSignal | undefined;
|
||||
const backend = new HarnessDriverBackend({
|
||||
...driver,
|
||||
async openSession(input) {
|
||||
receivedSignal = input.signal;
|
||||
return new FakeHarnessSession();
|
||||
},
|
||||
});
|
||||
|
||||
await backend.openSession({
|
||||
identity: { runId: "run-signal", sessionId: "session-signal", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
expect(receivedSignal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it("forwards the native recovery signal to the harness driver", async () => {
|
||||
const controller = new AbortController();
|
||||
let receivedSignal: AbortSignal | undefined;
|
||||
const backend = new HarnessDriverBackend({
|
||||
...driver,
|
||||
async recoverSession(_snapshot, options) {
|
||||
receivedSignal = options.signal;
|
||||
return { recovered: true, session: new FakeHarnessSession() };
|
||||
},
|
||||
});
|
||||
|
||||
const recovery = await backend.recoverSession({
|
||||
backendKind: "runner",
|
||||
driverKind: "fake",
|
||||
sessionId: "driver-1",
|
||||
providerSessionId: "provider-1",
|
||||
providerIdentity,
|
||||
identity: { runId: "run-recover-signal", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
}, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
expect(recovery.recovered).toBe(true);
|
||||
expect(receivedSignal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it("allows only the run id to change when a harness session is attached", async () => {
|
||||
const attachedRunIds: string[] = [];
|
||||
class AttachableHarnessSession extends FakeHarnessSession {
|
||||
async attachRun(input: { runId: string }) {
|
||||
attachedRunIds.push(input.runId);
|
||||
}
|
||||
}
|
||||
const backend = new HarnessDriverBackend({
|
||||
...driver,
|
||||
async openSession() { return new AttachableHarnessSession(); },
|
||||
});
|
||||
const originalIdentity = {
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
agentId: "agent-1",
|
||||
};
|
||||
const session = await backend.openSession({
|
||||
identity: originalIdentity,
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
|
||||
for (const identity of [
|
||||
{ ...originalIdentity, runId: "run-forged-company", companyId: "company-2" },
|
||||
{ ...originalIdentity, runId: "run-forged-issue", issueId: "issue-2" },
|
||||
{ ...originalIdentity, runId: "run-forged-agent", agentId: "agent-2" },
|
||||
{ ...originalIdentity, runId: "run-forged-session", sessionId: "session-2" },
|
||||
]) {
|
||||
await expect(session.attachRun?.({ identity })).rejects.toThrow(
|
||||
"native_session_attach_binding_mismatch",
|
||||
);
|
||||
expect(session.identity()).toEqual(originalIdentity);
|
||||
}
|
||||
expect(attachedRunIds).toEqual([]);
|
||||
|
||||
await expect(session.attachRun?.({
|
||||
identity: { ...originalIdentity, runId: "run-2" },
|
||||
})).resolves.toBeUndefined();
|
||||
expect(attachedRunIds).toEqual(["run-2"]);
|
||||
expect(session.identity()).toEqual({ ...originalIdentity, runId: "run-2" });
|
||||
});
|
||||
|
||||
it("delegates native runtime-request resolutions to the harness session", async () => {
|
||||
runtimeResolutions.length = 0;
|
||||
const backend = new HarnessDriverBackend(driver);
|
||||
const session = await backend.openSession({
|
||||
identity: {
|
||||
runId: "run-1",
|
||||
sessionId: "session-1",
|
||||
companyId: "company-1",
|
||||
issueId: "issue-1",
|
||||
agentId: "agent-1",
|
||||
},
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
|
||||
await session.resolveRuntimeRequest?.({
|
||||
requestId: "permission-1",
|
||||
turnId: "turn-1",
|
||||
resolution: { action: "accept_for_session" },
|
||||
});
|
||||
|
||||
expect(runtimeResolutions).toEqual([
|
||||
{
|
||||
requestId: "permission-1",
|
||||
turnId: "turn-1",
|
||||
resolution: { action: "accept_for_session" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits one non-replayable input expiration and terminal wait after provider loss", async () => {
|
||||
const questionSet = {
|
||||
schema: "paperclip.question_set.v1" as const,
|
||||
questions: [{ id: "target", prompt: "Which target?", required: true, answerMode: "text" as const }],
|
||||
};
|
||||
class LostProviderSession extends FakeHarnessSession {
|
||||
override async *events() {
|
||||
yield prpEvent(1, "runtime_request.created", { request: {
|
||||
schema: "paperclip.runtime_request.v2",
|
||||
requestKind: "runtime",
|
||||
requestId: "input-1",
|
||||
type: "input",
|
||||
status: "pending",
|
||||
prompt: "Which target?",
|
||||
input: questionSet,
|
||||
turnId: "turn-1",
|
||||
itemId: "input-1",
|
||||
} });
|
||||
throw new Error("provider transport lost");
|
||||
}
|
||||
override async snapshot(): Promise<PersistedHarnessSession> {
|
||||
return { driverKind: "fake", driverSessionId: "driver-1", lastSourceSequence: 1 };
|
||||
}
|
||||
}
|
||||
const backend = new HarnessDriverBackend({ ...driver, async openSession() { return new LostProviderSession(); } });
|
||||
const session = await backend.openSession({
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.created" } });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: {
|
||||
eventType: "runtime_request.expired",
|
||||
sourceSeq: 2,
|
||||
payload: {
|
||||
requestId: "input-1",
|
||||
reason: "provider_process_lost",
|
||||
replayAllowed: false,
|
||||
request: { input: questionSet },
|
||||
},
|
||||
} });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: {
|
||||
eventType: "turn.interrupted",
|
||||
sourceSeq: 3,
|
||||
payload: { reason: "provider_process_lost" },
|
||||
} });
|
||||
await expect(iterator.next()).resolves.toMatchObject({ done: true });
|
||||
});
|
||||
|
||||
it("does not synthesize a fallback after the input was already resolved", async () => {
|
||||
class ResolvedThenLostSession extends FakeHarnessSession {
|
||||
override async *events() {
|
||||
yield prpEvent(1, "runtime_request.created", { request: {
|
||||
schema: "paperclip.runtime_request.v2",
|
||||
requestKind: "runtime",
|
||||
requestId: "input-1",
|
||||
type: "input",
|
||||
status: "pending",
|
||||
prompt: "Which target?",
|
||||
input: { schema: "paperclip.question_set.v1", questions: [{ id: "target", prompt: "Which target?", required: true, answerMode: "text" }] },
|
||||
} });
|
||||
yield prpEvent(2, "runtime_request.resolved", { requestId: "input-1", action: "submit" });
|
||||
throw new Error("provider transport lost after resolution");
|
||||
}
|
||||
}
|
||||
const backend = new HarnessDriverBackend({ ...driver, async openSession() { return new ResolvedThenLostSession(); } });
|
||||
const session = await backend.openSession({
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await iterator.next();
|
||||
await iterator.next();
|
||||
await expect(iterator.next()).rejects.toThrow("provider transport lost after resolution");
|
||||
});
|
||||
|
||||
it("does not synthesize a fallback after explicit run cancellation", async () => {
|
||||
class CancelledProviderSession extends FakeHarnessSession {
|
||||
override async *events() {
|
||||
yield prpEvent(1, "runtime_request.created", { request: {
|
||||
schema: "paperclip.runtime_request.v2",
|
||||
requestKind: "runtime",
|
||||
requestId: "input-1",
|
||||
type: "input",
|
||||
status: "pending",
|
||||
prompt: "Which target?",
|
||||
input: { schema: "paperclip.question_set.v1", questions: [{ id: "target", prompt: "Which target?", required: true, answerMode: "text" }] },
|
||||
} });
|
||||
throw new Error("provider stopped after cancellation");
|
||||
}
|
||||
async interrupt() {}
|
||||
}
|
||||
const backend = new HarnessDriverBackend({ ...driver, async openSession() { return new CancelledProviderSession(); } });
|
||||
const session = await backend.openSession({
|
||||
identity: { runId: "run-1", sessionId: "session-1", companyId: "company-1", issueId: "issue-1", agentId: "agent-1" },
|
||||
workingDirectory: "/workspace",
|
||||
});
|
||||
const iterator = session.events()[Symbol.asyncIterator]();
|
||||
await expect(iterator.next()).resolves.toMatchObject({ value: { eventType: "runtime_request.created" } });
|
||||
await session.cancel({
|
||||
reason: "operator cancelled the run",
|
||||
signal: new AbortController().signal,
|
||||
}).cleanup;
|
||||
await expect(iterator.next()).rejects.toThrow("provider stopped after cancellation");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
import type {
|
||||
HarnessDriver,
|
||||
HarnessSession,
|
||||
HarnessSessionRecoveryOptions,
|
||||
PersistedHarnessSession,
|
||||
} from "../contracts/harness-driver.js";
|
||||
import type {
|
||||
NativeSession,
|
||||
NativeSessionBackend,
|
||||
NativeSessionBackendDescriptor,
|
||||
NativeSessionRecoveryOptions,
|
||||
OpenNativeSessionInput,
|
||||
PersistedNativeSession,
|
||||
} from "../contracts/native-session-backend.js";
|
||||
import type {
|
||||
PrpEvent,
|
||||
PrpTerminalState,
|
||||
} from "../protocol/replay-contract.js";
|
||||
|
||||
/**
|
||||
* Package-owned adapter from the concrete harness driver contract to the
|
||||
* normalized session boundary consumed by Paperclip. Provider mechanics stay
|
||||
* behind HarnessDriver; the control plane sees only PRP events and results.
|
||||
*/
|
||||
export class HarnessDriverBackend implements NativeSessionBackend {
|
||||
readonly #driver: HarnessDriver;
|
||||
|
||||
constructor(driver: HarnessDriver) {
|
||||
this.#driver = driver;
|
||||
}
|
||||
|
||||
async descriptor(): Promise<NativeSessionBackendDescriptor> {
|
||||
const descriptor = await this.#driver.descriptor();
|
||||
return {
|
||||
kind: "runner",
|
||||
name: descriptor.kind,
|
||||
version: descriptor.version,
|
||||
capabilities: structuredClone(descriptor.capabilities),
|
||||
runtimeContextCapabilities: descriptor.runtimeContextCapabilities === undefined ? undefined : structuredClone(descriptor.runtimeContextCapabilities),
|
||||
};
|
||||
}
|
||||
|
||||
async openSession(input: OpenNativeSessionInput): Promise<NativeSession> {
|
||||
const session = await this.#driver.openSession({
|
||||
runId: input.identity.runId,
|
||||
normalizedSessionId: input.identity.sessionId,
|
||||
workingDirectory: input.workingDirectory ?? process.cwd(),
|
||||
...(input.signal === undefined ? {} : { signal: input.signal }),
|
||||
});
|
||||
try {
|
||||
assertProviderSessionIdentity(
|
||||
session,
|
||||
(await this.#driver.descriptor()).kind,
|
||||
"session.open",
|
||||
);
|
||||
} catch (error) {
|
||||
await session.close({
|
||||
reason: "provider session bootstrap returned an incomplete identity",
|
||||
force: true,
|
||||
}).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
return new HarnessNativeSession(input, session);
|
||||
}
|
||||
|
||||
async recoverSession(
|
||||
snapshot: PersistedNativeSession,
|
||||
options: NativeSessionRecoveryOptions,
|
||||
) {
|
||||
if (this.#driver.recoverSession === undefined) {
|
||||
return { recovered: false, reason: "driver does not support recovery" };
|
||||
}
|
||||
const persisted: PersistedHarnessSession = {
|
||||
driverKind: snapshot.driverKind ?? snapshot.backendKind,
|
||||
driverSessionId: snapshot.sessionId,
|
||||
providerSessionId: snapshot.providerSessionId,
|
||||
...(snapshot.providerIdentity === undefined
|
||||
? {}
|
||||
: { providerIdentity: structuredClone(snapshot.providerIdentity) }),
|
||||
...(snapshot.providerRecoveryPolicy === undefined
|
||||
? {}
|
||||
: { providerRecoveryPolicy: snapshot.providerRecoveryPolicy }),
|
||||
runId: snapshot.identity.runId,
|
||||
normalizedSessionId: snapshot.identity.sessionId,
|
||||
activeTurnId: snapshot.activeTurnId ?? snapshot.terminalTurns?.at(-1)?.turnId ?? null,
|
||||
lastSourceSequence: parseCursor(snapshot.cursor),
|
||||
...(snapshot.semanticResult === undefined || snapshot.semanticResult === null
|
||||
? {}
|
||||
: {
|
||||
semanticResult: {
|
||||
result: snapshot.semanticResult,
|
||||
fingerprint: canonicalJson(snapshot.semanticResult),
|
||||
turnId: snapshot.activeTurnId ?? snapshot.terminalTurns?.at(-1)?.turnId ?? "recovered",
|
||||
},
|
||||
}),
|
||||
terminalTurns: snapshot.terminalTurns ?? [],
|
||||
pendingRuntimeRequests: snapshot.pendingRuntimeRequests ?? [],
|
||||
lineage: snapshot.lineage ?? [],
|
||||
};
|
||||
const recoveryOptions: HarnessSessionRecoveryOptions = {
|
||||
signal: options.signal,
|
||||
};
|
||||
const recovered = await this.#driver.recoverSession(
|
||||
persisted,
|
||||
recoveryOptions,
|
||||
);
|
||||
if (!recovered.recovered || recovered.session === undefined) {
|
||||
return { recovered: false, reason: recovered.reason };
|
||||
}
|
||||
try {
|
||||
assertProviderSessionIdentity(
|
||||
recovered.session,
|
||||
(await this.#driver.descriptor()).kind,
|
||||
"session.recover",
|
||||
);
|
||||
} catch (error) {
|
||||
await recovered.session.close({
|
||||
reason: "provider session recovery returned an incomplete identity",
|
||||
force: true,
|
||||
}).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
recovered: true,
|
||||
session: new HarnessNativeSession(
|
||||
{ identity: snapshot.identity },
|
||||
recovered.session,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function assertProviderSessionIdentity(
|
||||
session: HarnessSession,
|
||||
provider: string,
|
||||
stage: "session.open" | "session.recover",
|
||||
): void {
|
||||
const ids = session.ids();
|
||||
if (
|
||||
typeof ids.driverSessionId !== "string"
|
||||
|| ids.driverSessionId.trim().length === 0
|
||||
|| typeof ids.providerSessionId !== "string"
|
||||
|| ids.providerSessionId.trim().length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
`provider_initialize_protocol_error: provider=${provider} stage=${stage} missing durable provider session identity`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HarnessNativeSession implements NativeSession {
|
||||
#input: OpenNativeSessionInput;
|
||||
readonly #session: HarnessSession;
|
||||
#terminal: PrpTerminalState | null = null;
|
||||
#explicitlyCancelled = false;
|
||||
|
||||
constructor(input: OpenNativeSessionInput, session: HarnessSession) {
|
||||
this.#input = structuredClone(input);
|
||||
this.#session = session;
|
||||
}
|
||||
|
||||
identity() {
|
||||
return structuredClone(this.#input.identity);
|
||||
}
|
||||
|
||||
async capabilities() {
|
||||
return {
|
||||
resume: true,
|
||||
typedEvents: true,
|
||||
steering: this.#session.steer !== undefined,
|
||||
interruption: this.#session.interrupt !== undefined,
|
||||
structuredResult: true,
|
||||
read: this.#session.read !== undefined,
|
||||
reconciliation: this.#session.reconcile !== undefined,
|
||||
usage: this.#session.usage !== undefined,
|
||||
runtimeRequestResolution: this.#session.resolveRuntimeRequest !== undefined,
|
||||
runtimeRequestHandoff: this.#session.handoffRuntimeRequest !== undefined,
|
||||
goals: this.#session.goal !== undefined,
|
||||
threadLineage: this.#session.lineage !== undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async attachRun(input: { identity: OpenNativeSessionInput["identity"] }): Promise<void> {
|
||||
const currentIdentity = this.#input.identity;
|
||||
if (
|
||||
input.identity.sessionId !== currentIdentity.sessionId
|
||||
|| input.identity.companyId !== currentIdentity.companyId
|
||||
|| input.identity.issueId !== currentIdentity.issueId
|
||||
|| input.identity.agentId !== currentIdentity.agentId
|
||||
) {
|
||||
throw new Error("native_session_attach_binding_mismatch");
|
||||
}
|
||||
if (this.#session.attachRun === undefined) {
|
||||
throw new Error("native_session_multi_run_unavailable");
|
||||
}
|
||||
await this.#session.attachRun({ runId: input.identity.runId });
|
||||
this.#input = { ...this.#input, identity: structuredClone(input.identity) };
|
||||
this.#terminal = null;
|
||||
this.#explicitlyCancelled = false;
|
||||
}
|
||||
|
||||
async *events(): AsyncIterable<PrpEvent> {
|
||||
let sourceInstanceId: string | null = null;
|
||||
let lastSourceSequence = 0;
|
||||
let sawTerminal = false;
|
||||
let synthesizedDurableWait = false;
|
||||
let streamFailure: unknown = null;
|
||||
const observedPendingInputs = new Map<string, Record<string, unknown>>();
|
||||
try {
|
||||
for await (const event of this.#session.events()) {
|
||||
sourceInstanceId = event.sourceInstanceId;
|
||||
lastSourceSequence = Math.max(lastSourceSequence, event.sourceSeq);
|
||||
if (event.eventType === "runtime_request.created") {
|
||||
const request = plainRecord(event.payload.request);
|
||||
if (
|
||||
request?.schema === "paperclip.runtime_request.v2"
|
||||
&& request.type === "input"
|
||||
&& typeof request.requestId === "string"
|
||||
) observedPendingInputs.set(request.requestId, structuredClone(request));
|
||||
} else if (["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(event.eventType)) {
|
||||
const requestId = typeof event.payload.requestId === "string" ? event.payload.requestId : null;
|
||||
if (requestId) observedPendingInputs.delete(requestId);
|
||||
}
|
||||
if (event.eventType === "run.terminal") {
|
||||
sawTerminal = true;
|
||||
this.#terminal = structuredClone(event.payload as PrpTerminalState);
|
||||
} else if (["turn.completed", "turn.failed", "turn.interrupted", "turn.cancelled"].includes(event.eventType)) {
|
||||
sawTerminal = true;
|
||||
const snapshot = await this.#session.snapshot();
|
||||
const disposition = snapshot.semanticResult?.result.reportedWorkDisposition ?? "yielded";
|
||||
this.#terminal = {
|
||||
schema: "paperclip.prp.terminal.v1",
|
||||
turnTerminalState:
|
||||
event.eventType === "turn.completed" ? "completed"
|
||||
: event.eventType === "turn.failed" ? "failed"
|
||||
: event.eventType === "turn.interrupted" ? "interrupted"
|
||||
: "cancelled",
|
||||
runTerminalState: event.eventType === "turn.completed" ? "succeeded" : event.eventType === "turn.failed" ? "failed" : "cancelled",
|
||||
reportedWorkDisposition: disposition,
|
||||
};
|
||||
}
|
||||
yield structuredClone(event);
|
||||
}
|
||||
} catch (error) {
|
||||
streamFailure = error;
|
||||
}
|
||||
|
||||
// The provider can disappear while its native RPC is awaiting the user.
|
||||
// Emit one canonical terminal fact and replace the stream failure with a
|
||||
// governed wait; the control plane can materialize the continuation without
|
||||
// ever trying to replay the dead provider request.
|
||||
if (!sawTerminal && !this.#explicitlyCancelled && sourceInstanceId) {
|
||||
const snapshot = await this.#session.snapshot().catch(() => null);
|
||||
let governedWaitTurnId: string | undefined;
|
||||
for (const request of observedPendingInputs.values()) {
|
||||
const sourceSeq = Math.max(lastSourceSequence, snapshot?.lastSourceSequence ?? 0) + 1;
|
||||
lastSourceSequence = sourceSeq;
|
||||
const requestId = String(request.requestId);
|
||||
const turnId = typeof request.turnId === "string" ? request.turnId : undefined;
|
||||
governedWaitTurnId ??= turnId;
|
||||
const itemId = typeof request.itemId === "string" ? request.itemId : requestId;
|
||||
yield {
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: `${sourceInstanceId}:${this.#input.identity.runId}:${sourceSeq}`,
|
||||
sourceSeq,
|
||||
sourceInstanceId,
|
||||
sourceKind: "runner",
|
||||
runId: this.#input.identity.runId,
|
||||
normalizedSessionId: this.#input.identity.sessionId,
|
||||
...(turnId ? { turnId } : {}),
|
||||
itemId,
|
||||
eventType: "runtime_request.expired",
|
||||
schemaVersion: 1,
|
||||
priority: 0,
|
||||
emittedAt: new Date().toISOString(),
|
||||
payload: {
|
||||
requestId,
|
||||
...(turnId ? { turnId } : {}),
|
||||
itemId,
|
||||
requestKind: "runtime",
|
||||
reason: "provider_process_lost",
|
||||
replayAllowed: false,
|
||||
requestType: "input",
|
||||
...(plainRecord(request.origin)?.adapter
|
||||
? { adapter: plainRecord(request.origin)?.adapter }
|
||||
: {}),
|
||||
request: structuredClone(request),
|
||||
},
|
||||
};
|
||||
}
|
||||
if (governedWaitTurnId) {
|
||||
const sourceSeq = Math.max(lastSourceSequence, snapshot?.lastSourceSequence ?? 0) + 1;
|
||||
lastSourceSequence = sourceSeq;
|
||||
synthesizedDurableWait = true;
|
||||
sawTerminal = true;
|
||||
this.#terminal = {
|
||||
schema: "paperclip.prp.terminal.v1",
|
||||
turnTerminalState: "interrupted",
|
||||
runTerminalState: "cancelled",
|
||||
reportedWorkDisposition: "yielded",
|
||||
};
|
||||
yield {
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: `${sourceInstanceId}:${this.#input.identity.runId}:${sourceSeq}`,
|
||||
sourceSeq,
|
||||
sourceInstanceId,
|
||||
sourceKind: "runner",
|
||||
runId: this.#input.identity.runId,
|
||||
normalizedSessionId: this.#input.identity.sessionId,
|
||||
turnId: governedWaitTurnId,
|
||||
eventType: "turn.interrupted",
|
||||
schemaVersion: 1,
|
||||
priority: 0,
|
||||
emittedAt: new Date().toISOString(),
|
||||
payload: { status: "interrupted", reason: "provider_process_lost" },
|
||||
};
|
||||
}
|
||||
}
|
||||
if (streamFailure && !synthesizedDurableWait) throw streamFailure;
|
||||
}
|
||||
|
||||
startTurn(input: Parameters<HarnessSession["startTurn"]>[0]) {
|
||||
return this.#session.startTurn(input);
|
||||
}
|
||||
|
||||
steer(input: { turnId: string; message: { role: "user"; text: string }; correlationId?: string }) {
|
||||
if (this.#session.steer === undefined) throw new Error("steering is unavailable");
|
||||
return this.#session.steer(input);
|
||||
}
|
||||
|
||||
interrupt(input: { turnId?: string; reason?: string }) {
|
||||
if (this.#session.interrupt === undefined) throw new Error("interruption is unavailable");
|
||||
return this.#session.interrupt(input);
|
||||
}
|
||||
|
||||
cancel(input: { reason: string; signal: AbortSignal }) {
|
||||
if (input.signal.aborted) {
|
||||
throw input.signal.reason ?? new Error("native session cancellation aborted");
|
||||
}
|
||||
// This flag is the adapter's synchronous publication boundary. Provider
|
||||
// interruption happens afterward as passive cleanup, so a slow or broken
|
||||
// transport cannot synthesize or publish new accepted output for the turn.
|
||||
this.#explicitlyCancelled = true;
|
||||
const interrupt = this.#session.interrupt;
|
||||
return {
|
||||
cleanup: interrupt === undefined
|
||||
? Promise.resolve()
|
||||
: Promise.resolve().then(() => interrupt.call(this.#session, {
|
||||
reason: input.reason,
|
||||
signal: input.signal,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
resolveRuntimeRequest(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
resolution: Parameters<NonNullable<HarnessSession["resolveRuntimeRequest"]>>[0]["resolution"];
|
||||
}) {
|
||||
if (this.#session.resolveRuntimeRequest === undefined) {
|
||||
throw new Error("native_runtime_request_resolution_unavailable");
|
||||
}
|
||||
return this.#session.resolveRuntimeRequest(input);
|
||||
}
|
||||
|
||||
handoffRuntimeRequest(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
reason: "durable_handoff";
|
||||
signal: AbortSignal;
|
||||
}) {
|
||||
if (this.#session.handoffRuntimeRequest === undefined) {
|
||||
throw new Error("native_runtime_request_handoff_unavailable");
|
||||
}
|
||||
return this.#session.handoffRuntimeRequest(input);
|
||||
}
|
||||
|
||||
async result() {
|
||||
const snapshot = await this.#session.snapshot();
|
||||
if (snapshot.semanticResult === undefined || snapshot.semanticResult === null) {
|
||||
return null;
|
||||
}
|
||||
if (this.#terminal === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
result: structuredClone(snapshot.semanticResult.result),
|
||||
terminal: structuredClone(this.#terminal),
|
||||
turnId: snapshot.semanticResult.turnId,
|
||||
};
|
||||
}
|
||||
|
||||
async snapshot(): Promise<PersistedNativeSession> {
|
||||
const snapshot = await this.#session.snapshot();
|
||||
return {
|
||||
backendKind: "runner",
|
||||
driverKind: snapshot.driverKind,
|
||||
sessionId: snapshot.driverSessionId,
|
||||
identity: structuredClone(this.#input.identity),
|
||||
providerSessionId: snapshot.providerSessionId,
|
||||
...(snapshot.providerIdentity === undefined
|
||||
? {}
|
||||
: { providerIdentity: structuredClone(snapshot.providerIdentity) }),
|
||||
...(snapshot.providerRecoveryPolicy === undefined
|
||||
? {}
|
||||
: { providerRecoveryPolicy: snapshot.providerRecoveryPolicy }),
|
||||
cursor:
|
||||
snapshot.lastSourceSequence === undefined
|
||||
? null
|
||||
: String(snapshot.lastSourceSequence),
|
||||
semanticResult: snapshot.semanticResult?.result ?? null,
|
||||
terminal: this.#terminal,
|
||||
activeTurnId: snapshot.activeTurnId ?? snapshot.semanticResult?.turnId ?? null,
|
||||
terminalTurns: snapshot.terminalTurns ?? [],
|
||||
pendingRuntimeRequests: snapshot.pendingRuntimeRequests ?? [],
|
||||
lineage: snapshot.lineage ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async usage(): Promise<Record<string, unknown> | null> {
|
||||
return this.#session.usage?.() ?? null;
|
||||
}
|
||||
|
||||
close(input: { reason: string }) {
|
||||
return this.#session.close(input);
|
||||
}
|
||||
}
|
||||
|
||||
function plainRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
export function createHarnessDriverBackend(driver: HarnessDriver): NativeSessionBackend {
|
||||
return new HarnessDriverBackend(driver);
|
||||
}
|
||||
|
||||
function parseCursor(cursor: string | null | undefined): number | undefined {
|
||||
if (cursor === undefined || cursor === null || cursor === "") return undefined;
|
||||
const parsed = Number(cursor);
|
||||
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown): string {
|
||||
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
const record = value as Record<string, unknown>;
|
||||
return `{${Object.keys(record)
|
||||
.sort()
|
||||
.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
|
||||
.join(",")}}`;
|
||||
}
|
||||
return JSON.stringify(value) ?? "undefined";
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import {
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { nativeSystemInstructions } from "./runtime-context.js";
|
||||
|
||||
const temporaryRoots: string[] = [];
|
||||
|
||||
function runtimeInput(rootPath: string, entryPath: string): NativeExecutionInput {
|
||||
return {
|
||||
runtimeContext: {
|
||||
prompt: { text: "Paperclip runtime." },
|
||||
instructions: { bundle: { rootPath }, entryPath },
|
||||
},
|
||||
} as unknown as NativeExecutionInput;
|
||||
}
|
||||
|
||||
describe("native runtime context files", () => {
|
||||
afterEach(() => {
|
||||
for (const root of temporaryRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reads an instruction entry contained by its bundle root", () => {
|
||||
const temporaryRoot = mkdtempSync(join(tmpdir(), "paperclip-runtime-context-"));
|
||||
temporaryRoots.push(temporaryRoot);
|
||||
const bundleRoot = join(temporaryRoot, "bundle");
|
||||
mkdirSync(bundleRoot);
|
||||
writeFileSync(join(bundleRoot, "AGENTS.md"), "Stay inside the bundle.\n");
|
||||
|
||||
expect(nativeSystemInstructions(runtimeInput(bundleRoot, "AGENTS.md")))
|
||||
.toContain("Stay inside the bundle.");
|
||||
});
|
||||
|
||||
it("rejects traversal and symlink escapes from the bundle root", () => {
|
||||
const temporaryRoot = mkdtempSync(join(tmpdir(), "paperclip-runtime-context-"));
|
||||
temporaryRoots.push(temporaryRoot);
|
||||
const bundleRoot = join(temporaryRoot, "bundle");
|
||||
mkdirSync(bundleRoot);
|
||||
writeFileSync(join(temporaryRoot, "outside.md"), "outside");
|
||||
symlinkSync(join(temporaryRoot, "outside.md"), join(bundleRoot, "linked.md"));
|
||||
|
||||
expect(() => nativeSystemInstructions(runtimeInput(bundleRoot, "../outside.md")))
|
||||
.toThrow("native_runtime_context_entry_outside_bundle");
|
||||
expect(() => nativeSystemInstructions(runtimeInput(bundleRoot, "linked.md")))
|
||||
.toThrow("native_runtime_context_entry_outside_bundle");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import { readFileSync, realpathSync } from "node:fs";
|
||||
import { isAbsolute, relative, resolve, sep } from "node:path";
|
||||
import { CODEX_SKILLLESS_BASE_INSTRUCTIONS } from "../contracts/codex.js";
|
||||
import type { NativeExecutionInput } from "../contracts/native-execution.js";
|
||||
import { composeNativeSystemInstructions } from "../contracts/runtime-context.js";
|
||||
|
||||
export function nativeSystemInstructions(input: NativeExecutionInput): string {
|
||||
if (!("runtimeContext" in input)) return CODEX_SKILLLESS_BASE_INSTRUCTIONS;
|
||||
const configuredRoot = resolve(input.runtimeContext.instructions.bundle.rootPath);
|
||||
const bundleRoot = realpathSync(configuredRoot);
|
||||
const entryPath = realpathSync(resolve(
|
||||
configuredRoot,
|
||||
input.runtimeContext.instructions.entryPath,
|
||||
));
|
||||
const pathFromRoot = relative(bundleRoot, entryPath);
|
||||
if (
|
||||
pathFromRoot === ".."
|
||||
|| pathFromRoot.startsWith(`..${sep}`)
|
||||
|| isAbsolute(pathFromRoot)
|
||||
) {
|
||||
throw new Error("native_runtime_context_entry_outside_bundle");
|
||||
}
|
||||
const entry = readFileSync(entryPath, "utf8");
|
||||
return composeNativeSystemInstructions(input.runtimeContext, entry);
|
||||
}
|
||||
|
||||
export function nativeTaskConstraints(input: NativeExecutionInput): string[] {
|
||||
const finalResponseConstraint =
|
||||
"Write the complete user-facing final response exactly once before invoking paperclip_finish or paperclip_block. Treat that semantic completion tool as the last action and do not emit a trailing acknowledgement.";
|
||||
if (!("runtimeContext" in input)) {
|
||||
return [
|
||||
"Do not discover or invoke skills.",
|
||||
"Do not call a control-plane API.",
|
||||
finalResponseConstraint,
|
||||
];
|
||||
}
|
||||
return [
|
||||
"Use only the assigned skills and provider-native tools.",
|
||||
"Use Paperclip semantic tools for coordination and finalization.",
|
||||
finalResponseConstraint,
|
||||
];
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import type {
|
||||
CompleteControlPlaneRunInput,
|
||||
ControlPlanePort,
|
||||
OpenControlPlaneRunInput,
|
||||
} from "../contracts/control-plane-port.js";
|
||||
import type { PrpEvent, PrpStructuredRunResult, PrpTerminalState } from "../protocol/replay-contract.js";
|
||||
|
||||
export interface ControlPlanePortConformanceSnapshot {
|
||||
eventCount: number;
|
||||
highestContiguousSourceSeq: number;
|
||||
duplicateDisposition: "duplicate";
|
||||
terminalReplayIdempotent: boolean;
|
||||
openBindingRejected: boolean;
|
||||
eventIdMutationRejected: boolean;
|
||||
eventSequenceMutationRejected: boolean;
|
||||
replayBindingRejected: boolean;
|
||||
resultMutationRejected: boolean;
|
||||
}
|
||||
|
||||
export interface ControlPlanePortConformanceHarness {
|
||||
port: ControlPlanePort;
|
||||
start?(): Promise<void>;
|
||||
stop?(): Promise<void>;
|
||||
}
|
||||
|
||||
export const CONTROL_PLANE_CONFORMANCE_OPEN: OpenControlPlaneRunInput = {
|
||||
identity: {
|
||||
runId: "00000000-0000-4000-8000-000000000006",
|
||||
sessionId: "session-standalone-conformance",
|
||||
companyId: "00000000-0000-4000-8000-000000000001",
|
||||
issueId: "00000000-0000-4000-8000-000000000003",
|
||||
agentId: "00000000-0000-4000-8000-000000000002",
|
||||
},
|
||||
backendKind: "mock",
|
||||
sourceInstanceId: "runner-standalone-conformance",
|
||||
};
|
||||
|
||||
export const CONTROL_PLANE_CONFORMANCE_RESULT: PrpStructuredRunResult = {
|
||||
schema: "paperclip.run_result.v1",
|
||||
reportedWorkDisposition: "done",
|
||||
summary: "Standalone conformance task completed.",
|
||||
completionClaim: {
|
||||
contractRevision: "standalone-v1",
|
||||
objectiveSatisfied: true,
|
||||
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: ["event:2"] }],
|
||||
remainingWork: [],
|
||||
},
|
||||
evidence: [{ kind: "event", ref: "event:2" }],
|
||||
verification: [{ commandOrCheck: "standalone-conformance", status: "passed", artifactRef: "event:2" }],
|
||||
attentionRequests: [],
|
||||
artifacts: [],
|
||||
};
|
||||
|
||||
export const CONTROL_PLANE_CONFORMANCE_TERMINAL: PrpTerminalState = {
|
||||
schema: "paperclip.prp.terminal.v1",
|
||||
turnTerminalState: "completed",
|
||||
runTerminalState: "succeeded",
|
||||
reportedWorkDisposition: "done",
|
||||
};
|
||||
|
||||
function event(sourceSeq: number, eventType: PrpEvent["eventType"], payload: Record<string, unknown>): PrpEvent {
|
||||
return {
|
||||
schema: "paperclip.prp.event.v1",
|
||||
sourceEventId: `runner-standalone-conformance:event:${sourceSeq}`,
|
||||
sourceSeq,
|
||||
sourceInstanceId: "runner-standalone-conformance",
|
||||
sourceKind: "runner",
|
||||
runId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.runId,
|
||||
normalizedSessionId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.sessionId,
|
||||
turnId: "turn-standalone-conformance",
|
||||
eventType,
|
||||
schemaVersion: 1,
|
||||
priority: eventType === "run.terminal" ? 0 : 1,
|
||||
emittedAt: `2026-08-09T00:00:0${sourceSeq}.000Z`,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export const CONTROL_PLANE_CONFORMANCE_EVENTS = [
|
||||
event(1, "session.started", { backend: "mock" }),
|
||||
event(2, "run.result.proposed", CONTROL_PLANE_CONFORMANCE_RESULT),
|
||||
event(3, "run.terminal", CONTROL_PLANE_CONFORMANCE_TERMINAL as unknown as Record<string, unknown>),
|
||||
] as const;
|
||||
|
||||
export async function runControlPlanePortConformance(
|
||||
harness: ControlPlanePortConformanceHarness,
|
||||
): Promise<ControlPlanePortConformanceSnapshot> {
|
||||
await harness.start?.();
|
||||
try {
|
||||
await harness.port.openRun(CONTROL_PLANE_CONFORMANCE_OPEN);
|
||||
let openBindingRejected = false;
|
||||
try {
|
||||
await harness.port.openRun({
|
||||
...CONTROL_PLANE_CONFORMANCE_OPEN,
|
||||
identity: { ...CONTROL_PLANE_CONFORMANCE_OPEN.identity, issueId: "forged-issue" },
|
||||
});
|
||||
} catch {
|
||||
openBindingRejected = true;
|
||||
}
|
||||
if (!openBindingRejected) throw new Error("mismatched run binding was accepted");
|
||||
const first = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[0]);
|
||||
if (first.disposition !== "committed" || first.highestContiguousSourceSeq !== 1) {
|
||||
throw new Error("first event did not commit at cursor 1");
|
||||
}
|
||||
const gap = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[2]);
|
||||
if (gap.highestContiguousSourceSeq !== 1) throw new Error("source gap was hidden");
|
||||
const recovered = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[1]);
|
||||
if (recovered.highestContiguousSourceSeq !== 3) throw new Error("source gap did not recover");
|
||||
const duplicate = await harness.port.appendEvent(CONTROL_PLANE_CONFORMANCE_EVENTS[1]);
|
||||
if (duplicate.disposition !== "duplicate") throw new Error("identical duplicate was not idempotent");
|
||||
let eventIdMutationRejected = false;
|
||||
try {
|
||||
await harness.port.appendEvent({
|
||||
...CONTROL_PLANE_CONFORMANCE_EVENTS[1],
|
||||
payload: { mutated: true },
|
||||
});
|
||||
} catch {
|
||||
eventIdMutationRejected = true;
|
||||
}
|
||||
if (!eventIdMutationRejected) throw new Error("mutated event id replay was accepted");
|
||||
let eventSequenceMutationRejected = false;
|
||||
try {
|
||||
await harness.port.appendEvent({
|
||||
...CONTROL_PLANE_CONFORMANCE_EVENTS[1],
|
||||
sourceEventId: "runner-standalone-conformance:mutated-sequence-two",
|
||||
payload: { mutated: true },
|
||||
});
|
||||
} catch {
|
||||
eventSequenceMutationRejected = true;
|
||||
}
|
||||
if (!eventSequenceMutationRejected) throw new Error("mutated source sequence replay was accepted");
|
||||
|
||||
const replay = await harness.port.replayEvents({
|
||||
runId: CONTROL_PLANE_CONFORMANCE_OPEN.identity.runId,
|
||||
sourceInstanceId: "runner-standalone-conformance",
|
||||
afterSourceSeq: 1,
|
||||
limit: 10,
|
||||
});
|
||||
if (replay.events.map((entry) => entry.sourceSeq).join(",") !== "2,3") {
|
||||
throw new Error("exclusive replay cursor returned the wrong event set");
|
||||
}
|
||||
let replayBindingRejected = false;
|
||||
try {
|
||||
await harness.port.replayEvents({
|
||||
runId: "forged-run",
|
||||
sourceInstanceId: "runner-standalone-conformance",
|
||||
afterSourceSeq: 0,
|
||||
limit: 10,
|
||||
});
|
||||
} catch {
|
||||
replayBindingRejected = true;
|
||||
}
|
||||
if (!replayBindingRejected) throw new Error("mismatched replay binding was accepted");
|
||||
|
||||
const completion: CompleteControlPlaneRunInput = {
|
||||
result: CONTROL_PLANE_CONFORMANCE_RESULT,
|
||||
terminal: CONTROL_PLANE_CONFORMANCE_TERMINAL,
|
||||
turnId: "turn-standalone-conformance",
|
||||
callerResultId: "result-standalone-conformance",
|
||||
callerDedupeKey: "dedupe-standalone-conformance",
|
||||
};
|
||||
await harness.port.completeRun(completion);
|
||||
await harness.port.completeRun(completion);
|
||||
let resultMutationRejected = false;
|
||||
try {
|
||||
await harness.port.completeRun({
|
||||
...completion,
|
||||
result: { ...completion.result, summary: "mutated result replay" },
|
||||
});
|
||||
} catch {
|
||||
resultMutationRejected = true;
|
||||
}
|
||||
if (!resultMutationRejected) throw new Error("mutated result replay was accepted");
|
||||
return {
|
||||
eventCount: replay.highestContiguousSourceSeq,
|
||||
highestContiguousSourceSeq: replay.highestContiguousSourceSeq,
|
||||
duplicateDisposition: duplicate.disposition,
|
||||
terminalReplayIdempotent: true,
|
||||
openBindingRejected,
|
||||
eventIdMutationRejected,
|
||||
eventSequenceMutationRejected,
|
||||
replayBindingRejected,
|
||||
resultMutationRejected,
|
||||
};
|
||||
} finally {
|
||||
await harness.stop?.();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import {
|
||||
HARNESS_DRIVER_CONTRACT_VERSION,
|
||||
type HarnessDriver,
|
||||
type HarnessDriverDescriptor,
|
||||
} from "../contracts/harness-driver.js";
|
||||
import { validatePrpEvent, type PrpEvent } from "../protocol/replay-contract.js";
|
||||
|
||||
export const HARNESS_DRIVER_CONFORMANCE_FIXTURE_SCHEMA =
|
||||
"paperclip-runner/harness-driver-conformance-fixture/v1" as const;
|
||||
export const HARNESS_DRIVER_CONFORMANCE_REPORT_SCHEMA =
|
||||
"paperclip-runner/harness-driver-conformance-report/v1" as const;
|
||||
|
||||
export const harnessDriverConformanceFixtureUrl = new URL(
|
||||
"../../protocol/fixtures/evals/harness-driver-conformance.json",
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
export interface HarnessDriverConformanceFixture {
|
||||
schema: typeof HARNESS_DRIVER_CONFORMANCE_FIXTURE_SCHEMA;
|
||||
validConfig: Record<string, unknown>;
|
||||
invalidConfig: Record<string, unknown>;
|
||||
completionMessage: string;
|
||||
interruptMessage: string;
|
||||
requiredCapabilities: string[];
|
||||
unsupportedFeatures: string[];
|
||||
}
|
||||
|
||||
export interface HarnessDriverConformanceReport {
|
||||
schema: typeof HARNESS_DRIVER_CONFORMANCE_REPORT_SCHEMA;
|
||||
contractVersion: typeof HARNESS_DRIVER_CONTRACT_VERSION;
|
||||
descriptor: HarnessDriverDescriptor;
|
||||
checks: {
|
||||
capabilityDescription: true;
|
||||
configValidation: true;
|
||||
sessionLifecycle: true;
|
||||
sessionRecovery: true;
|
||||
semanticTools: true;
|
||||
eventValidation: true;
|
||||
interruptAndCancel: true;
|
||||
usage: true;
|
||||
transcriptCompleteness: true;
|
||||
unsupportedFeatures: true;
|
||||
};
|
||||
eventCount: number;
|
||||
semanticToolCallCount: number;
|
||||
usage: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function record(value: unknown, path: string): Record<string, unknown> {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${path} must be an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function text(value: unknown, path: string): string {
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
throw new Error(`${path} must be a non-empty string`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function strings(value: unknown, path: string): string[] {
|
||||
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
||||
throw new Error(`${path} must be an array of strings`);
|
||||
}
|
||||
return [...value] as string[];
|
||||
}
|
||||
|
||||
export function parseHarnessDriverConformanceFixture(
|
||||
value: unknown,
|
||||
): HarnessDriverConformanceFixture {
|
||||
const fixture = record(value, "fixture");
|
||||
if (fixture.schema !== HARNESS_DRIVER_CONFORMANCE_FIXTURE_SCHEMA) {
|
||||
throw new Error(
|
||||
`fixture.schema must be ${HARNESS_DRIVER_CONFORMANCE_FIXTURE_SCHEMA}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
schema: HARNESS_DRIVER_CONFORMANCE_FIXTURE_SCHEMA,
|
||||
validConfig: structuredClone(record(fixture.validConfig, "fixture.validConfig")),
|
||||
invalidConfig: structuredClone(record(fixture.invalidConfig, "fixture.invalidConfig")),
|
||||
completionMessage: text(fixture.completionMessage, "fixture.completionMessage"),
|
||||
interruptMessage: text(fixture.interruptMessage, "fixture.interruptMessage"),
|
||||
requiredCapabilities: strings(fixture.requiredCapabilities, "fixture.requiredCapabilities"),
|
||||
unsupportedFeatures: strings(fixture.unsupportedFeatures, "fixture.unsupportedFeatures"),
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadHarnessDriverConformanceFixture(
|
||||
url: URL = harnessDriverConformanceFixtureUrl,
|
||||
): Promise<HarnessDriverConformanceFixture> {
|
||||
return parseHarnessDriverConformanceFixture(
|
||||
JSON.parse(await readFile(url, "utf8")) as unknown,
|
||||
);
|
||||
}
|
||||
|
||||
async function collectEvents(events: AsyncIterable<PrpEvent>): Promise<PrpEvent[]> {
|
||||
const collected: PrpEvent[] = [];
|
||||
for await (const event of events) {
|
||||
const validation = validatePrpEvent(event);
|
||||
if (!validation.ok) {
|
||||
throw new Error(
|
||||
`driver conformance received invalid event: ${validation.issues[0]?.message ?? "unknown issue"}`,
|
||||
);
|
||||
}
|
||||
collected.push(structuredClone(validation.event));
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
/** Run the deterministic, provider-free harness-driver V1 contract. */
|
||||
export async function runHarnessDriverConformance(input: {
|
||||
driver: HarnessDriver;
|
||||
fixture?: HarnessDriverConformanceFixture;
|
||||
}): Promise<HarnessDriverConformanceReport> {
|
||||
const fixture = input.fixture ?? await loadHarnessDriverConformanceFixture();
|
||||
const descriptor = await input.driver.descriptor();
|
||||
if (!descriptor.kind || !descriptor.displayName || !descriptor.version) {
|
||||
throw new Error("driver descriptor is missing stable identity fields");
|
||||
}
|
||||
for (const capability of fixture.requiredCapabilities) {
|
||||
if (descriptor.capabilities[capability as keyof typeof descriptor.capabilities] !== true) {
|
||||
throw new Error(`driver capability ${capability} is required by conformance V1`);
|
||||
}
|
||||
}
|
||||
for (const unsupported of fixture.unsupportedFeatures) {
|
||||
if (!descriptor.capabilities.unsupported?.includes(unsupported)) {
|
||||
throw new Error(`driver must explicitly describe unsupported feature ${unsupported}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.driver.validateConfig === undefined) {
|
||||
throw new Error("driver does not expose deterministic config validation");
|
||||
}
|
||||
const valid = await input.driver.validateConfig(fixture.validConfig);
|
||||
const invalid = await input.driver.validateConfig(fixture.invalidConfig);
|
||||
if (!valid.ok || invalid.ok || invalid.issues.length === 0) {
|
||||
throw new Error("driver config validation did not accept valid and reject invalid input");
|
||||
}
|
||||
|
||||
const completeSession = await input.driver.openSession({
|
||||
runId: "run_driver_conformance_complete",
|
||||
normalizedSessionId: "session_driver_conformance_complete",
|
||||
workingDirectory: "/deterministic/conformance",
|
||||
});
|
||||
await completeSession.startTurn({ message: { role: "user", text: fixture.completionMessage } });
|
||||
const completeEvents = await collectEvents(completeSession.events());
|
||||
const semanticInputs = completeEvents.filter((event) => event.eventType === "mcp_app.tool_input");
|
||||
const semanticResults = completeEvents.filter((event) => event.eventType === "mcp_app.tool_result");
|
||||
if (semanticInputs.length === 0 || semanticInputs.length !== semanticResults.length) {
|
||||
throw new Error("driver conformance requires paired semantic tool input/result events");
|
||||
}
|
||||
const snapshot = await completeSession.snapshot();
|
||||
if (snapshot.semanticResult === undefined || snapshot.semanticResult === null) {
|
||||
throw new Error("completed driver session did not persist a semantic result");
|
||||
}
|
||||
const usage = await completeSession.usage?.();
|
||||
if (usage === undefined || usage === null) {
|
||||
throw new Error("driver conformance requires a deterministic usage snapshot");
|
||||
}
|
||||
const transcript = await completeSession.transcript?.();
|
||||
if (
|
||||
transcript === undefined
|
||||
|| !transcript.complete
|
||||
|| transcript.eventCount !== completeEvents.length
|
||||
|| transcript.events.length !== completeEvents.length
|
||||
|| transcript.omissionReason !== null
|
||||
) {
|
||||
throw new Error("driver conformance transcript is incomplete or inconsistent");
|
||||
}
|
||||
if (input.driver.recoverSession === undefined) {
|
||||
throw new Error("driver declares resume but does not expose session recovery");
|
||||
}
|
||||
const recovery = await input.driver.recoverSession(snapshot, {
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
if (
|
||||
!recovery.recovered
|
||||
|| recovery.session === undefined
|
||||
|| recovery.session.ids().driverSessionId !== snapshot.driverSessionId
|
||||
) {
|
||||
throw new Error("driver did not recover the persisted conformance session identity");
|
||||
}
|
||||
const recoveredSnapshot = await recovery.session.snapshot();
|
||||
if (
|
||||
recoveredSnapshot.runId !== snapshot.runId
|
||||
|| recoveredSnapshot.normalizedSessionId !== snapshot.normalizedSessionId
|
||||
|| recoveredSnapshot.semanticResult?.fingerprint !== snapshot.semanticResult?.fingerprint
|
||||
) {
|
||||
throw new Error("driver recovery did not preserve the persisted conformance state");
|
||||
}
|
||||
await recovery.session.close({ reason: "conformance_recovery_complete" });
|
||||
await completeSession.close({ reason: "conformance_complete" });
|
||||
|
||||
const interruptedSession = await input.driver.openSession({
|
||||
runId: "run_driver_conformance_interrupt",
|
||||
normalizedSessionId: "session_driver_conformance_interrupt",
|
||||
workingDirectory: "/deterministic/conformance",
|
||||
});
|
||||
const { turnId } = await interruptedSession.startTurn({
|
||||
message: { role: "user", text: fixture.interruptMessage },
|
||||
});
|
||||
if (interruptedSession.interrupt === undefined) {
|
||||
throw new Error("driver conformance requires interrupt support");
|
||||
}
|
||||
await interruptedSession.interrupt({ turnId, reason: "conformance_interrupt" });
|
||||
const interruptedEvents = await collectEvents(interruptedSession.events());
|
||||
if (
|
||||
!interruptedEvents.some((event) => event.eventType === "turn.interrupted")
|
||||
|| !interruptedEvents.some((event) =>
|
||||
event.eventType === "run.terminal"
|
||||
&& record(event.payload, "terminal payload").runTerminalState === "cancelled")
|
||||
) {
|
||||
throw new Error("driver interrupt did not produce an explicit cancelled terminal");
|
||||
}
|
||||
await interruptedSession.close({ reason: "conformance_cancel", force: true });
|
||||
|
||||
return {
|
||||
schema: HARNESS_DRIVER_CONFORMANCE_REPORT_SCHEMA,
|
||||
contractVersion: HARNESS_DRIVER_CONTRACT_VERSION,
|
||||
descriptor: structuredClone(descriptor),
|
||||
checks: {
|
||||
capabilityDescription: true,
|
||||
configValidation: true,
|
||||
sessionLifecycle: true,
|
||||
sessionRecovery: true,
|
||||
semanticTools: true,
|
||||
eventValidation: true,
|
||||
interruptAndCancel: true,
|
||||
usage: true,
|
||||
transcriptCompleteness: true,
|
||||
unsupportedFeatures: true,
|
||||
},
|
||||
eventCount: completeEvents.length,
|
||||
semanticToolCallCount: semanticInputs.length,
|
||||
usage: structuredClone(usage),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import type {
|
||||
NativeRunEvent,
|
||||
NativeRunIdentity,
|
||||
NativeRunResult,
|
||||
} from "./types.js";
|
||||
import type {
|
||||
PrpEvent,
|
||||
PrpStructuredRunResult,
|
||||
PrpTerminalState,
|
||||
} from "../protocol/replay-contract.js";
|
||||
import type { PersistedNativeSession } from "./native-session-backend.js";
|
||||
|
||||
export interface OpenControlPlaneRunInput {
|
||||
identity: NativeRunIdentity;
|
||||
backendKind: "runner" | "remote" | "mock";
|
||||
sourceInstanceId?: string;
|
||||
}
|
||||
|
||||
export interface AppendedEventReceipt {
|
||||
cursor: number;
|
||||
highestContiguousSourceSeq: number;
|
||||
disposition: "committed" | "duplicate";
|
||||
}
|
||||
|
||||
export interface AppendControlPlaneEventOptions {
|
||||
/**
|
||||
* Cancellation is a durability boundary: an aborted append must settle
|
||||
* without committing before it rejects.
|
||||
*/
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface CheckpointControlPlaneSessionOptions {
|
||||
/**
|
||||
* Cancellation is a durability boundary: once aborted, checkpoint work
|
||||
* must settle without committing a new snapshot.
|
||||
*/
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ReplayControlPlaneEventsInput {
|
||||
runId: string;
|
||||
sourceInstanceId: string;
|
||||
afterSourceSeq: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface ReplayedControlPlaneEvents {
|
||||
events: PrpEvent[];
|
||||
highestContiguousSourceSeq: number;
|
||||
}
|
||||
|
||||
export interface FinalizeControlPlaneOperationOptions {
|
||||
/**
|
||||
* Finalization cancellation is a durability boundary. Once aborted, a
|
||||
* mutating operation must settle without committing new durable state.
|
||||
*/
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface CompleteControlPlaneRunInput {
|
||||
result: PrpStructuredRunResult;
|
||||
terminal: PrpTerminalState;
|
||||
turnId?: string | null;
|
||||
callerResultId?: string | null;
|
||||
callerDedupeKey?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The only control-plane surface the standalone runner may call.
|
||||
*
|
||||
* Production Paperclip implements this port in a later integration phase. The
|
||||
* runner package never imports the server, UI, or database that sits behind it.
|
||||
*/
|
||||
export interface ControlPlanePort {
|
||||
openRun(input: OpenControlPlaneRunInput): Promise<void>;
|
||||
loadSessionCheckpoint?(): Promise<PersistedNativeSession | null>;
|
||||
checkpointSession?(
|
||||
snapshot: PersistedNativeSession,
|
||||
options?: CheckpointControlPlaneSessionOptions,
|
||||
): Promise<void>;
|
||||
appendEvent(
|
||||
event: NativeRunEvent | PrpEvent,
|
||||
options?: AppendControlPlaneEventOptions,
|
||||
): Promise<AppendedEventReceipt>;
|
||||
replayEvents(
|
||||
input: ReplayControlPlaneEventsInput,
|
||||
options?: FinalizeControlPlaneOperationOptions,
|
||||
): Promise<ReplayedControlPlaneEvents>;
|
||||
completeRun(
|
||||
result: NativeRunResult | CompleteControlPlaneRunInput,
|
||||
options?: FinalizeControlPlaneOperationOptions,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
|
@ -0,0 +1,513 @@
|
|||
import type {
|
||||
PrpEvent,
|
||||
PrpStructuredRunResult,
|
||||
} from "../protocol/replay-contract.js";
|
||||
import type { NativeSessionCapabilities, NativeUserMessage } from "./types.js";
|
||||
import {
|
||||
PAPERCLIP_RUNTIME_REQUEST_SCHEMA_V2,
|
||||
parsePaperclipQuestionResponse,
|
||||
type PaperclipQuestionResponse,
|
||||
type PaperclipQuestionSet,
|
||||
type PaperclipRuntimeRequestOrigin,
|
||||
} from "./question-set.js";
|
||||
|
||||
export * from "./question-set.js";
|
||||
|
||||
export const HARNESS_DRIVER_CONTRACT_VERSION = 1 as const;
|
||||
|
||||
export interface HarnessDriverConfigIssue {
|
||||
path: string;
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type HarnessDriverConfigValidation =
|
||||
| { ok: true; config: Record<string, unknown>; issues: [] }
|
||||
| { ok: false; config: null; issues: HarnessDriverConfigIssue[] };
|
||||
|
||||
export interface HarnessTranscriptSnapshot {
|
||||
schema: "paperclip-runner/harness-transcript/v1";
|
||||
complete: boolean;
|
||||
eventCount: number;
|
||||
events: PrpEvent[];
|
||||
omissionReason: string | null;
|
||||
}
|
||||
|
||||
export interface HarnessDriverDescriptor {
|
||||
kind: string;
|
||||
displayName: string;
|
||||
version: string;
|
||||
protocolVersion?: string;
|
||||
capabilities: NativeSessionCapabilities;
|
||||
runtimeContextCapabilities?: NativeRuntimeContextCapabilities;
|
||||
}
|
||||
|
||||
export interface NativeRuntimeContextCapabilities {
|
||||
instructions: "native" | "unsupported";
|
||||
skills: "native" | "unsupported";
|
||||
mcp: "native" | "unsupported";
|
||||
}
|
||||
|
||||
export interface OpenHarnessSessionInput {
|
||||
runId: string;
|
||||
normalizedSessionId: string;
|
||||
workingDirectory: string;
|
||||
/** Abort provider bootstrap and release any not-yet-returned provider state. */
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface HarnessSessionRecoveryOptions {
|
||||
/** Abort provider recovery and release any not-yet-returned provider state. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export class HarnessCapabilityUnavailableError extends Error {
|
||||
readonly operation: string;
|
||||
|
||||
constructor(operation: string, detail: string) {
|
||||
super(`${operation} is unavailable: ${detail}`);
|
||||
this.name = "HarnessCapabilityUnavailableError";
|
||||
this.operation = operation;
|
||||
}
|
||||
}
|
||||
|
||||
export class HarnessReconciliationError extends Error {
|
||||
readonly recoverable = true;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "HarnessReconciliationError";
|
||||
}
|
||||
}
|
||||
|
||||
export class HarnessOperationAlreadyTerminalError extends Error {
|
||||
readonly code = "already_terminal" as const;
|
||||
|
||||
constructor(operation: string) {
|
||||
super(`${operation} lost a race with the committed turn terminal`);
|
||||
this.name = "HarnessOperationAlreadyTerminalError";
|
||||
}
|
||||
}
|
||||
|
||||
export class HarnessStaleTurnError extends Error {
|
||||
readonly code = "stale_turn" as const;
|
||||
|
||||
constructor(turnId: string) {
|
||||
super(`turn ${turnId} is not the active turn`);
|
||||
this.name = "HarnessStaleTurnError";
|
||||
}
|
||||
}
|
||||
|
||||
export type HarnessRuntimeRequestKind =
|
||||
| "command_approval"
|
||||
| "file_approval"
|
||||
| "permission_approval"
|
||||
| "user_input"
|
||||
| "elicitation";
|
||||
|
||||
export interface HarnessRuntimeRequest {
|
||||
requestId: string;
|
||||
requestKind: HarnessRuntimeRequestKind;
|
||||
method: string;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
status: "pending";
|
||||
prompt: string;
|
||||
details: Record<string, unknown>;
|
||||
/** Provider-neutral presentation model for structured input requests. */
|
||||
input?: PaperclipQuestionSet;
|
||||
/** Diagnostic origin only; provider response shapes never enter PRP. */
|
||||
origin?: PaperclipRuntimeRequestOrigin;
|
||||
}
|
||||
|
||||
export type HarnessRuntimeRequestResolution =
|
||||
| { action: "accept" | "accept_for_session" | "decline" | "cancel" }
|
||||
| {
|
||||
action: "submit";
|
||||
answers: Record<string, { answers: string[] }>;
|
||||
}
|
||||
| {
|
||||
action: "submit";
|
||||
content: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
action: "submit";
|
||||
response: PaperclipQuestionResponse;
|
||||
};
|
||||
|
||||
export type HarnessRuntimeRequestAction = HarnessRuntimeRequestResolution["action"];
|
||||
|
||||
export type HarnessRuntimeRequestHandoffResult = "handed_off" | "already_settled";
|
||||
|
||||
/**
|
||||
* A runtime-input handoff commits its durable state transition before the
|
||||
* method returns. Provider interruption is cleanup only: it remains observed,
|
||||
* but cannot acquire mutation authority or reverse an already committed turn.
|
||||
*/
|
||||
export interface HarnessRuntimeRequestHandoff {
|
||||
result: HarnessRuntimeRequestHandoffResult;
|
||||
cleanup: Promise<void>;
|
||||
}
|
||||
|
||||
const RUNTIME_REQUEST_ACTIONS: readonly HarnessRuntimeRequestAction[] = [
|
||||
"accept",
|
||||
"accept_for_session",
|
||||
"decline",
|
||||
"cancel",
|
||||
"submit",
|
||||
];
|
||||
|
||||
export class HarnessRuntimeRequestResolutionError extends Error {
|
||||
readonly code = "invalid_resolution" as const;
|
||||
readonly requestKind: HarnessRuntimeRequestKind;
|
||||
|
||||
constructor(requestKind: HarnessRuntimeRequestKind, detail: string) {
|
||||
super(`${requestKind} rejected its resolution: ${detail}`);
|
||||
this.name = "HarnessRuntimeRequestResolutionError";
|
||||
this.requestKind = requestKind;
|
||||
}
|
||||
}
|
||||
|
||||
function plainRecord(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function parseAnswers(value: unknown): Record<string, { answers: string[] }> | null {
|
||||
const fields = plainRecord(value);
|
||||
if (fields === null || Object.keys(fields).length === 0) return null;
|
||||
const parsed: Record<string, { answers: string[] }> = {};
|
||||
for (const [field, entry] of Object.entries(fields)) {
|
||||
const answer = plainRecord(entry);
|
||||
if (answer === null || !Array.isArray(answer.answers)) return null;
|
||||
if (answer.answers.some((value) => typeof value !== "string")) return null;
|
||||
parsed[field] = { answers: [...(answer.answers as string[])] };
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a resolution against the kind of request it answers, so a
|
||||
* mismatched submit fails closed instead of degrading into an accepted empty
|
||||
* response. Every driver runs this before it touches its provider, and the
|
||||
* browser transport runs it again at its own untrusted edge.
|
||||
*/
|
||||
export function parseHarnessRuntimeRequestResolution(
|
||||
requestKind: HarnessRuntimeRequestKind,
|
||||
value: unknown,
|
||||
questionSet?: PaperclipQuestionSet,
|
||||
): HarnessRuntimeRequestResolution {
|
||||
const candidate = plainRecord(value) ?? {};
|
||||
const rawAction = candidate.action;
|
||||
if (
|
||||
typeof rawAction !== "string" ||
|
||||
!RUNTIME_REQUEST_ACTIONS.includes(rawAction as HarnessRuntimeRequestAction)
|
||||
) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
`unsupported action ${JSON.stringify(rawAction) ?? "undefined"}`,
|
||||
);
|
||||
}
|
||||
const action = rawAction as HarnessRuntimeRequestAction;
|
||||
if (action !== "submit" && ("answers" in candidate || "content" in candidate || "response" in candidate)) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
`${action} does not carry submitted form data`,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "submit" && "response" in candidate) {
|
||||
if (requestKind !== "user_input" && requestKind !== "elicitation") {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"approval requests do not accept submitted question responses",
|
||||
);
|
||||
}
|
||||
if ("answers" in candidate || "content" in candidate) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"canonical submissions cannot also carry provider-specific answers or content",
|
||||
);
|
||||
}
|
||||
if (questionSet === undefined) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"canonical submission requires the persisted question set",
|
||||
);
|
||||
}
|
||||
try {
|
||||
return {
|
||||
action,
|
||||
response: parsePaperclipQuestionResponse(questionSet, candidate.response),
|
||||
};
|
||||
} catch (error) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
error instanceof Error ? error.message : "invalid question response",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (requestKind === "user_input") {
|
||||
if (action === "accept" || action === "accept_for_session") {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"user input requires submit, decline, or cancel",
|
||||
);
|
||||
}
|
||||
if (action !== "submit") return { action };
|
||||
if ("content" in candidate) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"user input submissions carry answers, not content",
|
||||
);
|
||||
}
|
||||
const answers = parseAnswers(candidate.answers);
|
||||
if (answers === null) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"submit requires answers shaped as { field: { answers: [string] } }",
|
||||
);
|
||||
}
|
||||
return { action, answers };
|
||||
}
|
||||
|
||||
if (requestKind === "elicitation") {
|
||||
if (action === "accept_for_session") {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"elicitation does not support session acceptance",
|
||||
);
|
||||
}
|
||||
if (action !== "submit") return { action };
|
||||
if ("answers" in candidate) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"elicitation submissions carry content, not answers",
|
||||
);
|
||||
}
|
||||
const content = plainRecord(candidate.content);
|
||||
if (content === null || Object.keys(content).length === 0) {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"submit requires a non-empty content object",
|
||||
);
|
||||
}
|
||||
return { action, content: structuredClone(content) };
|
||||
}
|
||||
|
||||
if (action === "submit") {
|
||||
throw new HarnessRuntimeRequestResolutionError(
|
||||
requestKind,
|
||||
"approval requests do not accept submitted form data",
|
||||
);
|
||||
}
|
||||
return { action };
|
||||
}
|
||||
|
||||
/**
|
||||
* The single payload shape every driver emits for a terminal
|
||||
* `runtime_request.*` fact. Request identity travels in the payload as well as
|
||||
* the event binding, so a consumer that only reads payloads still settles the
|
||||
* request it belongs to.
|
||||
*/
|
||||
export type HarnessRuntimeRequestOutcome = {
|
||||
requestId: string;
|
||||
requestKind: HarnessRuntimeRequestKind;
|
||||
turnId: string;
|
||||
itemId: string;
|
||||
action?: HarnessRuntimeRequestAction;
|
||||
reason?: string;
|
||||
/** Enables content-free lifecycle counters partitioned by adapter. */
|
||||
adapter?: string;
|
||||
requestType?: "input" | "permission";
|
||||
/** Canonical submitted answers retained for durable replay and audit UI. */
|
||||
response?: PaperclipQuestionResponse;
|
||||
};
|
||||
|
||||
export function harnessRuntimeRequestOutcome(
|
||||
request: Pick<
|
||||
HarnessRuntimeRequest,
|
||||
"requestId" | "requestKind" | "turnId" | "itemId" | "origin" | "input"
|
||||
>,
|
||||
outcome: {
|
||||
action?: HarnessRuntimeRequestAction | null;
|
||||
reason?: string | null;
|
||||
response?: PaperclipQuestionResponse | null;
|
||||
} = {},
|
||||
): HarnessRuntimeRequestOutcome {
|
||||
return {
|
||||
requestId: request.requestId,
|
||||
requestKind: request.requestKind,
|
||||
turnId: request.turnId,
|
||||
itemId: request.itemId,
|
||||
...(outcome.action ? { action: outcome.action } : {}),
|
||||
...(outcome.reason ? { reason: outcome.reason } : {}),
|
||||
...(outcome.response ? { response: structuredClone(outcome.response) } : {}),
|
||||
...(request.input
|
||||
? {
|
||||
...(request.origin?.adapter ? { adapter: request.origin.adapter } : {}),
|
||||
requestType: "input" as const,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Canonical non-replayable expiration used when a live input moves to a durable wait. */
|
||||
export function harnessRuntimeInputExpiredOutcome(
|
||||
request: HarnessRuntimeRequest,
|
||||
reason: "durable_handoff" | "provider_process_lost",
|
||||
): Omit<HarnessRuntimeRequestOutcome, "requestKind"> & {
|
||||
requestKind: "runtime";
|
||||
replayAllowed: false;
|
||||
request: Record<string, unknown>;
|
||||
} {
|
||||
return {
|
||||
...harnessRuntimeRequestOutcome(request, { reason }),
|
||||
requestKind: "runtime",
|
||||
replayAllowed: false,
|
||||
request: {
|
||||
schema: PAPERCLIP_RUNTIME_REQUEST_SCHEMA_V2,
|
||||
requestKind: "runtime",
|
||||
requestId: request.requestId,
|
||||
type: "input",
|
||||
status: request.status,
|
||||
prompt: request.prompt,
|
||||
input: structuredClone(request.input),
|
||||
origin: structuredClone(request.origin),
|
||||
turnId: request.turnId,
|
||||
itemId: request.itemId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface HarnessThreadGoal {
|
||||
threadId: string;
|
||||
objective: string;
|
||||
status: "active" | "paused" | "blocked" | "usageLimited" | "budgetLimited" | "complete";
|
||||
tokenBudget: number | null;
|
||||
tokensUsed: number;
|
||||
timeUsedSeconds: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export type HarnessGoalOperation =
|
||||
| { action: "get" }
|
||||
| { action: "set"; objective: string; tokenBudget?: number | null }
|
||||
| { action: "pause" | "resume" | "clear" };
|
||||
|
||||
export interface HarnessThreadLineageEntry {
|
||||
threadId: string;
|
||||
providerSessionId: string | null;
|
||||
parentThreadId: string | null;
|
||||
depth: number;
|
||||
nickname: string | null;
|
||||
role: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface PersistedHarnessSemanticResult {
|
||||
result: PrpStructuredRunResult;
|
||||
fingerprint: string;
|
||||
callId?: string | null;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
export interface PersistedHarnessTurnTerminal {
|
||||
turnId: string;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
export interface AcpxSessionIdentity {
|
||||
kind: "acpx";
|
||||
normalizedSessionId: string;
|
||||
acpxRecordId: string;
|
||||
backendSessionId: string;
|
||||
agentSessionId: string;
|
||||
profileDigest: string;
|
||||
workspaceDigest: string;
|
||||
requestedModel: string;
|
||||
effectiveModel: string;
|
||||
/** Missing on legacy snapshots; those used the historical approve-reads behavior. */
|
||||
permissionMode?: "approve-all" | "approve-reads" | "deny-all";
|
||||
}
|
||||
|
||||
export type PersistedHarnessProviderIdentity = AcpxSessionIdentity;
|
||||
|
||||
export interface PersistedHarnessSession {
|
||||
driverKind: string;
|
||||
driverSessionId: string;
|
||||
providerSessionId?: string | null;
|
||||
runId?: string;
|
||||
normalizedSessionId?: string;
|
||||
activeTurnId?: string | null;
|
||||
semanticResult?: PersistedHarnessSemanticResult | null;
|
||||
terminalTurns?: PersistedHarnessTurnTerminal[];
|
||||
pendingRuntimeRequests?: HarnessRuntimeRequest[];
|
||||
goal?: HarnessThreadGoal | null;
|
||||
lineage?: HarnessThreadLineageEntry[];
|
||||
lastSourceSequence?: number;
|
||||
/** Tagged provider identity used to reject cross-profile recovery. */
|
||||
providerIdentity?: PersistedHarnessProviderIdentity;
|
||||
/** Narrow escape hatch for a durable response-wake when a provider cannot reload its prior native session. */
|
||||
providerRecoveryPolicy?:
|
||||
| "same_session_only"
|
||||
| "allow_replacement_after_governed_wait"
|
||||
| "allow_replacement_after_resume_failure";
|
||||
}
|
||||
|
||||
export interface HarnessSessionRecoveryResult {
|
||||
recovered: boolean;
|
||||
session?: HarnessSession;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface HarnessSession {
|
||||
ids(): {
|
||||
driverSessionId: string;
|
||||
providerSessionId?: string | null;
|
||||
displayId?: string | null;
|
||||
};
|
||||
events(): AsyncIterable<PrpEvent>;
|
||||
attachRun?(input: { runId: string }): Promise<void> | void;
|
||||
startTurn(input: {
|
||||
message: NativeUserMessage;
|
||||
requestedCollaborationMode?: "default" | "plan";
|
||||
}): Promise<{ turnId: string; effectiveCollaborationMode?: "default" | "plan" }>;
|
||||
steer?(input: { turnId: string; message: NativeUserMessage; correlationId?: string }): Promise<void>;
|
||||
interrupt?(input: { turnId?: string; reason?: string; signal?: AbortSignal }): Promise<void>;
|
||||
pendingRuntimeRequests?(): HarnessRuntimeRequest[];
|
||||
resolveRuntimeRequest?(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
resolution: HarnessRuntimeRequestResolution;
|
||||
}): Promise<void>;
|
||||
handoffRuntimeRequest?(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
reason: "durable_handoff";
|
||||
/** Do not synchronously commit if runtime ownership is already revoked. */
|
||||
signal: AbortSignal;
|
||||
}): HarnessRuntimeRequestHandoff;
|
||||
goal?(input: HarnessGoalOperation): Promise<HarnessThreadGoal | null>;
|
||||
lineage?(): HarnessThreadLineageEntry[];
|
||||
read?(): Promise<Record<string, unknown>>;
|
||||
reconcile?(): Promise<Record<string, unknown>>;
|
||||
usage?(): Promise<Record<string, unknown> | null>;
|
||||
transcript?(): Promise<HarnessTranscriptSnapshot>;
|
||||
snapshot(): Promise<PersistedHarnessSession>;
|
||||
close(input: { reason: string; force?: boolean }): Promise<void>;
|
||||
}
|
||||
|
||||
/** A local harness implementation hidden behind the runner daemon boundary. */
|
||||
export interface HarnessDriver {
|
||||
descriptor(): Promise<HarnessDriverDescriptor>;
|
||||
validateConfig?(config: unknown): Promise<HarnessDriverConfigValidation>;
|
||||
openSession(input: OpenHarnessSessionInput): Promise<HarnessSession>;
|
||||
recoverSession?(
|
||||
snapshot: PersistedHarnessSession,
|
||||
options: HarnessSessionRecoveryOptions,
|
||||
): Promise<HarnessSessionRecoveryResult>;
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
import type {
|
||||
NativeRunIdentity,
|
||||
NativeSessionCapabilities,
|
||||
NativeUserMessage,
|
||||
} from "./types.js";
|
||||
import type {
|
||||
PrpEvent,
|
||||
PrpStructuredRunResult,
|
||||
PrpTerminalState,
|
||||
} from "../protocol/replay-contract.js";
|
||||
import type {
|
||||
HarnessRuntimeRequest,
|
||||
HarnessRuntimeRequestHandoff,
|
||||
HarnessRuntimeRequestResolution,
|
||||
HarnessThreadLineageEntry,
|
||||
NativeRuntimeContextCapabilities,
|
||||
PersistedHarnessProviderIdentity,
|
||||
PersistedHarnessTurnTerminal,
|
||||
} from "./harness-driver.js";
|
||||
|
||||
export interface NativeSessionBackendDescriptor {
|
||||
kind: "runner" | "remote" | "mock";
|
||||
name: string;
|
||||
version: string;
|
||||
capabilities: NativeSessionCapabilities;
|
||||
runtimeContextCapabilities?: NativeRuntimeContextCapabilities;
|
||||
}
|
||||
|
||||
export interface OpenNativeSessionInput {
|
||||
identity: NativeRunIdentity;
|
||||
workingDirectory?: string;
|
||||
/**
|
||||
* When present, aborts provider bootstrap if the caller's recovery deadline
|
||||
* expires. Backends own cleanup for work that has not returned a session yet.
|
||||
*/
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface NativeSessionRecoveryOptions {
|
||||
/** Abort provider recovery and release any not-yet-returned provider state. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface PersistedNativeSession {
|
||||
backendKind: NativeSessionBackendDescriptor["kind"];
|
||||
driverKind?: string | null;
|
||||
sessionId: string;
|
||||
identity: NativeRunIdentity;
|
||||
providerSessionId?: string | null;
|
||||
/** Tagged provider-owned identity required for safe driver recovery. */
|
||||
providerIdentity?: PersistedHarnessProviderIdentity;
|
||||
providerRecoveryPolicy?:
|
||||
| "same_session_only"
|
||||
| "allow_replacement_after_governed_wait"
|
||||
| "allow_replacement_after_resume_failure";
|
||||
cursor?: string | null;
|
||||
semanticResult?: PrpStructuredRunResult | null;
|
||||
terminal?: PrpTerminalState | null;
|
||||
activeTurnId?: string | null;
|
||||
terminalTurns?: PersistedHarnessTurnTerminal[];
|
||||
pendingRuntimeRequests?: HarnessRuntimeRequest[];
|
||||
lineage?: HarnessThreadLineageEntry[];
|
||||
}
|
||||
|
||||
export interface NativeSessionRecoveryResult {
|
||||
recovered: boolean;
|
||||
session?: NativeSession;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancellation is a synchronous authority transition followed by passive
|
||||
* provider cleanup. Once `cancel` returns, the provider session must no longer
|
||||
* be able to publish accepted output or acquire new mutation authority for the
|
||||
* cancelled turn. Cleanup may stop processes or transports, but it must not
|
||||
* perform durable control-plane mutations.
|
||||
*/
|
||||
export interface NativeSessionCancellation {
|
||||
cleanup: Promise<void>;
|
||||
}
|
||||
|
||||
export interface NativeSessionSnapshotOptions {
|
||||
/** Stop provider snapshot work that outlives the execution deadline. */
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
export interface NativeSession {
|
||||
identity(): NativeRunIdentity;
|
||||
capabilities(): Promise<NativeSessionCapabilities>;
|
||||
attachRun?(input: { identity: NativeRunIdentity }): Promise<void>;
|
||||
events(input?: { afterCursor?: string | null }): AsyncIterable<PrpEvent>;
|
||||
startTurn(input: {
|
||||
message: NativeUserMessage;
|
||||
requestedCollaborationMode?: "default" | "plan";
|
||||
}): Promise<{ turnId: string; effectiveCollaborationMode?: "default" | "plan" }>;
|
||||
steer?(input: { turnId: string; message: NativeUserMessage; correlationId?: string }): Promise<void>;
|
||||
interrupt?(input: { turnId?: string; reason?: string }): Promise<void>;
|
||||
/** Commit cancellation synchronously; the returned promise owns cleanup only. */
|
||||
cancel?(input: { reason: string; signal: AbortSignal }): NativeSessionCancellation;
|
||||
resolveRuntimeRequest?(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
resolution: HarnessRuntimeRequestResolution;
|
||||
}): Promise<void>;
|
||||
handoffRuntimeRequest?(input: {
|
||||
requestId: string;
|
||||
turnId: string;
|
||||
reason: "durable_handoff";
|
||||
/**
|
||||
* Revokes durable mutation authority when event consumption fails. The
|
||||
* method must commit synchronously before returning; its returned promise
|
||||
* owns provider cleanup only and must not mutate durable request state.
|
||||
*/
|
||||
signal: AbortSignal;
|
||||
}): HarnessRuntimeRequestHandoff;
|
||||
result(): Promise<{
|
||||
result: PrpStructuredRunResult;
|
||||
terminal: PrpTerminalState;
|
||||
turnId: string | null;
|
||||
} | null>;
|
||||
usage?(): Promise<Record<string, unknown> | null>;
|
||||
snapshot(options?: NativeSessionSnapshotOptions): Promise<PersistedNativeSession>;
|
||||
/**
|
||||
* Idempotently stop provider work and release every pending `events().next()`
|
||||
* before this promise resolves. Implementations must settle every promise
|
||||
* previously returned by the session (including interrupt, cancel, handoff,
|
||||
* and iterator teardown). The runtime bounds its wait for a broken provider,
|
||||
* revokes that session's mutation authority, removes it from reuse, and keeps
|
||||
* observing late cleanup so a contract violation cannot defeat a run timeout
|
||||
* or become an unhandled rejection.
|
||||
*/
|
||||
close(input: { reason: string }): Promise<void>;
|
||||
}
|
||||
|
||||
/** Normalized control-plane boundary shared by runner and hosted backends. */
|
||||
export interface NativeSessionBackend {
|
||||
descriptor(): Promise<NativeSessionBackendDescriptor>;
|
||||
openSession(input: OpenNativeSessionInput): Promise<NativeSession>;
|
||||
/** Open a fresh provider session after an explicitly governed continuity break. */
|
||||
openReplacementSession?(
|
||||
input: OpenNativeSessionInput,
|
||||
previous: PersistedNativeSession,
|
||||
): Promise<NativeSession>;
|
||||
recoverSession?(
|
||||
snapshot: PersistedNativeSession,
|
||||
options: NativeSessionRecoveryOptions,
|
||||
): Promise<NativeSessionRecoveryResult>;
|
||||
}
|
||||
|
|
@ -1,12 +1,17 @@
|
|||
export * from "./catalog/index.js";
|
||||
export * from "./contracts/control-plane-port.js";
|
||||
export * from "./contracts/completion-result.js";
|
||||
export * from "./contracts/codex.js";
|
||||
export * from "./contracts/durable-recovery.js";
|
||||
export * from "./contracts/harness-driver.js";
|
||||
export * from "./contracts/local-runner.js";
|
||||
export * from "./contracts/native-execution.js";
|
||||
export * from "./contracts/native-session-backend.js";
|
||||
export * from "./contracts/question-set.js";
|
||||
export * from "./contracts/runtime-context.js";
|
||||
export * from "./contracts/types.js";
|
||||
export * from "./backends/harness-driver-backend.js";
|
||||
export * from "./native-session-runtime.js";
|
||||
export {
|
||||
DurablePrpControlPlane,
|
||||
type DurablePrpControlPlaneOptions,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -6,5 +6,7 @@
|
|||
* accidental production dependency.
|
||||
*/
|
||||
export * from "./index.js";
|
||||
export * from "./conformance/control-plane-port.js";
|
||||
export * from "./conformance/harness-driver.js";
|
||||
export * from "./conformance/semantic-conformance.js";
|
||||
export * from "./protocol/replay-loader.js";
|
||||
|
|
|
|||
Loading…
Reference in New Issue