Files
PaperClipAI/packages/paperclip-runner/src/backends/codex-native-backend.ts
T
DottaandPaperclip a386a59998 Reduce repeated native completion guidance and preserve final replies (#15151)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Native agents receive task constraints and completion tools from
Paperclip.
> - Completion tools already define the procedure for reporting a
result.
> - Repeated procedure text adds instructions to each full task turn.
> - The final reply must still explain a blocker and link a saved
document.
> - This pull request removes repeated procedure text and keeps these
visible outcome requirements explicit.
> - A document receipt supplies the exact link, and stricter evals check
the persisted reply and browser navigation.

## Linked Issues or Issue Description

Refs: #14961. Related: #14948 and #15007.

**What happened?**

Native task envelopes repeat completion procedure text. A reduced
envelope needs explicit final-reply requirements. The `write_document`
receipt also lacks a canonical document link.

**Expected behavior**

Keep the completion tools as the source of procedure details. Require
one accepted completion result before the final reply. A blocked reply
must explain the reason, owner and unblock action. A document reply must
contain a working link to the saved document.

**Steps to reproduce**

1. Run the native assigned-skill document case and native blocker case.
2. Inspect the run-attributed provider final and its persisted comment.
3. Check the blocker explanation or open the final reply's document
link.

## What Changed

- Remove repeated completion procedure text from the native task
constraints and backend instructions.
- Keep explicit blocker and document-link requirements in full task
turns.
- Return a company/task-scoped `documentHref` from `write_document`.
Preserve the link in the idempotent mutation receipt.
- Repeat canonical links for this run's current saved revisions in
accepted completion feedback. Give blocked providers final-response
guidance for the cause, owner and unblock action.
- Keep internal document/comment anchors when Markdown issue links load
cached issue details.
- Add a manual six-cell comparison suite with strict source, build,
default-instruction and budget admission.
- Capture eighteen shared runnerd RPC projections and six direct
OpenCode HTTP projections across start, resume and continuation phases,
using scripted local transports and no provider execution.
- Apply v3 checks only to the manual instruction comparison; preserve v2
checks for the existing native completion suite. Check the actual
persisted blocker reason and exact saved-document link. Click the
rendered document link and check the original content marker in the
classic document card or the new document tab.
- Forward exact OpenCode finishing calls through the controller. Wait
for acceptance, keep accepted feedback and concrete rejection text, and
reject malformed responses. Preserve ordinary dynamic-tool response
handling.
- Settle the completion decision and tool response before mapping a
racing idle/error/abort event or handling explicit close/interruption.
Reject a concurrent finishing call before controller admission.
- Add a provider-free regression through real runnerd, the OpenCode
proxy and a fake provider. Reject the first completion, accept the
corrected report in the same turn, and propose one result.
- Keep all original verdicts unchanged. Treat replay under new checks as
separate diagnostics.

## Verification

- `pnpm -r typecheck` and `pnpm build` pass locally.
- Native document-authority tests pass, including company/run
authorization and idempotent replay.
- Native runtime-context, backend and measurement tests pass.
- Final-answer calibration, protocol scoring, source-admission and
catalog tests pass. Wrong reasons, absent links and wrong link targets
fail.
- `pnpm test:e2e:runner:typecheck` passes. Discovery lists exactly six
single-attempt local cells with the declared models.
- Exported `prepareNativeInstructionPreflight` then
`verifyNativeInstructionPreflight` pass on this clean committed source.
They build locally and make zero provider calls.
- Corrective live confirmation is incomplete. Source 3a7349d passed both
Claude and both Codex cases. OpenCode saved the correct document but
omitted its final link; its blocker case was canceled before paid
execution. Preserve this failure. The e171282 confirmation was stopped
during build after fresh review found a completion-settlement race; it
executed zero providers. Source c3e0cb303 fixes that race. Two affected
OpenCode cases await fresh review and one bounded confirmation; earlier
results remain attributed to their original source.
- OpenCode proxy parsing, driver, factory and input tests: 81 pass
across retained focused runs, including six settlement races.
Evaluator/scoring/admission checks: 122 pass. The real proxy regression
passes. Fresh local prepare then verify passes with 18 shared and 6
direct scripted captures, fresh SDK/Rust builds and zero providers.
- The full local suite recorded two failures: a webhook timeout and a
Git-scan load count mismatch. Both files pass in isolation with
unchanged assertions/time budgets; preserve the original failure log.
Fresh c3e0cb303 CI and review are pending. This PR remains draft.

## Risks

- Final-answer wording can vary by provider. The checks cover the
declared release-access blocker and saved document fixture, not general
answer quality.
- A single trial does not establish general equivalence, cause, speed,
cost or live resume behavior.
- `documentHref` is an additive receipt field. It points to the current
saved document, not an immutable historical revision. Replaying an older
receipt does not fabricate a new link.
- The correction adds four production paths for document receipts,
accepted completion feedback and UI navigation, plus four OpenCode
controller/proxy paths, beyond the original three instruction paths.
Completion rejection must remain repairable; the production-boundary
regression covers it.
- Preserve the frozen comparison context for live measurement. A
merge-tree check against current master is clean. Do not relabel earlier
live results as results from a later source tree.

## Model Used

- OpenAI Codex, GPT-6 family. The exact serving model ID and context
window are unavailable in this session. Capabilities used: reasoning,
code editing, shell execution, test authoring and evidence review.

## 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 (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [ ] 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

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-05 08:10:14 -05:00

236 lines
8.8 KiB
TypeScript

import { createCodexTaskEnvelope } from "../contracts/codex.js";
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
import type { NativeExecutionInput } from "../contracts/native-execution.js";
import type { PersistedHarnessSession } from "../contracts/harness-driver.js";
import type {
NativeSessionBackend,
NativeSessionBackendDescriptor,
PersistedNativeSession,
} from "../contracts/native-session-backend.js";
import type { CodexAppServerTransport } from "../drivers/codex/app-server-transport.js";
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
import type { CodexWorkingDirectoryAuthority } from "../drivers/codex/codex-boundaries.js";
import { HarnessDriverBackend } from "./harness-driver-backend.js";
import {
nativeSystemInstructions,
nativeTaskSkillInputs,
nativeTaskConstraints,
} from "./runtime-context.js";
export interface CodexNativeSessionBackendOptions {
/** Effective provider environment, including the assigned workspace boundary. */
environment?: NodeJS.ProcessEnv;
/** Filesystem that authoritatively admits the workspace path. */
workingDirectoryAuthority?: CodexWorkingDirectoryAuthority;
runnerInstanceId?: string;
onSpawn?: (meta: {
pid: number;
processGroupId: number | null;
startedAt: string;
}) => Promise<void>;
transportFactory?: (context?: {
providerRecoveryPolicy?: PersistedNativeSession["providerRecoveryPolicy"];
persistedSession?: Pick<
PersistedHarnessSession,
| "driverSessionId"
| "providerSessionId"
| "providerIdentity"
| "activeTurnId"
>;
}) => CodexAppServerTransport;
/** Current server constraints; does not commit task status before the turn ends. */
completionFeedback?: (result: import("../protocol/replay-contract.js").PrpStructuredRunResult) => Promise<string>;
dynamicTools?: readonly Readonly<Record<string, unknown>>[];
dynamicToolHandler?: (call: {
tool: string;
callId: string;
threadId: string;
turnId: string;
arguments: unknown;
}) => Promise<unknown>;
}
function transportDriverIdentity(input: NativeExecutionInput): {
kind:
| "codex_app_server"
| "opencode_server"
| "claude_managed_agents_api"
| "aws_agentcore_harness_api"
| "acpx_runtime";
displayName: string;
version: string;
} {
switch (input.provider.kind) {
case "codex":
return {
kind: "codex_app_server",
displayName: "Codex app-server",
version: "codex-v2",
};
case "opencode":
return {
kind: "opencode_server",
displayName: "OpenCode server",
version: "1.18.32",
};
case "claude_managed":
return {
kind: "claude_managed_agents_api",
displayName: "Claude Managed Agent",
version: input.provider.managedProfile.betaVersion,
};
case "aws_agentcore":
return {
kind: "aws_agentcore_harness_api",
displayName: "AWS AgentCore Harness",
version: input.provider.agentCoreProfile.qualificationRevision,
};
case "acpx":
return {
kind: "acpx_runtime",
displayName: `${ACPX_CAPABILITY_PROFILES[input.provider.agent].displayName} via ACPX`,
version: "0.13.1",
};
default:
throw new Error(
"Native provider is not available through the local runnerd transport",
);
}
}
function createTransportBackedNativeSessionBackend(
input: NativeExecutionInput,
options: CodexNativeSessionBackendOptions,
): NativeSessionBackend {
if (
options.workingDirectoryAuthority === "remote_runner" &&
!options.transportFactory
) {
throw new Error(
"Remote runner workspace authority requires a runnerd transport",
);
}
const driverIdentity = transportDriverIdentity(input);
const isCodex = input.provider.kind === "codex";
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
const supportsCollaborativePlanning =
isCodex ||
input.provider.kind === "opencode" ||
input.provider.kind === "acpx";
if (
input.provider.kind === "codex" &&
input.provider.approvalPolicy !== undefined &&
input.provider.approvalPolicy !== "never"
) {
throw new Error(
"paperclip_runner_codex_permission_mode_unqualified: set codexPermissionMode to never before starting or recovering this native run",
);
}
const constraints = [
...(supportsCollaborativePlanning &&
"executionMode" in input &&
input.executionMode === "plan"
? [
"Use native plan collaboration mode and do not modify workspace files.",
"Treat the supplied Paperclip planning context as the canonical pinned base revision.",
"Complete one structured provider plan item; Paperclip will synchronize it after completion.",
"Keep the final response to a short synchronization summary instead of repeating the full plan.",
]
: []),
...nativeTaskConstraints(input),
];
return new HarnessDriverBackend(
new CodexAppServerDriver({
...(input.provider.model ? { model: input.provider.model } : {}),
...(input.provider.kind === "codex" && "reasoningEffort" in input.provider && input.provider.reasoningEffort
? { reasoningEffort: input.provider.reasoningEffort }
: {}),
// Runnerd owns provider permissions for non-Codex facades. Their
// Codex-compatible surface must never open a second approval channel.
approvalPolicy:
input.provider.kind === "codex"
? (input.provider.approvalPolicy ?? "never")
: "never",
baseInstructions: nativeSystemInstructions(input),
instructionWorkingCopyRoot: "runtimeContext" in input ? input.runtimeContext.instructions.workingCopy?.rootPath : undefined,
includeSkillInstructions: isCodex && "runtimeContext" in input,
skillInputs: isCodex
? nativeTaskSkillInputs(
input.task.description,
"runtimeContext" in input ? input.runtimeContext : null,
)
: [],
requestedCollaborationMode:
supportsCollaborativePlanning && "executionMode" in input
? input.executionMode
: "default",
taskEnvelope: createCodexTaskEnvelope({
objective: input.completionContract.contract.objective,
contractRevision: input.completionContract.contract.revision,
criteria: input.completionContract.contract.criteria,
constraints,
}),
conversationMode: preparedContext ? "prepared" : "task",
runnerInstanceId:
options.runnerInstanceId ?? `paperclip-native-${input.binding.runId}`,
onSpawn: options.onSpawn,
transportFactory: options.transportFactory,
dynamicTools: options.dynamicTools,
dynamicToolHandler: options.dynamicToolHandler,
completionFeedback: options.completionFeedback,
environment: options.environment,
workingDirectoryAuthority: options.workingDirectoryAuthority,
driverIdentity,
capabilities: isCodex
? {}
: { steering: false, goals: false, threadLineage: false,
toolRefreshOnResume: input.provider.kind !== "acpx"
|| ACPX_CAPABILITY_PROFILES[input.provider.agent].toolRefreshOnResume === true },
collaborationModes: supportsCollaborativePlanning
? ["default", "plan"]
: ["default"],
requireProviderSessionIdentity: options.transportFactory !== undefined,
}),
preparedContext ? constraints : undefined,
);
}
/** Inspect the selected runnerd harness without starting a provider process. */
export function describeRunnerdNativeSessionBackend(
input: NativeExecutionInput,
): Promise<NativeSessionBackendDescriptor> {
return createTransportBackedNativeSessionBackend(input, {}).descriptor();
}
/**
* Uses the Codex JSON-RPC facade strictly as the TypeScript transport shape.
* Runnerd still selects and owns the real provider process from run.prepare.
*/
export function createRunnerdNativeSessionBackend(
input: NativeExecutionInput,
options: CodexNativeSessionBackendOptions,
): NativeSessionBackend {
if (!options.transportFactory) {
throw new Error("Runnerd native backend requires a transport factory");
}
return createTransportBackedNativeSessionBackend(input, options);
}
/**
* Constructs the first production-native provider boundary. Other provider
* contracts may already be persisted, but their runtime implementations are
* deliberately shipped in separate provider slices.
*/
export function createCodexNativeSessionBackend(
input: NativeExecutionInput,
options: CodexNativeSessionBackendOptions = {},
): NativeSessionBackend {
if (input.provider.kind !== "codex") {
throw new Error("Codex native backend requires provider kind codex");
}
return createTransportBackedNativeSessionBackend(input, options);
}