AgentHarness.start_session/2 accepts a common set of session options and a
provider_options escape hatch. Common options express intent; adapters map
them to the provider's native protocol.
AgentHarness.start_session(provider,
cwd: "/absolute/workspace",
model: "provider-model-name",
system_prompt: "Project-specific instructions",
approval_policy: :on_request,
sandbox: :workspace_write,
mcp_servers: %{},
skills: [],
env: %{},
provider_options: %{},
event_buffer_size: 1_000,
completed_turn_cache_size: 1_000,
startup_timeout: 30_000,
startup_finalization_timeout: 5_000,
turn_start_timeout: 30_000,
provider_command_timeout: 30_000,
store_failure: :degrade
)Provider values are not artificially reduced to one cross-provider enum. Both
adapters map the common approval_policy and sandbox fields, but each value
must use that provider's native shape. For Claude, approval_policy becomes
permission_mode, while sandbox uses the claude_code sandbox schema.
startup_timeout bounds Store reconciliation and provider opening for one
session. After the provider opens, startup_finalization_timeout separately
bounds the initial Store commit and each cleanup phase; it defaults to 5_000
milliseconds. The public caller follows the phase transition, so one budget is
not silently consumed by another. Readiness ends with a bounded two-way caller
acknowledgement. Until the SessionServer processes that acknowledgement, Store
snapshots carry a pending startup-attempt marker. A partial or hard-killed
unacknowledged aggregate is therefore reclaimable on the next start even if its
last write had already reached status: :idle. A handshake that does not finish
within that phase budget returns {:error, :session_start_ack_timeout} and the
pending attempt remains safely reclaimable.
turn_start_timeout bounds the provider's asynchronous admission of a locally
accepted turn. These are per-session positive millisecond values. If turn
admission crosses its deadline, AgentHarness cannot know whether upstream work
began, so it fails the turn and retires that SessionServer instead of reusing
the conversation as idle.
provider_command_timeout is the SessionServer watchdog for respond/2 and
the provider-facing part of cancel/1. It defaults to 30_000 milliseconds.
An uncertain command timeout retires the provider session so an answer or
cancellation that may already have crossed the transport boundary is never
treated as safely retryable.
store_failure: :degrade keeps a session alive after a write failure, switches
its status snapshot to durability: {:degraded, failure}, and publishes a
non-durable :store_failed event. Use store_failure: :stop when fail-stop
durability is required.
event_buffer_size bounds recent replay events. completed_turn_cache_size
bounds completed Turn values, terminal events, and request records held by the
live SessionServer; it defaults to the event-buffer size and may be zero or
:infinity. A configured Store remains the source for older cold lookups. The
built-in Memory Store retains its full journal until guarded purge, so use a
durable Store with an explicit retention policy for high-volume fleets.
Process-level limits and internal command deadlines are application settings:
config :agent_harness,
max_sessions: 100,
max_provider_processes: 200,
max_runner_tasks: 500,
session_call_timeout: 30_000,
provider_command_call_timeout: 60_000,
codex_call_timeout: 25_000,
codex_startup_call_timeout: 25_000,
claude_call_timeout: 5_000,
claude_interrupt_timeout: 3_000,
claude_stop_timeout: 1_000,
store_call_timeout: 5_000,
provider_close_timeout: 5_000,
provider_open_shutdown_grace: 250,
session_shutdown_timeout: 60_000The three max_* settings default to :infinity. Configure them before the
application starts. The default provider-command deadline order is outward:
the built-in transport call is shorter than provider_command_timeout, which
must be shorter than provider_command_call_timeout. This lets the adapter
report first, then the SessionServer retire uncertainty, before the public
caller can give up. The default turn-start watchdog is likewise longer than
either built-in provider call, and session shutdown has more time than provider
cleanup. A built-in provider call timeout is classified as uncertain and
retires the affected session.
provider_open_shutdown_grace is the non-negative number of milliseconds the
built-in opening-runtime guardian allows supervised cleanup before forcing that
runtime down when its SessionServer disappears during startup. It defaults to
250. session_shutdown_timeout defaults to 60_000 milliseconds so normal
session shutdown has room for bounded provider cleanup and final persistence.
store_call_timeout bounds calls to the built-in Memory Store; a custom Store
must enforce its own I/O deadline because its callback runs in the calling
process.
Bring your own CLI and authentication
AgentHarness launches a CLI (local by default) and relies on credentials that
CLI's environment already owns. It does not open login flows, accept
passwords, store provider credentials, or choose a paid plan for you. The
selected executable, any custom client module, and any AgentHarness.Exec
implementation are trusted code boundaries.
Authenticate each CLI outside your Elixir application:
$ codex login
$ claude
Then verify a small request directly in the CLI before debugging AgentHarness.
AgentHarness defaults to guarded :subscription mode. API keys, cloud
providers, custom endpoints, and other intentional routes require
auth: :inherit. Plan eligibility and billing are provider policy, not a
library capability; read Billing and authentication
before unattended use.
Credentials in logs
Session env, provider_options, and mcp_servers can carry credentials,
so SessionConfig redacts them when inspected: top-level keys stay visible
with "[REDACTED]" values. The SessionServer and each provider session
also scrub their raw state term via format_status/1, so the state
rendered in crash reports and :sys.get_status/1 is display-safe even
under Erlang ~p formatting, which bypasses the Inspect protocol.
One path stays open: supervisor progress and child-start reports include
the unredacted start arguments of a session process, config included.
Elixir's logger drops those SASL reports by default; leave
handle_sasl_reports off in any environment whose logs are shipped or
retained.
Codex authentication and environment
Codex defaults to the fail-closed subscription mode:
provider_options = %{auth: :subscription}This mode uses the ChatGPT login in the selected Codex home. Before launching the app-server, AgentHarness:
- clears API keys, endpoint variables, and OSS/custom-provider selectors;
- forces the SDK to no API key, the official base URL, and the OpenAI backend;
- validates the SDK's resolved model payload contains no environment or config overrides;
- pins every app-server thread to the built-in
openaimodel provider; - rejects model payloads, governed/remote execution surfaces, raw config overrides, custom provider backends, OSS providers, and thread profiles;
- inspects effective user, project, and system Codex configuration and rejects custom model providers, custom OpenAI base URLs, provider/profile tables, or an API-forced login method.
These checks prevent ambient or per-session provider routing from silently
switching the child to API billing. auth: :inherit is the explicit escape
hatch for those advanced configurations.
AgentHarness also inspects the selected file-backed Codex auth record before
launch. Stored api_key and bedrock_api_key modes are rejected. A keyring
store cannot currently be inspected by the Elixir SDK, so subscription mode
fails closed for that store as well. The validated CODEX_HOME is then set
explicitly in the child environment so an unvalidated inherited home cannot
take precedence.
To isolate a Codex home or account:
{:ok, session} =
AgentHarness.start_session(:codex,
cwd: "/work/project",
env: %{"CODEX_HOME" => "/work/codex-profile"}
)Environment entries are merged into the app-server process environment. Subscription mode overwrites auth- and provider-routing entries after that merge. Avoid putting credentials directly into logs, metadata, or exception messages.
To intentionally preserve API credentials or another custom environment, select:
provider_options = %{auth: :inherit}The only accepted Codex auth modes are :subscription and :inherit.
AgentHarness does not refresh ChatGPT tokens or generate upstream attestation
tokens itself. If an app-server asks the host for either operation, the active
turn fails and that provider connection is torn down rather than leaving an
unresolvable request pending.
Claude authentication modes
Claude defaults to:
provider_options = %{auth: :subscription}This uses the globally installed claude executable and refuses to start
unless claude auth status reports a logged-in, first-party claude.ai
account. The check uses the selected executable, working directory, and
scrubbed child environment.
Subscription mode forcibly unsets known higher-precedence or alternate routing configuration, including:
ANTHROPIC_API_KEY,ANTHROPIC_AUTH_TOKEN, andANTHROPIC_BASE_URL;- custom auth headers and provider-specific base URLs;
- Bedrock, Mantle, Vertex, Foundry, and Claude Platform on AWS selectors;
- ambient
CLAUDE_CODE_OAUTH_TOKENand refresh-token overrides; - host-managed credential-file, credential-variable, and provider-routing selectors.
It also fails session startup if either of these API-key sources is configured:
provider_options: %{api_key: "..."};config :claude_code, api_key: "...".
Those checks matter because Claude Code's documented authentication precedence
places cloud providers, bearer tokens, API keys, and configured key helpers
ahead of the saved /login subscription. Remove the conflicting configuration
to use subscription mode, or select auth: :inherit when another credential is
intentional. See
Claude Code authentication and its
environment-variable reference.
Because CLI settings, setting_sources, arbitrary extra_args, or a custom
SDK adapter can install an apiKeyHelper or re-enable a credential source,
subscription mode rejects nonempty values for those provider options. It pins
the local Port adapter and supplies empty settings, source, and argument
values, overriding auth-sensitive config :claude_code defaults. Use the
common session options for sandbox, MCP, skills, model, and tools; select
auth: :inherit if custom Claude settings or adapters are intentional.
There is one boundary the library cannot override: Claude Code always applies
organization-managed policy, and managed settings have higher precedence than
CLI arguments. Server-managed settings, MDM policy, registry/plist policy, or
system managed-settings.json files can inject env or an apiKeyHelper
after AgentHarness's scrub. claude auth status does not reveal every such
route. Treat managed policy as trusted external configuration and verify it
with your administrator before relying on subscription-only billing. See
Claude Code settings and precedence
and SDK settings behavior.
To let the child process inherit your environment unchanged:
provider_options = %{auth: :inherit}Use :inherit only when intentional—for example, when an OAuth token or API
key is managed by your local environment. The only accepted auth modes are
:subscription and :inherit.
MCP servers
MCP configuration belongs to the logical session:
mcp_servers = %{
"project-files" => %{
command: "project-mcp",
args: ["--root", "/work/project"],
env: %{"LOG_LEVEL" => "warning"}
},
"internal-docs" => %{
url: "https://docs.example.test/mcp"
}
}
{:ok, session} =
AgentHarness.start_session(:codex,
cwd: "/work/project",
mcp_servers: mcp_servers
)Use strings for stable MCP server names. Configuration values are provider configuration, so the exact accepted fields depend on the installed CLI.
Codex inserts the map into the app-server thread's config.mcp_servers.
Nested atom keys are converted to strings. In auth: :inherit mode, existing
MCP entries supplied in Codex thread_options.config are merged, with common
mcp_servers entries taking precedence by server name. Subscription mode
rejects caller-supplied raw thread config and builds the MCP map itself.
Claude always passes an explicit inline MCP map and enables strict MCP mode,
including when the map is empty. This prevents SDK application defaults and
global CLI MCP servers from leaking into a session. To opt into global Claude
MCP configuration, set provider_options: %{strict_mcp_config: false}; doing so
weakens per-session isolation.
The underlying Claude SDK also accepts BEAM-native MCP server modules. That is a Claude-specific extension; do not rely on it in provider-neutral code.
Skills
The common skills option accepts paths or descriptors:
skills = [
"/work/skills/testing/SKILL.md",
%{name: "release", path: "/work/skills/release/SKILL.md"}
]Codex skills
Codex normalizes each entry into an explicit skill input:
%{type: :skill, name: "release", path: "/work/skills/release/SKILL.md"}Skill inputs are prepended to every turn, and skills_enabled is enabled on
the thread. A Codex skill descriptor may set enabled: false to omit it.
Paths are sent to Codex as configured. Validate that they exist and are readable before starting orchestration.
Claude skills and plugins
Claude accepts three path forms:
- a plugin root containing
.claude-plugin/plugin.json; - a directory containing
SKILL.md; - a direct path to
SKILL.md.
Existing plugin roots are loaded directly. Standalone skill directories are copied, including supporting files, into one generated session plugin under the system temporary directory. The generated plugin is removed when the provider session closes.
The adapter adds the Skill tool to allowed_tools when it creates a plugin.
If you provide your own restrictive tool configuration, keep Skill allowed.
This translation is why Claude reports skill support as :emulated while
Codex reports it as :native.
Codex options
Codex uses the codex_sdk app-server transport. A complete configuration may
look like:
{:ok, session} =
AgentHarness.start_session(:codex,
cwd: "/work/project",
model: "your-codex-model",
system_prompt: "Use the project's conventions.",
approval_policy: :on_request,
sandbox: :workspace_write,
provider_options: %{
auth: :subscription,
codex_path: "/opt/bin/codex",
codex_options: %{},
connect_options: %{
init_timeout_ms: 10_000
},
thread_options: %{
web_search_enabled: true,
model_reasoning_summary: :concise
},
turn_options: %{
effort: :medium
}
}
)Recognized layers:
| Key | Applied to |
|---|---|
:auth | :subscription (default) or intentional :inherit |
:client | Trusted test/custom Codex client module |
:codex_path | Executable override |
:codex_options | Codex.Options construction |
:connect_options | App-server connection/initialization |
:exec | Remote execution (see below); requires :inherit |
:thread_options | New or resumed Codex thread |
:turn_options | Every turn in this AgentHarness session |
:provider_session_id | Resume a specific Codex thread |
:thread_id | Alias for :provider_session_id |
:resume | Alias for an exact thread ID |
String forms of documented option keys are canonicalized without creating arbitrary atoms.
Common session values are used as defaults:
cwd → thread working_directory
model → thread model
system_prompt → thread developer_instructions
approval_policy → thread ask_for_approval
sandbox → thread sandboxExplicit safe thread_options override those defaults. Subscription mode
reserves provider-, profile-, and raw-config routing keys; use auth: :inherit
when those settings are intentional. Each call to start_turn/3 may also
supply Codex turn options:
AgentHarness.start_turn(session, "Analyze this failure",
effort: :high,
additional_context: "CI failed only on Linux",
output_schema: schema
)id and metadata remain AgentHarness options and are not sent to Codex.
Run the Codex app-server in another execution environment
Setting exec: runs codex app-server through an AgentHarness.Exec
implementation instead of the SDK's subprocess transport. The app-server's
JSON-RPC protocol — the initialize handshake, threads, turns, approvals,
interrupts — stays in your application's node; only the CLI process moves.
{:ok, session} =
AgentHarness.start_session(:codex,
cwd: "/workspace",
provider_options: %{
auth: :inherit,
exec: {MyApp.SandboxExec, sandbox: sandbox},
codex_path: "codex"
}
)Remote execution requires auth: :inherit because :subscription verifies
local Codex state (config layers, the credential store) that says nothing
about the environment the app-server would run in. The spawn spec is
remote-safe: codex_path and cwd resolve in the execution environment, and
the process environment is built only from the SDK credential overrides and
the session env. Model routing config (model_payload) is not translated
into app-server flags; set the model per session or per thread instead.
As with the Claude exec adapter, transport loss fails the turn and retires
the session, and skills: paths must exist where the CLI runs. See
Writing an Exec implementation for
building the exec module itself.
Resume a Codex conversation
Provider session IDs appear in AgentHarness.status/1, :session_updated
events, terminal result data, and the Store snapshot.
To create a new AgentHarness process around an existing Codex thread:
{:ok, resumed} =
AgentHarness.start_session(:codex,
cwd: "/work/project",
provider_options: %{provider_session_id: saved_thread_id}
):last is deliberately rejected. In the SDK's app-server transport it does
not identify an exact thread and can silently start a new one. Resolve and
persist the concrete Codex thread ID instead. AgentHarness does not
automatically reconstruct stopped sessions from Store in v0.x.
Codex structured input
String prompts are converted to text input. Codex also accepts a list of structured input maps:
AgentHarness.start_turn(session, [
%{type: :text, text: "Review this image"},
%{type: :local_image, path: "/work/screenshot.png"}
])Configured skill items are prepended automatically.
Claude options
Claude uses the claude_code Elixir SDK to drive the globally installed CLI
with a bidirectional stream:
{:ok, session} =
AgentHarness.start_session(:claude,
cwd: "/work/project",
model: "sonnet",
system_prompt: "Use the project's conventions.",
approval_policy: :default,
sandbox: %{enabled: true},
provider_options: %{
auth: :subscription,
allowed_tools: ["Read", "Grep", "Glob", "Bash(mix test:*)"],
disallowed_tools: ["Bash(rm:*)"],
max_turns: 20,
readiness_timeout: 15_000,
question_timeout: 300_000
}
)AgentHarness consumes these internal keys:
| Key | Meaning |
|---|---|
:auth | :subscription (default) or :inherit |
:auth_check_timeout | Bound for claude auth status; default 5_000 ms |
:readiness_timeout | Time to wait for the CLI initialization handshake; default 10_000 ms |
:question_timeout | Time to wait for your response; default :infinity |
:client | Trusted test/custom client module |
Other provider options are passed to ClaudeCode.start_link/1 after common
fields are mapped, except that subscription mode rejects or replaces
auth-sensitive api_key, settings, setting_sources, extra_args, and
adapter values. Useful options include allowed_tools, disallowed_tools,
permission_mode, max_turns, resume, fork_session, agents,
output_format, and plugins. Refer to the installed claude_code dependency
and Claude Code version for the complete accepted set.
Unknown binary option names are rejected rather than converted to atoms. Prefer atom-keyed options in application code.
Partial messages are enabled by default, producing character-level
:message_delta, :thinking_delta, and :tool_input_delta events. Set
include_partial_messages: false to reduce event volume; complete
assistant/tool messages and the terminal result remain available.
Claude maps common fields directly to Claude-native SDK options:
approval_policy: :default, # Claude `permission_mode`
sandbox: %{enabled: true} # Claude SDK sandbox configurationConsult the installed claude_code version for the sandbox schema. Do not use
dangerous permission-bypass modes unless the CLI is already contained by a
separate, trusted sandbox. Explicit provider_options.permission_mode or
provider_options.sandbox values override the corresponding common field.
Session startup waits for Claude's CLI initialization handshake before emitting
:session_ready. readiness_timeout bounds that wait. The handshake verifies
transport initialization, not whether a later model request will pass account,
quota, or model checks; those errors can still arrive during a turn.
The subscription auth check is a separate, no-model CLI command and does not
consume inference usage. Its timeout is controlled by auth_check_timeout.
Claude turn filters are applied by AgentHarness after reading the SDK stream.
They can suppress nonterminal provider messages, but the terminal
ResultMessage is always retained so a successful turn cannot become a
:missing_result failure.
Claude currently supports one-shot approvals. A
Response.approve(scope: :session) returns
{:error, {:unsupported_approval_scope, :session}} rather than silently
downgrading the requested scope.
Question timeout
Claude's can_use_tool callback waits while your Elixir application resolves a
question or permission request. With the default :infinity, the Claude turn
can remain blocked indefinitely.
Set a positive millisecond timeout to fail closed:
provider_options = %{question_timeout: 60_000}When it expires, AgentHarness denies the tool and interrupts that request. This
is independent of await/2 and stream timeouts.
Resume or fork Claude
{:ok, resumed} =
AgentHarness.start_session(:claude,
cwd: "/work/project",
provider_options: %{
auth: :subscription,
resume: saved_claude_session_id
}
)
{:ok, forked} =
AgentHarness.start_session(:claude,
cwd: "/work/project",
provider_options: %{
auth: :subscription,
resume: saved_claude_session_id,
fork_session: true
}
)AgentHarness records updated Claude session IDs, but v0.x does not automatically recreate a stopped session from Store.
Run the Claude CLI in another execution environment
AgentHarness.Providers.Claude.Adapter.Exec is a ClaudeCode.Adapter that
spawns the CLI through an AgentHarness.Exec implementation instead of a
local port. The stream-json protocol, control handshake, and
question/approval routing stay in your application's node; only the CLI
process moves. AgentHarness.Exec.Local reproduces local behavior, and an
application-provided exec module can place the process in a remote sandbox.
{:ok, session} =
AgentHarness.start_session(:claude,
cwd: "/workspace",
env: %{"ANTHROPIC_API_KEY" => api_key},
provider_options: %{
auth: :inherit,
adapter: {
AgentHarness.Providers.Claude.Adapter.Exec,
exec: {MyApp.SandboxExec, sandbox: sandbox},
cli_path: "claude"
}
}
)The exec adapter requires auth: :inherit because :subscription pins the
local port adapter and verifies local CLI authentication. Its spawn spec is
remote-safe: cwd and cli_path resolve in the execution environment, and
the process environment is built only from the SDK variables, the session
env, and api_key — the orchestrator's environment is never forwarded.
Writing an Exec implementation walks through building an exec module, using E2B as the example.
Two limits to plan around. The adapter does not reconnect after the exec
reports exit; transport loss fails the turn and retires the session, matching
AgentHarness's provider-loss semantics. And SDK features that expect
filesystem access next to the CLI — session history and skills:/plugin
materialization — operate on the orchestrator's filesystem, so do not
configure skills: for a session whose CLI runs elsewhere; deliver
instructions through system_prompt instead.
Pi options
Pi is driven through pi --mode rpc. It is the smallest of the supported
harnesses: four core tools, no permission system, and no MCP. A session that
sets mcp_servers, approval_policy, or sandbox is rejected at
start_session/2 rather than opened with those settings quietly dropped.
{:ok, session} =
AgentHarness.start_session(:pi,
cwd: "/absolute/path/to/project",
model: "anthropic/claude-sonnet-5",
provider_options: %{
tools: ["read", "grep", "find", "ls"],
thinking: "medium",
agent_dir: "/absolute/path/to/isolated/pi-home"
}
)| Option | Meaning |
|---|---|
auth | :subscription (default) or :inherit |
api_key | Explicit key; only valid with auth: :inherit |
provider | Pi provider name when the model pattern does not carry one |
executable | Path or name of the pi binary |
exec | Remote execution (see below); requires auth: :inherit |
tools | Allowlist of tool names |
exclude_tools | Denylist of tool names |
no_tools | Disable all tools |
thinking | off, minimal, low, medium, high, xhigh, or max |
extensions | Extension file paths to load |
resume | Session file path or partial session ID to continue |
fork | Session file path or partial session ID to branch from |
session | false for an ephemeral run that is never written to disk |
session_dir | Directory for session storage and lookup |
agent_dir | Sets PI_CODING_AGENT_DIR, isolating config and sessions |
name | Session display name |
offline | Skip pi's startup network calls |
By default the harness assigns its own session id with --session-id, so
provider_session_id matches the AgentHarness session id. Setting resume,
fork, or session: false hands that choice back to pi, and the id is read
from pi instead.
Run the pi CLI in another execution environment
Setting exec: runs pi --mode rpc through an AgentHarness.Exec
implementation instead of a local port. Pi's JSONL protocol stays in your
application's node; only the CLI process moves.
{:ok, session} =
AgentHarness.start_session(:pi,
cwd: "/workspace",
model: "openai/gpt-4.1-nano",
provider_options: %{
auth: :inherit,
exec: {MyApp.SandboxExec, sandbox: sandbox},
executable: "pi"
}
)Remote execution requires auth: :inherit because :subscription verifies
local pi /login state that says nothing about the environment the CLI would
run in. executable and cwd resolve in the execution environment, only the
session env (and agent_dir) is forwarded, and stderr stays out of the
data stream. skills: and extensions: paths must exist where the CLI runs.
See Writing an Exec implementation for
building the exec module itself.
Questions from Pi
Pi has no built-in question tool. Dialogs reach the harness through pi's
extension UI sub-protocol, so they appear only when a loaded extension calls
ctx.ui.confirm, ctx.ui.select, ctx.ui.input, or ctx.ui.editor. Each one
arrives as a %Request{kind: :question}:
confirmcarriestrue/falsechoices. Answer withResponse.approve/0,Response.deny/0, orResponse.answer(boolean).selectcarries the extension's options as choices.inputandeditorare free-form; answer withResponse.answer(text).
Response.cancel/0 dismisses any dialog. A dialog raised while no turn is
active is dismissed automatically, because an unanswered dialog blocks pi
indefinitely.
Steering
Pi can accept messages while a turn is running, through its steer and
follow_up commands. AgentHarness allows one active turn per session and has
no steering entry point, so the adapter reports steer: :unsupported and
start_turn/3 during a running turn returns {:error, {:turn_in_progress, turn}}.
The protocol support is in place for when a public API lands.
Provider differences
| Concern | Codex | Claude | Pi |
|---|---|---|---|
| Transport | Codex app-server via codex_sdk | Bidirectional Claude CLI via claude_code | pi --mode rpc JSONL over a port |
| Turn input | String or structured input list | String | String, or a list of strings joined by newline |
| Token deltas | Native | Native | Native |
| Questions | Native app-server request | Native can_use_tool callback | Extension UI dialogs, only when an extension asks |
| Approvals | Command, file, permissions, MCP elicitation | Tool permission callback | Unsupported; pi has no permission system |
| Session-scoped approval | Advertised for file/permission protocols; command requests follow availableDecisions | Explicitly unsupported; returns an error | Not applicable |
| Per-session MCP | Native | Native | Unsupported; a configured server is rejected |
| Skills | Explicit native skill input | Session plugin generated when needed | Native --skill per skill path |
| Cancellation | Turn interrupt, then drain the authoritative terminal event | CLI interrupt | abort, then the aborted stop reason |
| Resume | Codex thread ID | Claude session ID | Session file path or partial session ID |
| Fork | Unsupported in the current adapter | Native via resume plus fork_session | Native via fork |
| Approval/sandbox config | Common session fields | Common fields with Claude-native values | Rejected; pi does not sandbox |
| Steering | Capability currently unsupported | Capability currently unsupported | Supported by pi, not yet exposed by the harness |
| Terminal signal | Codex terminal turn event | Claude ResultMessage | Non-retrying agent_end (agent_settled on older CLIs) |
| Remote execution | exec: provider option | adapter: with the Exec adapter | exec: provider option |
Provider-specific event data remains available in Event.raw. Write your
orchestrator against normalized lifecycle and request events, then inspect
raw only where a provider-specific feature is intentional.