From e5f2fe5ead67b3c34dd583b77921b25e1c01f944 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 6 Oct 2026 16:24:52 -0500 Subject: [PATCH] refactor(native): use existing tool guidance for hiring Remove the obsolete fixed search_api/call_api hiring recipe. Keep the dedicated hiring tool, generic fallback and delegation guidance unchanged. Rotate checkpoints across the prompt revision and record bounded matched qualification. Co-Authored-By: Paperclip --- .../2026-10-06-native-hiring-guidance.md | 69 +++++++++++++++++++ .../src/contracts/runtime-context.ts | 4 +- .../native-session-resume.test.ts | 29 ++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 doc/plans/2026-10-06-native-hiring-guidance.md diff --git a/doc/plans/2026-10-06-native-hiring-guidance.md b/doc/plans/2026-10-06-native-hiring-guidance.md new file mode 100644 index 00000000000..d9767e4bfc8 --- /dev/null +++ b/doc/plans/2026-10-06-native-hiring-guidance.md @@ -0,0 +1,69 @@ +# Native hiring guidance — bounded qualification + +## Outcome and finish line + +Reduce native hiring guidance without worse hiring/reuse outcomes. Prepare one +reviewed PR with source checks, complete instruction measurement, and a matched +Codex/Claude/OpenCode Product E2E comparison. No merge is authorized. + +## Scope + +Base: `b508a05c43054ca9cfc54926eaa2af4a825e48dc` (includes #15372). +Branch: `codex/native-hiring-guidance`. +Remove only the obsolete fixed recipe that mandates `search_api`/`call_api` for +hiring. The existing available `hire_agent` tool already describes persistent +teammates, runtime inheritance and reuse through `list_agents`. Keep its schema, +authority and the generic API fallback unchanged. Preserve the helper-thread +boundary, all assigned-worker, dependency, connection and completion guidance. +Advance the fixed prompt revision to v6; verify old v5 checkpoints rotate while +same-context checkpoints remain reusable, including tool-refresh paths. + +## Measurement and comparisons + +Use the existing production full-tool-catalog measurement for all three native +transports at start, resume and continuation, plus the actual OpenCode MCP +catalog. Count serialized UTF-8 bytes and normalized projections, not billed +tokens or private vendor prompts. Baseline captured before edits at exact base. + +Select only `everyday-workflows..local.hire-reuse` for `runner-codex`, +`runner-acpx-claude`, and `runner-opencode`. Six cells total across candidate and +baseline; one attempt each, existing 720000 ms cell deadlines, 12-run bounds and +1000-cent company/lead hard stops. No completed-source behavioral rerolls. +Freeze sources, fixtures, controls and trusted workflow revision before dispatch. +Paid runs have the human's standing authorization. Original grades, partial +attempts, all actual runs, unknown charges and cleanup receipts stay preserved. + +The unchanged oracle requires one managed native teammate with the correct +manager/binding, actual child execution, reuse of that same identity, and both +usable deliveries. Inspect final parent artifact identity and tool selection +separately; passing aggregate grades alone do not establish these properties. +Earlier failed relocation and OpenCode cohorts remain historical failures. + +## Local admission + +The reduction is 123 UTF-8 bytes (23 words) in the fixed prompt. No text moves +into tool descriptions. Across all nine provider/phase projections, normalized +start/resume size changes from 53,341 to 53,218 bytes; continuation changes from +50,949 to 50,826 bytes. All 41 tool definitions and input projections remain +identical after workspace-path normalization. This is about 0.23% of the +start projection, not a token, bill, latency or vendor-prompt claim. + +The four focused files pass 1,929 distinct tests, including old-prompt rotation +and valid current-context resume. Product E2E admission passes 1,404 TypeScript +and 128 Node tests. Full repository typecheck and build pass. Product E2E +typecheck passes after refreshing stale built dependencies. The full repository +test run is pending. Repeat only the six credential-free measurement tests after +the candidate commit to attach a clean immutable source receipt. + +The first local test attempt exposed an incorrect new test control: it requested +a tool refresh from a provider marked as unable to refresh. The corrected control +uses the same capability flag. This did not change production behavior. + +## State + +- [x] Inspect current production contracts and capture clean baseline measurement. +- [x] Implement prompt-only hiring recipe removal and checkpoint boundary coverage. +- [x] Relevant tests, source-scope audit and candidate measurement (precommit). +- [ ] Freeze matched source refs and complete six bounded original live cells. +- [ ] Inspect grades, artifacts, source controls, usage and cleanup. +- [ ] Full checks, reviewed PR, truthful public summary and final readiness. diff --git a/packages/paperclip-runner/src/contracts/runtime-context.ts b/packages/paperclip-runner/src/contracts/runtime-context.ts index 850825c29b1..9492f9a65fc 100644 --- a/packages/paperclip-runner/src/contracts/runtime-context.ts +++ b/packages/paperclip-runner/src/contracts/runtime-context.ts @@ -1,8 +1,8 @@ import { createHash } from "node:crypto"; export const NATIVE_RUNTIME_ASSET_SCHEMA = "paperclip.runtime-asset.v1" as const; -export const PAPERCLIP_EXECUTION_PROMPT_REVISION = "paperclip-execution.v5" as const; -export const PAPERCLIP_EXECUTION_PROMPT = "You are running as a Paperclip agent. Complete the assigned task in the provided execution environment. Follow the attached agent instructions and use assigned skills and tools when relevant. Use Paperclip tools for coordination. To hire or reuse a persistent teammate, use list_agents, then search_api for agent-hires and call_api if a hire is needed. Provider helper threads do not create Paperclip agents. When the user assigns work or a revision to a teammate, use create_task with that agent's ID; review their result rather than doing their assigned work yourself. When remaining work depends on a child task, use set_dependencies to add its ID while preserving existing blocker IDs. Complete independent work, then call paperclip_block with the child agent as owner and child completion as the unblock action. End the turn so the child can use the workspace. Do not sleep or poll for child results while holding the workspace. Paperclip resumes the parent when the dependency completes. When the user asks to connect a service, call connections_search before any service tool, even when that tool is already installed. Follow the returned instruction and wait for any required user choice before executing. For other tasks needing a service, use installed tools if available; otherwise use connections_search and follow its instruction. The request appears as a card in the task. Finish independent work before yielding for access; do not poll or request the same connection repeatedly. Paperclip will continue automatically with updated tools after resolution. After a decline, pursue alternatives unless the user explicitly asks to retry. Finish exactly once with `paperclip_finish` or `paperclip_block`." as const; +export const PAPERCLIP_EXECUTION_PROMPT_REVISION = "paperclip-execution.v6" as const; +export const PAPERCLIP_EXECUTION_PROMPT = "You are running as a Paperclip agent. Complete the assigned task in the provided execution environment. Follow the attached agent instructions and use assigned skills and tools when relevant. Use Paperclip tools for coordination. Provider helper threads do not create Paperclip agents. When the user assigns work or a revision to a teammate, use create_task with that agent's ID; review their result rather than doing their assigned work yourself. When remaining work depends on a child task, use set_dependencies to add its ID while preserving existing blocker IDs. Complete independent work, then call paperclip_block with the child agent as owner and child completion as the unblock action. End the turn so the child can use the workspace. Do not sleep or poll for child results while holding the workspace. Paperclip resumes the parent when the dependency completes. When the user asks to connect a service, call connections_search before any service tool, even when that tool is already installed. Follow the returned instruction and wait for any required user choice before executing. For other tasks needing a service, use installed tools if available; otherwise use connections_search and follow its instruction. The request appears as a card in the task. Finish independent work before yielding for access; do not poll or request the same connection repeatedly. Paperclip will continue automatically with updated tools after resolution. After a decline, pursue alternatives unless the user explicitly asks to retry. Finish exactly once with `paperclip_finish` or `paperclip_block`." as const; export interface NativeRuntimeAssetReference { schema: typeof NATIVE_RUNTIME_ASSET_SCHEMA; diff --git a/server/src/services/native-runtime/native-session-resume.test.ts b/server/src/services/native-runtime/native-session-resume.test.ts index 60290408caf..8838b0c4ff9 100644 --- a/server/src/services/native-runtime/native-session-resume.test.ts +++ b/server/src/services/native-runtime/native-session-resume.test.ts @@ -2303,6 +2303,35 @@ describe("rebindNativeSessionCheckpoint", () => { ).toBeNull(); }); + it.each([false, true])("rotates the old hiring prompt even with tool refresh %s", (toolRefreshOnResume) => { + const current = execution(currentRunId); + const previous = execution(previousRunId); + const oldText = previous.runtimeContext.prompt.text.replace( + "Provider helper threads do not create Paperclip agents.", + "To hire or reuse a persistent teammate, use list_agents, then search_api for agent-hires and call_api if a hire is needed. Provider helper threads do not create Paperclip agents.", + ); + // A real retained v5 input: keep its text and both digests internally consistent. + const oldContext = { + ...previous.runtimeContext, + prompt: { + revision: "paperclip-execution.v5", + text: oldText, + digest: createHash("sha256").update(oldText).digest("hex"), + }, + } as unknown as typeof previous.runtimeContext; + oldContext.aggregateDigest = canonicalNativeRuntimeContextDigest(oldContext); + expect(rebindNativeSessionCheckpoint({ + previousRun: previousRun({ nativeExecutionInput: { ...previous, runtimeContext: oldContext } }), + currentExecution: current, + toolRefreshOnResume, + refreshTools: toolRefreshOnResume, + })).toBeNull(); + expect(rebindNativeSessionCheckpoint({ + previousRun: previousRun(), currentExecution: current, + toolRefreshOnResume, refreshTools: toolRefreshOnResume, + })).not.toBeNull(); + }); + it("rotates when assigned context changes but permits a fresh run-scoped MCP binding", () => { const reboundCredential = execution(currentRunId); reboundCredential.runtimeContext.mcp.bindingId = "native-mcp:fresh-run";