Coding agents in practice¶
Claude Code and Codex are engines inside a Python Agent. Keep the same tool and state references when changing engines; pair different runtimes for writing and review. Provider APIs and deterministic Plans compose through that same contract.
Swap the engine, keep the state¶
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine, LLMEngine, Memory, Session, Store
def project_status() -> str:
"""Return a deterministic project status."""
return "The draft is ready for review."
state = dict(tools=[project_status], memory=Memory(), store=Store(), session=Session())
claude = Agent(name="claude", engine=ClaudeCodeEngine(), **state)
codex = Agent(name="codex", engine=CodexEngine(), **state)
api = Agent(name="api", engine=LLMEngine("gpt-6.1-sol"), **state)
assert claude.memory is codex.memory is api.memory
assert claude.store is codex.store is api.store
assert claude.session is codex.session is api.session
Changing engines shares application state. A Claude native transcript cannot be migrated into a Codex thread. Native tools, permissions, structured schemas and attachments differ; engine interchangeability describes the Agent contract.
Choose the runtime¶
| Capability | Claude Code | Codex |
|---|---|---|
| Transport | Claude Agent SDK | codex app-server JSON-RPC |
| Python tools | Temporary in-process MCP adapter | App Server dynamic tools |
| Default native access | WebSearch/WebFetch; rooted Read/Glob/Grep | Native read-only sandbox; shell available within runtime policy |
| Writes | Explicit Write/Edit extras plus policy | workspace-write plus policy |
| Shell confinement | File roots do not confine Bash; gate required | Runtime sandbox and approvals |
| Structured output | SDK output_format for derivable schemas | Native outputSchema for representable strict object schemas; fallback otherwise |
| Reasoning | reasoning_effort plus thinking | reasoning_effort, passed through |
| Durable history | persist_session / session_id | persist_thread / thread_id |
| Named continuity | session_alias / SessionRegistry | session_alias / SessionRegistry |
| Usage telemetry | /usage prose parsing | account/rateLimits/read RPC |
| Dollar cost | SDK-reported cost | No reported price; adapter records 0.0 |
With subscription login the runtimes use plan quota. The adapters reuse the configured runtime authentication and do not prohibit API billing. Use supported runtime aliases/IDs: Claude defaults to sonnet; Codex defaults to its configured model. Model availability and effort support remain runtime concerns; no invented aliases or guaranteed low-cost ranking is implied.
Models and reasoning choices¶
Claude's sonnet/opus/haiku aliases resolve through the installed runtime and account. Full supported IDs can pin a model. See the model identifier table and current official docs rather than assuming a permanent family version or price order. fallback_model is a native fallback for Claude; outer Agent fallback is a different mechanism.
Codex model=None keeps the runtime-configured model; an explicit ID passes to thread/start without a model/list availability check. Invalid IDs/efforts can therefore fail at runtime. Native IDs and aliases differ from LLMEngine's provider catalogue and LazyTools' quota-routing tiers.
| Knob | Engine | Meaning |
|---|---|---|
| reasoning_effort | Both | Model effort; Claude validates its allowed strings, Codex passes through |
| thinking | Claude | adaptive, disabled, or a positive token budget |
| fallback_model | Claude | Native SDK model fallback |
from lazybridge import ClaudeCodeEngine, CodexEngine
claude_default = ClaudeCodeEngine(model="sonnet")
claude_reasoned = ClaudeCodeEngine(model="opus", reasoning_effort="high", thinking="adaptive")
claude_budgeted = ClaudeCodeEngine(thinking=8_000)
codex_default = CodexEngine()
codex_reasoned = CodexEngine(reasoning_effort="medium")
Supported effort modes depend on the model/runtime. An integer thinking budget and an effort label are not interchangeable units. There is no Codex thinking argument.
Privileges: a four-tier policy¶
Use allow for known permitted actions, session for a scoped remembered grant, ask for individual approval, and deny as the default. The core TieredGate extension implements this policy and audit trail. The permission guide shows complete config and callback examples. Claude writer profiles still need extra_tools; supplying a gate does not itself expose Write/Edit/Bash.
| Tier | Behavior |
|---|---|
| allow | Run a known permitted action without asking |
| session | Ask once, then remember a grant within the gate's scope |
| ask | Ask for each action |
| deny | Refuse, including unmatched actions |
This complete policy uses the Core extension instead of an undefined human callback. It configures a terminal approval channel but does not invoke writes:
from pathlib import Path
from lazybridge import Agent, ClaudeCodeEngine, ClaudeCodePolicy, CodingAgentConfig
from lazybridge.ext.approval import Rule, TerminalChannel, TieredGate
repo = str(Path.cwd().resolve())
gate = TieredGate(channel=TerminalChannel(), rules=(
Rule("allow", "Read"), Rule("allow", "Glob"), Rule("allow", "Grep"),
Rule("allow", "WebSearch"), Rule("allow", "WebFetch"),
Rule("session", "Write"), Rule("session", "Edit"),
Rule("ask", "Bash"),
))
config = CodingAgentConfig(
claude=ClaudeCodePolicy(preapprove_application_tools=False,
extra_tools=("Write", "Edit", "Bash")),
approval_gate=gate,
)
writer = Agent(name="writer", engine=ClaudeCodeEngine(
cwd=repo, file_roots=[repo], config=config,
))
Rules are ordered and unmatched actions deny. The broad Write/Edit session rules above are a policy choice, not a file-specific grant; narrow rules and argument scope for sensitive workflows. TieredGate owns provider/agent/cwd/ policy scope and structured audit records. A generic allow_for_session grant otherwise lives on the LazyBridge Session, with action/command scope described in the permission guide. A native transcript alone does not persist that grant.
The confinement asymmetry¶
Claude file_roots constrain native file hooks, including under permissive permission settings. Bash can touch what its process can touch; it needs a separate gate. The constructor detects a missing gate:
from pathlib import Path
from lazybridge import ClaudeCodeEngine, ClaudeCodePolicy, CodingAgentConfig
repo = str(Path.cwd().resolve())
try:
ClaudeCodeEngine(file_roots=[repo], config=CodingAgentConfig(
claude=ClaudeCodePolicy(extra_tools=("Bash",)),
))
except ValueError as exc:
print("Gate required:", exc)
else:
raise AssertionError("Expected unconfined-tool validation")
Application tools run in Python, outside the native filesystem sandbox. Command policies must consider every compound segment, rather than approve a whole command just because it starts with a permitted prefix.
Durable conversations¶
from pathlib import Path
from tempfile import TemporaryDirectory
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine, SessionRegistry
repo = str(Path.cwd().resolve())
with TemporaryDirectory() as directory:
registry = SessionRegistry(path=Path(directory) / "sessions.json")
claude = Agent(name="writer", engine=ClaudeCodeEngine(
cwd=repo, session_alias="draft", session_registry=registry, tag=None,
))
codex = Agent(name="reviewer", engine=CodexEngine(
cwd=repo, session_alias="draft", session_registry=registry,
))
claude("Remember the draft topic: weekend deployment.")
codex("Remember the review topic: weekend deployment.")
assert registry.resolve("claude", repo, "draft") is not None
assert registry.resolve("codex", repo, "draft") is not None
These are two native conversations because engine kind is part of the key. Use a persistent registry file in real work. Resumes omit repeated Memory priming while preserving explicit context. Runs/streams serialize in-process; separate processes require caller coordination. Explicit native IDs override alias lookup. SessionRegistry explains precedence, corruption handling and registry/transcript locking.
Keeping raw native handles¶
Aliases are optional. A persisted engine exposes its handle for an application to store and resume later:
from pathlib import Path
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine
repo = str(Path.cwd().resolve())
claude_engine = ClaudeCodeEngine(cwd=repo, persist_session=True, tag=None)
Agent(name="claude-reviewer", engine=claude_engine)("Remember the review topic.")
claude_handle = claude_engine.session_id
assert claude_handle is not None
Agent(name="claude-followup", engine=ClaudeCodeEngine(
cwd=repo, session_id=claude_handle, tag=None,
))("Explain the remembered topic.")
codex_engine = CodexEngine(cwd=repo, persist_thread=True)
Agent(name="codex-reviewer", engine=codex_engine)("Remember the review topic.")
codex_handle = codex_engine.thread_id
assert codex_handle is not None
Agent(name="codex-followup", engine=CodexEngine(
cwd=repo, thread_id=codex_handle,
))("Explain the remembered topic.")
To resume in another process, persist the appropriate ID with its engine kind and cwd or use SessionRegistry. Claude runtime mode is a Session-local channel, not a substitute for a durable registry.
Finding and retaining native history¶
Claude labels newly created durable sessions with tag; Codex uses thread_source and the initialize originator. These labels identify history rather than grant access. Follow the engine guides' read-only listing and rollout inspection steps. Review active use before cleanup; there is no documented Core delete-all operation, and deleting an alias does not remove native history.
Tools and practical recipes¶
Mix deterministic Python tools with named specialist agents:
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine, CodingAgentConfig
def project_context() -> str:
"""Return the deterministic design constraints for this example."""
return "Deploy on weekdays; require input validation and a passing test suite."
reviewer = Agent(name="reviewer", engine=CodexEngine(
config=CodingAgentConfig.reviewer(),
system="Report defects with evidence. Never patch files.",
))
consultant = Agent(name="consultant", engine=ClaudeCodeEngine(web=True),
tools=[project_context, reviewer])
print(consultant("Propose a deployment design using project_context.").text())
The consultant can invoke its specialist as a Tool. A fail-closed reviewer profile keeps native access read-only and refuses unapproved application tools. Python tool side effects still need application policy.
For two independent answers on the same question, use a parallel panel:
from lazybridge import Agent, ClaudeCodeEngine, CodexEngine, CodingAgentConfig
claude_reviewer = Agent(name="claude-review", engine=ClaudeCodeEngine(
config=CodingAgentConfig.reviewer(), system="Review the policy's edge cases.",
))
codex_reviewer = Agent(name="codex-review", engine=CodexEngine(
config=CodingAgentConfig.reviewer(), system="Review the policy's edge cases.",
))
panel = Agent.parallel(claude_reviewer, codex_reviewer, name="panel")
result = panel("Review a rule allowing deployments only on weekdays.")
assert "[claude-review]" in result.text() and "[codex-review]" in result.text()
This is a labelled join of separate answers, not a consensus or acceptance record. Use run_branches when you need individual Envelopes. A gated writer's complete policy is above; native repository review targets are in the Codex guide.
Composition and collaboration¶
Both engines work as child agents in tools, Agent.chain, Agent.parallel and Plan steps. A producer can have an opposite-engine verify judge, or a pipeline can wrap only its critical step with verify. Start with Claude Code writes, Codex reviews for runnable in-process review and credential-free feedback/exhaustion tests.
verify is a bounded repair loop, not release acceptance. Deterministic tests, schema checks and explicit acceptance records must gate shipping.
Integration through LazyTools¶
LazyTools owns connectors, code-support tools, the MCP server, repository code bridge, projects and quota routing. Its distribution is lazytoolkit and import namespace lazytools. Do not import removed lazybridge.ext.mcp/gateway or external_tools modules.
- Code support: synchronous codex/claude ask, write, review and named-session tools; the caller blocks for the delegated turn.
- MCP server: opt in to coding providers and configure per-call model/effort/session overrides there.
- Code bridge: detached repository jobs, durable tickets, result/evidence and tier-based --review-of routing that excludes the producer engine.
- Projects: Store-backed plans, claims and verification records.
- Quota routing: deterministic eligibility ladder; benchmark in progress.
Core approval/delegation hooks remain available for applications building their own orchestration. Since 1.1.0, native output/attachments/usage, policy controls, durable aliases/locking and admission/job lifecycle hooks have expanded the engine workflow. Changelog tracks the release sequence.
| Release | Coding-engine and collaboration changes |
|---|---|
| 1.1.0 | Claude Code and Codex engines behind the Agent contract. |
| 1.2.0 | Claude extra_tools, durable-session tagging and Codex thread_source labels. |
| 1.2.1 | Native Codex structured schemas, per-agent compaction, Claude usage parsing and shared bounded tool execution. |
| 1.3.0 | Codex web_search overrides and TieredGate. |
| 1.4.0 | Claude native tool events; structured step outputs delivered correctly as context. |
| 1.5.0 | Generic delegation/knowledge/approval queues, Codex usage RPC and portable approval_gate on LLMEngine. |
| 1.6.0–1.6.2 | Durable aliases/SessionRegistry, writable roots and command-scope policy; stream/alias locks and transport diagnostics. |
| 1.7.0 | Durable late approval consumption, wait_budget/expiry and admission/job provenance hooks. |
| 1.8.0 and subsequent main | Writer alias/factory/lifecycle overrides, JobRunner/JobContext, reservation release/refund and exact structured delegation outcomes; improved Codex executable discovery and pre-turn transport retry. |
Timeouts and failures¶
Default engine request_timeout is 120 seconds for the whole run, retries included. Set a larger deadline for repository tasks and align an MCP host's separate tool timeout. Missing/unreadable CLI is permanent. Codex distinguishes pre-turn transport failure from CodexTurnUncertain after a durable turn was sent; inspect native history before replaying possible side effects. Neither engine provides cross-process transcript locking or automatic recovery of an uncertain repository turn.
Troubleshooting¶
| Symptom | Likely boundary | Next check |
|---|---|---|
| Claude says it has no writing tool | Built-in exposure | extra_tools, not only writer(gate) |
| Application tools all deny | Gate/preapproval | Select an explicit policy and inspect normalized requests |
| Bash rejected at construction | No unconfined-tool gate | Configure a deliberate approval rule |
| File access denied | Native roots/sandbox | Inspect resolved paths and engine policy |
| Codex CLI missing | Executable discovery | Set CODEX_BIN or follow the engine setup guide |
| Codex dollar cost is zero | RPC reports no price | Inspect tokens/quota; do not interpret zero as free |
| A runtime MCP call is refused | Runtime-specific registration/approval | Python tools use tools/dynamic callbacks; external MCP follows LazyTools docs |
| Follow-up forgets history | Ephemeral call or wrong alias/cwd/kind | Inspect persistence and native ID before resuming |
| Review returns but is not approved | Bounded verify exhaustion | Run deterministic acceptance checks and inspect review evidence |
See Claude Code and Codex for full constructor tables, structured schema limits, streaming and multimodal behavior.