Skip to main content

Deterministic LangGraph orchestration with optional Codex execution

Project description

langgraph-codex

Put Codex inside the LangGraph you already own.

langgraph-codex is a small adapter library for one practical pattern: keep your deterministic graph, then replace one bounded node with a Codex-backed node when ordinary Python is not enough.

  • LangGraph owns orchestration, state, routing, persistence, and checkpoints.
  • Python owns parsing, context building, policy, and validation.
  • Codex owns one explicit execution step with a clear prompt and workspace.
  • Your application stays in control.

It is not a chat framework, hidden agent runtime, repository automation product, or broad model abstraction layer.

This README describes langgraph-codex v0.1.4.

Install

uv add langgraph-codex

For local development:

uv sync --extra dev
make check

Start Here: Codex As One LangGraph Node

This is the main use case. Build a normal langgraph.graph.StateGraph, authorize Codex once, and use create_codex_node only for the node that needs agentic execution.

import pathlib
import typing

import langgraph.graph

from langgraph_codex.execution import ExecutionResult
from langgraph_codex.graph import create_codex_node
from langgraph_codex.runtime import create_codex_executor, ensure_codex_authorized


class ReviewState(typing.TypedDict, total=False):
    workspace_path: typing.Required[pathlib.Path]
    repo_context: dict[str, list[str]]
    codex_result: ExecutionResult
    validation_message: str


def inspect_codebase(_state: ReviewState) -> dict[str, dict[str, list[str]]]:
    return {
        "repo_context": {
            "package_files": ["langgraph_codex/graph/nodes.py"],
            "test_files": ["tests/test_graphs.py", "tests/test_execution.py"],
            "example_files": ["examples/00_existing_langgraph_graph.py"],
        }
    }


def prompt_for_codex(state: ReviewState) -> str:
    return "\n".join(
        [
            "Audit the existing langgraph-codex repository.",
            f"Repository context: {state.get('repo_context', {})}",
            "Write codebase_audit.md with concrete findings about tests and examples.",
            "Do not invent an unrelated service configuration.",
        ]
    )


def validate_result(state: ReviewState) -> dict[str, str]:
    result = state.get("codex_result")
    if result is None:
        return {"validation_message": "Codex did not return a result."}
    if result.returncode != 0:
        return {"validation_message": f"Codex failed: {result.stderr}"}

    return {"validation_message": "Codex completed. Validate codebase_audit.md next."}


ensure_codex_authorized()
codex_node = create_codex_node(
    executor=create_codex_executor(timeout_seconds=300),
    prompt_builder=prompt_for_codex,
    workspace_path=lambda state: state["workspace_path"],
)


def draft_audit(state: ReviewState) -> dict[str, ExecutionResult]:
    update = codex_node(state)
    result = update.get("codex_result")
    if not isinstance(result, ExecutionResult):
        raise TypeError("Codex node did not return codex_result.")

    return {"codex_result": result}

graph = langgraph.graph.StateGraph(ReviewState)
graph.add_node("inspect_codebase", inspect_codebase)
graph.add_node("draft_audit", draft_audit)
graph.add_node("validate_result", validate_result)
graph.add_edge(langgraph.graph.START, "inspect_codebase")
graph.add_edge("inspect_codebase", "draft_audit")
graph.add_edge("draft_audit", "validate_result")
graph.add_edge("validate_result", langgraph.graph.END)

result = graph.compile().invoke({"workspace_path": pathlib.Path.cwd()})
print(result["validation_message"])

That is the intended shape:

prepare deterministic context -> call Codex node -> validate deterministically -> route

Authorization

Real Codex execution requires the Codex CLI and credentials. The runtime helpers load local .env values and map OPEN_AI_SECRET_KEY to OPENAI_API_KEY when needed.

cp .env.example .env

Then set the relevant values:

OPEN_AI_SECRET_KEY=...
OPEN_AI_KEY_NAME=github-actions
OPEN_AI_MODEL=

See docs/codex-authorization.md for local setup, GitHub Actions secrets, and CI guidance.

Typed Codex Options

The Codex-facing defaults are enum-backed and exported for application code. Plain strings still work when you need a newer CLI value, but stable options no longer need naked literals:

import pathlib

from langgraph_codex import CodexApprovalPolicy, CodexSandbox, ExecutionOption
from langgraph_codex.execution import CodexExecutor, ExecutionRequest

executor = CodexExecutor(
    sandbox=CodexSandbox.WORKSPACE_WRITE,
    approval_policy=CodexApprovalPolicy.NEVER,
    profile="ci",
    config_overrides={"reasoning.effort": "low"},
)

request = ExecutionRequest(
    workspace_path=pathlib.Path.cwd(),
    prompt="Run the bounded migration and report changed files.",
    options={
        ExecutionOption.MODEL.value: "gpt-5.1",
        ExecutionOption.TIMEOUT_SECONDS.value: 300,
        ExecutionOption.EXTRA_ARGS.value: ["--json"],
    },
)

Useful enums include:

  • CodexSandbox: READ_ONLY, WORKSPACE_WRITE, DANGER_FULL_ACCESS
  • CodexApprovalPolicy: UNTRUSTED, ON_FAILURE, ON_REQUEST, NEVER
  • ExecutionOption: per-request model, timeout, sandbox, approval policy, profile, extra args, config overrides, writable roots, git-repo-check behavior, JSON events, output schema path, and output-last-message path

Dangerous bypass flags and equivalent config overrides are rejected before subprocess execution.

Structured AI Outputs

Codex can produce machine-readable output for downstream graph nodes. Use OUTPUT_SCHEMA_PATH to pass a JSON Schema file to the CLI, OUTPUT_LAST_MESSAGE_PATH to persist the final response, and JSON_EVENTS to parse JSONL stdout into structured graph state:

import pathlib

from langgraph_codex import ExecutionOption
from langgraph_codex.execution import ExecutionRequest
from langgraph_codex.utils.validation import require_structured_output

request = ExecutionRequest(
    workspace_path=pathlib.Path.cwd(),
    prompt="Return a JSON object with status and changed_files.",
    options={
        ExecutionOption.OUTPUT_SCHEMA_PATH.value: "schemas/remediation-result.schema.json",
        ExecutionOption.OUTPUT_LAST_MESSAGE_PATH.value: "codex-result.json",
        ExecutionOption.JSON_EVENTS.value: True,
    },
)

validators = [require_structured_output("last_message_json")]

When the last message contains valid JSON, ExecutionResult.structured_outputs includes:

  • last_message_path
  • last_message
  • last_message_json
  • json_events

This keeps AI output useful for routing, validation, and persistence without scraping free-form stdout.

Prompt Rendering

You can still pass normal strings to create_codex_node, but structured prompts are now first-class. A prompt_builder may return PromptSpec; the node renders it before calling the executor.

from langgraph_codex import PromptFile, PromptSection, PromptSpec


def prompt_for_codex(state: ReviewState) -> PromptSpec:
    return PromptSpec(
        title="Patch billing export",
        objective="Fix the missing purchase order references.",
        context_sections=[
            PromptSection("Ticket", state["ticket"]),
            PromptSection("Repository notes", "\n".join(state["repo_context"]["files"])),
        ],
        files=[PromptFile("services/billing/export.py", "Exporter under review.")],
        acceptance_criteria=[
            "Existing billing tests pass.",
            "The export includes purchase order references.",
        ],
    )

For custom prompt layouts, use the Markdown renderer options:

from langgraph_codex import MarkdownPromptRenderOptions, PromptBlock
from langgraph_codex.utils.prompts import render_prompt

prompt = render_prompt(
    prompt_for_codex(state),
    options=MarkdownPromptRenderOptions(
        section_order=(
            PromptBlock.OBJECTIVE,
            PromptBlock.CONTEXT,
            PromptBlock.FILES,
            PromptBlock.ACCEPTANCE_CRITERIA,
        ),
        bullet="*",
    ),
)

The default renderer preserves the stable Markdown order used by earlier releases.

For common Codex tasks, use typed prompt recipes that return ordinary PromptSpec objects:

from langgraph_codex.prompts import PromptSpec, create_implementation_prompt


def prompt_for_codex(state: ReviewState) -> PromptSpec:
    return create_implementation_prompt(
        objective="Patch the billing export workflow.",
        requirements=[
            "Include purchase order references in every exported row.",
            "Keep the existing CSV schema order stable.",
        ],
        validation_commands=["uv run pytest tests/test_billing_export.py"],
        files=[("services/billing/export.py", "CSV export implementation.")],
    )

Recipe helpers are available for code review, implementation, test generation, documentation updates, and migration planning. They only assemble structured prompt content; rendering and graph execution continue to use the normal PromptSpec path.

Validation

Codex output should be checked by deterministic code before anything downstream consumes it.

Good validators check files, schemas, command output, tests, checksums, and domain-specific facts:

from langgraph_codex.utils.validation import require_files

validators = [require_files(["remediation_plan.md"])]

You can use the built-in validation helpers, or write normal LangGraph nodes that inspect your application state and route from there.

Examples

The examples are intentionally few and close to the production integration shape:

Run:

uv run python3 -m examples.00_existing_langgraph_graph
uv run python3 -m examples.01_real_codex_node
uv run python3 -m examples.02_codebase_audit

Convenience Builders

For small tests and quick starts, the package also includes complete graph builders:

  • build_context_only_graph()
  • build_execution_graph()
  • build_retry_graph()

Most production applications should prefer create_codex_node inside their own graph.

build_retry_graph() can accept a retry_strategy callback. The callback receives the current graph state after deterministic review has failed, including validation_result and the next retry_count, and returns normal state updates. Those updates are applied before the prompt is rendered again, so applications can add validation feedback to additional_instructions, tighten context, or attach retry-specific metadata:

def retry_strategy(state):
    validation = state["validation_result"]
    return {
        "additional_instructions": [
            f"Previous validation failed: {validation.message}",
            "Address the deterministic validation failure before continuing.",
        ]
    }

graph = build_retry_graph(
    executor=executor,
    validators=validators,
    retry_strategy=retry_strategy,
)

Builder internals use exported graph constants instead of loose strings:

from langgraph_codex.graph import GraphNode, ReviewRoute

assert GraphNode.REVIEW.value == "review"
assert ReviewRoute.RETRY.value == "retry"

Those constants are useful when composing custom graphs, asserting route behavior, or instrumenting node-level metrics.

Advanced Node Usage

create_codex_node supports static and dynamic metadata/options, custom result keys, custom result mapping, state-derived workspaces, and structured prompt builders:

codex_node = create_codex_node(
    executor=executor,
    prompt_builder=prompt_for_codex,
    workspace_path=lambda state: state["workspace_path"],
    metadata={"workflow": "billing-remediation"},
    metadata_builder=lambda state: {"ticket_id": state["ticket_id"]},
    options={ExecutionOption.TIMEOUT_SECONDS.value: 300},
    options_builder=lambda state: {ExecutionOption.MODEL.value: state["model"]},
    result_key="remediation_result",
)

This lets a deterministic graph choose the Codex model, timeout, profile, writable roots, and validation context per task without rebuilding the executor.

CI/CD

The repository validates:

  • GitHub Actions workflow syntax with actionlint;
  • Ruff formatting and linting;
  • Pylint with 10.00/10;
  • strict mypy and Pyright;
  • Python compile checks;
  • pytest across Python 3.11, 3.12, and 3.13;
  • offline examples;
  • wheel and source distribution build plus Twine metadata validation;
  • optional real Codex smoke checks through workflow dispatch.

Release publishing uses PyPI trusted publishing through the pypi GitHub environment.

Design Notes

The package deliberately stays small. It does not own memory, UI, checkpoint storage, broad model selection, or repository policy. Persistent memory and checkpointing should be configured in the LangGraph application that owns the graph; this package only contributes bounded nodes, builders, and deterministic helpers. Those broader concerns belong in the graph and infrastructure you already control.

Read more in docs/design-philosophy.md. Planned follow-up work is tracked in docs/follow-up-work.md.

Testing Without Codex

Use FakeExecutor when you want CI-safe tests, examples, or local development without calling the Codex CLI.

from langgraph_codex.execution import FakeExecutor

executor = FakeExecutor(stdout="Priority: medium. Area: billing exports.")

FakeExecutor records requests and returns deterministic results, so you can assert prompt content, metadata, options, and graph routing without network credentials.

Development

make sync
make format
make check

Useful targets include make quality, make test, make package-check, make examples, make examples-codex, and make clean.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

langgraph_codex-0.1.4.tar.gz (153.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langgraph_codex-0.1.4-py3-none-any.whl (36.4 kB view details)

Uploaded Python 3

File details

Details for the file langgraph_codex-0.1.4.tar.gz.

File metadata

  • Download URL: langgraph_codex-0.1.4.tar.gz
  • Upload date:
  • Size: 153.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for langgraph_codex-0.1.4.tar.gz
Algorithm Hash digest
SHA256 850f32f578823632f05b09a3284cc973a777012efab4024300f9635fd115254d
MD5 5097bae13b2b480717a11d8a91ca0153
BLAKE2b-256 9a47a3439d933afcc0da60b33f92d61bd73862547c33b66782f9c196169f085f

See more details on using hashes here.

File details

Details for the file langgraph_codex-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: langgraph_codex-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 36.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.16 {"installer":{"name":"uv","version":"0.9.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for langgraph_codex-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 a5e3f2e1b504bbebad3e68d1942ff7a1e611b20f7b5f8992c610d74ab6381df9
MD5 7e69ee4066b685a98d1d2f8fafadcace
BLAKE2b-256 a744b33df08264478892eb960eb8aad2faae9b0a76b1f6ce5af2e6614a6e9ee8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page