Files
deepseek-harness/.agents/notes/implemented/feature/2026-08-04-claude-code-and-codex-subagent-backends.md

21 KiB

Agent Note: Claude Code and Codex subagent backends

Status: implemented

English | 中文

Problem

The named ctx.subagents registry lets a parent agent delegate work without knowing how the child runs, but the harness needs first-party routes to the real Codex and Claude Code products. Each route must hand the product one self-contained task, let it work in the parent Session's workspace, return a final answer or an explicit failure or cancellation, and leave no managed product process behind.

The product integrations must not become second owners for task text, cwd, cancellation, result settlement, or process trees. Required evidence therefore separates three facts: a keyless real-product test proves the official integration, native authentication shape, deterministic answer, and teardown; a Loader composition test proves that the public package and documented tool configuration load without starting the product; and a credentialed e2e proves that the production provider and real product can obtain a unique answer from the real DeepSeek service. Direct model HTTP or a product double cannot replace either product-running tier, and a hand-mounted plugin cannot replace the Loader tier.

Decision

The harness publishes two sibling one-shot provider packages whose default registry names are codex and claude-code. This note owns their product protocols, result mapping, and process lifecycle; the named-instance decision owns Profile-selected provider identity, optional instance model, and static tool binding; the production-install exclusion decision owns their independent optional Bundles and host-plane placement; the product one-shot background decision owns the model-visible scheduling choice; the non-interactive permissions decision owns each product Provider's Profile-selected mode and safe permission decisions; and the minimal-diagnostics decision owns coarse product action categories. Both packages accept multiple named instances. Loading either provider starts no product process, and each tool accepts only a standalone text task; product and instance selection remain deployment configuration.

Both providers report inheritsParentContext: false, advertise no optional start capabilities, and pass the parent Session cwd without copying the parent conversation. Their documented tools use backgroundMode: 'one-shot' and maxDepth: 'provider-managed': the consumer keeps foreground collection as the default and may place the same run in the generic Job runtime, while recursion policy stays with the out-of-process product. Every call creates a fresh product process and a non-resumable product conversation. ctx.subagents owns named-request resolution and paired lifecycle events; dsh-tool-subagent owns model-visible scheduling and foreground-versus-Job adaptation; ctx.jobs and dsh-tool-jobs own Job ids, state, output, controls, notices, and parent-owner cancellation; each product provider owns native result mapping, while dsh-subprocess owns credential scrubbing, process-tree termination, and whole-tree exit observation.

configured tool -> dsh-tool-subagent -> ctx.subagents -> product provider -> product process
  foreground <- final product outcome
  background -> ctx.jobs / dsh-tool-jobs -> Job id / state / notice / controls
  both -> provider disposal -> dsh-subprocess -> whole-tree exit

Ownership and lifecycle

Layer Owner Responsibility Observable result
Delegation lifecycle ctx.subagents Resolve the named provider request and pair lifecycle events around the published SubagentRun Unsupported context or malformed input fails before a run is published; start and terminal events remain paired
Scheduling and adaptation dsh-tool-subagent Interpret run_in_background, choose foreground collection or one-shot Job registration, and map the shared stop reason Foreground returns the product outcome; background returns a Job id after registration
Job state and control ctx.jobs and dsh-tool-jobs Own Job state, output, cancellation, owner cleanup, completion notices, and model-facing controls The exact parent can collect, list, or stop background work and receives its completion notice
Native run and teardown Product provider and dsh-subprocess Produce one native result, close the product protocol, request best-effort native cancellation, and prove process-tree exit Foreground return and Job settlement both wait for idempotent disposal and whole-tree exit

Codex provider

@deepseek-ai/dsh-subagent-codex registers a Profile-selected provider name that defaults to codex, resolves the codex bin declared by its pinned @openai/codex@0.149.1 package, and starts that wrapper through the current Node executable with app-server --stdio. The wrapper selects the private native platform payload; the provider neither resolves nor falls back to a host codex. Its public configuration contains a non-empty providerName, an optional non-empty model, an explicit env overlay, a positive finite disposeGraceMs no greater than the repository's shared MAX_TIMER_DELAY_MS, and a three-value native permissionMode that defaults to never. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged on every ephemeral thread/start; omission leaves native Codex settings authoritative. Installation, login, CODEX_HOME, model discovery or fallback, base URL, and product-session settings remain native Codex or deployment responsibilities; the selected mode owns only the thread approval/reviewer/sandbox fields described by the non-interactive permissions decision.

Before publication, the provider validates a non-empty text-only task, starts the managed app-server in the parent workspace, completes initializeinitialized, maps the optional model and resolved mode into official thread/start fields, and creates an ephemeral: true thread. The fixed app-server argv contains no model, mode, or task text. The published run owns exactly one turn/start; its thread and turn ids remain private and are never persisted in the parent Session.

turn/completed is the authoritative remote terminal fact. The latest agentMessage with phase: "final_answer" wins, and that selected message must contain nonblank text. When the product emits no explicit final phase, the latest message with phase: null is the compatibility fallback and must likewise be nonblank; commentary never replaces either answer. The minimal-diagnostics decision owns Codex action categories, HTTP status, lifecycle stages, process outcomes, and stop-reason preservation. Local cancellation remains aborted without a failure diagnostic.

For command and file approvals, the unattended wire selects a non-approval decision offered by the request, preferring cancel; the stable 0.149.1 request shape without an offered-decision list falls back to decline. It grants no requested permissions for the turn, answers user-input requests with no answers, and declines MCP elicitation. It records safe categories for those requests, declined command/file items, and structured sandboxError terminals. Product stderr is forwarded unchanged to the Host but is neither classified nor copied into the diagnostic. A request with no legal unattended response, or any unknown server request, fails the run instead of waiting for a user interface the provider does not supply.

An unpublished startup failure closes the wire, terminates the acquired process tree, waits for exit, detaches the stderr observer, and then rejects start() with its fixed operation stage. Published disposal best-effort interrupts a known turn, closes the wire, ends stdin, invokes the shared termination escalation, waits for whole-tree exit, and detaches the observer. Independent cleanup failure reports teardown; when startup and rollback both fail, the aggregate's top message retains both safe stage lines while the underlying causes remain internal.

Codex 0.149.1 speaks the Responses protocol, while DeepSeek's public OpenAI-compatible endpoint speaks Chat Completions. The credentialed Codex e2e therefore uses a loopback-only, test-private bridge for one no-tool nonce request: real Codex sends Responses to the bridge, the bridge forwards the received bearer credential and extracted task to the fixed official DeepSeek endpoint, and it wraps the real text in the minimal Responses SSE lifecycle. The bridge is neither a production proxy nor evidence that Codex connects to DeepSeek Chat Completions natively.

Claude Code provider

@deepseek-ai/dsh-subagent-claude-code registers a Profile-selected provider name that defaults to claude-code and invokes @anthropic-ai/claude-agent-sdk@0.3.241. The provider omits pathToClaudeCodeExecutable, so the SDK selects Claude Code 2.1.241 from the matching OS, CPU, and Linux-libc platform package in its own optional dependency closure. The provider does not resolve or fall back to a host claude; an omitted, unsupported, missing, or damaged platform payload fails the first delegation at the SDK startup boundary. The provider uses the official query() entrypoint and passes the SDK's native claude or claude.exe command, arguments, cwd, environment, and forwarded signal from spawnClaudeCodeProcess to dsh-subprocess; its private SpawnedProcess adapter exposes only the stream, event, kill, and exit facts the SDK requires.

The public configuration contains a non-empty providerName, an optional non-empty model, an explicit env overlay, a positive finite disposeGraceMs no greater than the repository's shared MAX_TIMER_DELAY_MS, and a five-value native permissionMode that defaults to dontAsk. Each named instance retains those resolved values for its own runs. An explicit model is passed unchanged through Options.model; omission leaves that field absent so native settings choose the model. Each run creates its own AbortController, sets persistSession: false, disables AskUserQuestion, and passes the resolved mode to the SDK; only bypassPermissions receives the SDK's explicit dangerous confirmation. The provider deliberately omits settingSources, so the SDK reads the host's normal user, project, and local Claude settings relative to the parent Session cwd. It neither copies nor filters those settings and does not create or modify login state. Remaining permission prompts are denied, MCP elicitation is declined, and blocking dialogs fail closed instead of waiting for a user interface the provider does not own.

The provider publishes only after both the SDK Query and a live managed CLI handle exist. It consumes the complete SDK stream and completes only when a result message has subtype: "success", is_error: false, and a nonblank result, and the iterator then ends normally. The minimal-diagnostics decision owns every non-success action category, stage, process outcome, and its ordering with a contributing permission decision. Local cancellation wins and becomes aborted without either diagnostic fact.

Startup rollback and published disposal close the SDK query, abort the per-run controller, invoke shared process-tree termination, and wait for whole-tree exit. Query.close() expresses graceful protocol intent but does not replace the subprocess owner's exit proof. An unpublished failure exposes only fixed query-start facts; a published process failure can expose its independent exit code and signal; an independent cleanup rejection exposes teardown. Original SDK, Host, and cleanup errors remain on internal cause chains and logs rather than entering the diagnostic.

The credentialed Claude Code e2e uses the official DeepSeek Claude Code contract directly: the runtime-only DeepSeek key becomes ANTHROPIC_AUTH_TOKEN, the fixed official base gains /anthropic, and the main and subagent model variables select the documented DeepSeek models. It starts the production provider and real SDK/CLI, requires one random nonce as the complete answer, persists no credential in settings, and waits for every managed handle to exit.

Distribution and evidence

Each product owns branch-complete package tests, a required keyless real-product spec, a Loader composition e2e, and a credentialed DeepSeek e2e. The keyless product tier uses the exact official distribution under test, a non-empty fake product key, an isolated temporary workspace and product home, and a loopback fixed-answer model. Missing product requests, wrong authentication, altered task text, a non-exact answer, a skipped real product, or a surviving managed handle fails the required test. The Codex Loader fixture exposes two named Codex instances and tools; the Claude Code Loader fixture exposes the default Codex tool plus two named Claude Code instances and tools. Both fixtures include generic Job controls and start neither product process. The credentialed tier starts the same production provider and real product with a runtime-only key, requires a unique nonce from the fixed official DeepSeek service, and proves quiescence again; it self-skips only when a local operator supplied no key, while trusted CI preflights the secret.

The Codex evidence pins @openai/codex@0.149.1, codex-cli 0.149.1, and all six optional platform aliases. Its generated schema proves optional ThreadStartParams.model; the real-product spec observes omitted-model inheritance, two explicit instance models, the package-local wrapper argv, exact Bearer key, original task, byte-exact final answer, native permission modes, explicit dangerous-bypass writing in suite-owned temporary storage, and wrapper/native whole-tree exit. An isolated wrapper fixture proves missing-payload failure without host fallback, named instances retain separate models, environments, and modes, and production never resolves a host codex from PATH. The minimal-diagnostics decision owns failure, process-outcome, and final presentation evidence.

The Codex credentialed e2e registers the production provider, starts the same real app-server, and requests one random nonce through the test-private bridge described above. It fixes the external endpoint and model, stores no credential or request payload, requires exactly one completed upstream response, compares the trimmed product answer byte-for-byte with the nonce, and waits for every managed handle to exit.

The Claude Code evidence pins Agent SDK 0.3.241, Claude Code 2.1.241, and all eight SDK platform packages. Its real-product spec lets the SDK select the installed payload, asserts that the shared subprocess argv begins with that package's native CLI, and observes omitted-model inheritance, two explicit instance models, the exact x-api-key, original task, byte-exact final answer, native permission modes, suite-owned denied and bypassed writes, and whole-tree exit. Package tests prove that production never resolves host PATH, omits the executable override, and forwards the SDK-selected Windows claude.exe without a batch shim. This evidence proves the pinned official SDK/CLI integration rather than compatibility with independently installed Claude versions; the minimal-diagnostics decision owns failure and process-outcome evidence. Loader coverage resolves both products through their optional Bundle patches while starting neither product.

The Claude Code credentialed e2e maps the key and fixed official endpoint only in the provider's in-memory environment, uses the documented deepseek-v4-pro[1m] and deepseek-v4-flash model variables, and traverses the production provider, official SDK, and real CLI. It compares the trimmed result with a random nonce and proves whole-tree exit without calling the Messages API directly from the test.

The project owner's distribution authorization is scoped to the official @anthropic-ai/claude-agent-sdk identity and the official Claude Code CLI/platform payloads each SDK version declares through optionalDependencies. THIRD_PARTY_NOTICES.md derives and discloses the current payload set without reclassifying its declared terms as permissive. Version, license-field, and payload-set changes still undergo ordinary dependency, lockfile, compatibility, terms, and notices review; unrelated non-permissive runtime packages continue to fail closed.

Alternatives considered

Direct model HTTP, codex exec, or a hand-written Claude CLI protocol. These paths bypass the products' official extensible integration surfaces and cannot prove native configuration, tools, approvals, result semantics, or teardown. Each provider uses its official product integration instead.

A shared product-process helper package. The existing subagent and subprocess seams already own every shared task, result, environment, and process-tree concern. A new helper would duplicate ownership without deleting either private product adapter, so each adapter calls the existing seams directly.

A model-visible product selector. Product availability, instance configuration, and authentication are deployment facts. Profile-bound tools keep each schema and provider binding explicit and avoid adding dynamic selection state to the common service.

Product doubles as required evidence. Doubles cover exhaustive private protocol branches but do not prove package exports, official distributions, authentication, or real process behavior. Required evidence drives each official product against a loopback model fixture.

Plugin-managed login, product home, model discovery or fallback, settings, sandbox rules, or fine-grained permission policy. Those choices would create another authority beside each product's native configuration and enlarge a one-shot provider into account management. A Provider may pass one opaque Profile-selected model override where the official product supports it, but it does not discover, validate, alias, or fall back between models. Neither Provider mirrors product rules or adds a human interaction channel.

Continuation, progress, product-native background state, and shared parent context. The provider payload remains one final answer for one self-contained task. The generic Job layer may add its id, status, notice, collection, and cancellation results, but product sessions, resume, follow-up, intermediate messages, parent transcript transfer, structured output, and provider-specific background state need separate user contracts and are not prebuilt.

Consequences

Users delegate through Profile-configured one-shot tools backed by the official product integrations. Explicit Profile installation and host-plane provider placement are owned by the production-install exclusion decision; named instance identity and tool binding are owned by the named-instance decision; per-Preset tool exposure and foreground-default optional Job scheduling are owned by the product one-shot background decision. This note's provider lifecycle keeps native settings and behavior while shared services retain the sole ownership of job settlement and process-tree quiescence.

Every delegation pays for a fresh product process and independent model context. Successful product payload remains final assistant text; a failed product run may separately expose the shared safe diagnostic containing provider-owned permission facts and safe product failure categories. Background scheduling additionally exposes generic Job ids, status, completion notices, and collection or cancellation results. Both products use Bundle-pinned platform CLIs plus native account and workspace settings and the selected Provider permission mode; a supported optional instance model overrides only that run's native model selection. Credentialed e2e runs also spend external API quota and depend on the official DeepSeek endpoint; deterministic protocol, failure, cancellation, and approval coverage remains in the keyless tier. The providers do not resume sessions, stream progress, accept new human interaction, roll back tool or file side effects, or impose a wall-clock timeout.

Compatibility is pinned by package-level unit coverage, keyless real-product loopback tests, credentialed DeepSeek nonce tests, public Loader composition, built-package and NodeNext consumer checks, generated documentation and notices, and the repository CI matrix. A supported product or DeepSeek endpoint/model baseline change must refresh those facts; production performs no separate runtime version probe.