mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-20 15:09:57 +02:00
## 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. Source3a7349dpassed 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. Thee171282confirmation was stopped during build after fresh review found a completion-settlement race; it executed zero providers. Sourcec3e0cb303fixes 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. Freshc3e0cb303CI 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>
236 lines
8.8 KiB
TypeScript
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);
|
|
}
|