Yoke

July 11, 2026 · View on GitHub

yoke (yōk), n. A harness that joins two, that they may pull as one. [akin to Skr. yoga, union.]

Python 3.11+ Typed Claude harness Codex harness License: Apache-2.0

A Python SDK for building agents on Claude Code and Codex.

Claude Code and Codex have become general-purpose agents: give them instructions, skills, and subagents, and they can be shaped to any task. Yoke lets you reuse them from code — one Harness that drives both.

Quickstart · How it compares · Sessions · Skills · Subagents · Workflows · Goals · Surfaces · CLI

Quickstart

pip install almanac-yoke

Install a provider extra when you want Yoke to manage that SDK directly:

pip install 'almanac-yoke[claude]'  # or [codex], or [all]

Define an agent, pick a harness, run:

from pathlib import Path

from yoke import Agent, Goal, Harness

agent = Agent(
    instructions="You are a careful maintainer. Make small, safe changes.",
    goal=Goal("Finish the requested implementation safely."),
)
harness = Harness("codex", agent=agent, cwd=Path.cwd())

result = await harness.run("Implement the bundle loader.")
print(result.output)

Swap "codex" for "claude" and the same agent runs there. Your existing Claude Code or ChatGPT login is all it needs — no API keys.

Embedding applications can observe a one-shot run while it is happening:

from yoke import RunOptions

seen = []
result = await harness.run(
    "Implement the bundle loader.",
    RunOptions(on_event=seen.append),
)

assert tuple(seen) == result.events

on_event is a synchronous callback receiving each normalized Event once. It is runtime-only—it is excluded from serialized options and agent folders. Live callback delivery is supported by the Claude Python SDK and Codex app-server surfaces. Passing on_event selects one of those surfaces when the surface is automatic, and raises UnsupportedFeature when an explicitly selected surface cannot deliver callbacks. Use harness.stream(...) for a portable event iterator.

How it compares

The question that places Yoke: who runs the agent?

The agent runs inYoke
Claude Agent SDK · Codex SDKthe lab's harness — one provider eachbuilds on them. They are the surfaces Yoke drives; one definition runs on both.
Pydantic AI · LangChainyour process — a loop you assemble over model APIsstarts from a different premise: the harness already is the agent, so there is no loop to assemble.
EveEve's own durable runtime, deployed on Vercelis closest in spirit — an agent is a directory — but compiles that directory onto Claude and Codex instead of shipping a runtime.

Sessions

session = await harness.start()

plan = await session.run("Draft the migration plan.")
step = await session.run("Apply step one.")

await session.close()

State lives with the provider; Yoke holds the handle. Sessions are native on every surface except the Codex CLI, where they work by resuming threads.

Skills

from yoke import Skill

agent = Agent(
    instructions="You are a careful maintainer.",
    skills=(Skill(path=Path("skills/source-grounding")),),
)

A skill is a folder with a SKILL.md. Claude loads skills natively, the Codex app-server mounts them as native skill roots, and other surfaces get them compiled to files.

Subagents

agent = Agent(
    instructions="You are a careful maintainer.",
    subagents={
        "reviewer": Agent(
            description="Find correctness and architecture risks.",
            instructions="Review concretely. Prefer file and line evidence.",
        ),
    },
)

A subagent is just another Agent. Claude runs declared subagents through its native Agent tool. Codex app-server derives temporary custom-agent TOML and the parent selects it with spawn_agent(agent_type=..., fork_turns="none"). Yoke does not silently use a generic child: an incompatible Codex model/backend fails honestly. Codex SDK/CLI surfaces retain their documented lowerings.

Runtime files are derived outside cwd and removed when the session closes. Set Harness(runtime_root=...) to choose their parent cache directory. This is different from agent.bundle(...).write(...), which explicitly exports durable provider files for a project. A later deployment reclaims runtime directories whose owning process exited before normal cleanup.

Workflows

from yoke import Step, Workflow, WorkflowOptions

workflow = Workflow(
    name="review",
    steps=(
        Step(name="draft", agent="main", prompt="Draft release notes."),
        Step(
            name="review",
            agent="reviewer",
            depends_on=("draft",),
            prompt="Review this draft: {draft}",
        ),
    ),
)

result = await harness.workflow(
    workflow,
    options=WorkflowOptions(timeout_seconds=120, step_timeout_seconds=60),
)

A workflow is a small dependency graph over the agent and its subagents; main is the reserved name for the root agent, and WorkflowOptions bounds the run. Neither provider has a native equivalent yet, so workflows are portable Yoke constructs — they run the same on every surface.

Goals

from yoke import Goal

session = await session.set_goal(Goal("Land the bundle loader.", token_budget=200_000))
print(await session.get_goal())

A goal is intent that outlives a single prompt. On the Codex app-server it is real thread state — readable, replaceable, clearable. Everywhere else it compiles into the provider loop, and explain() tells you which you got.

No pretending

Claude and Codex expose different primitives, and Yoke does not flatten them into a weak common denominator. The capability map is part of the API:

for row in harness.explain().reports:
    print(row.feature, row.support, row.lowering)  # native, compiled, emulated, unsupported
FeatureClaude SDKCodex app-serverCodex SDKCodex CLI
Sessionsnativenativenativeresume-based
Streamingnativenativenative transportJSONL/process
Skillsnativenative skill rootscompiledfiles/compiled
Subagentsnativecompiled → native toolcompiledfiles/compiled
Goalsprovider loopnative statecompiled contextprovider loop
Workflowsportable Yokeportable Yokeportable Yokeportable/limited
Permissions/hooksnative callbacksnative request eventssandbox/approvalflags/config

Agents are folders

Everything defined in Python can be saved as files, edited by hand, and loaded back:

agent.save("agent")
agent = Agent.from_folder("agent")
agent/
  agent.yaml
  instructions.md
  skills/
    source-grounding/SKILL.md
  subagents/
    reviewer/
  workflows/
    ship/

Provider files like .claude/ and .codex/ are compiled from this source, only when you ask:

agent.bundle(provider="codex", surface="codex_cli").write(Path.cwd())

Surfaces

This is the deeper layer:

agent definition -> provider surface -> real harness

Each provider ships more than one way in. Harness("codex") picks the strongest one for you (the app-server); address a surface directly when you require an exact one:

SurfaceWhat it is
codex:appCodex app-server — sessions, native goals, skill roots
codex:sdkCodex Python SDK
codex:cliCodex CLI — codex exec, resumable threads
claude:sdkClaude Agent SDK for Python

discover reports what this machine already has — surfaces installed, logins ready, models available — and picks the first ready surface satisfying the requested features:

from yoke import Feature, discover

found = await discover("codex", Path.cwd(), agent)  # reuses your local login

for surface in found.surfaces:
    print(surface.surface, [model.id for model in surface.models])

harness = found.harness(Feature.STREAMING)

Claude also accepts runtime-only Credentials (redacted, never serialized); Codex logins persist provider state, so they go through an explicit await harness.login(...).

CLI

The same agents and folders, from the shell:

yoke run agents codealmanac "Review this repo"
yoke explain agents codealmanac
yoke status agents codealmanac
yoke install agents codealmanac --provider codex:cli
yoke runs

CLI runs leave inspectable snapshots under .yoke/runs/. SDK users can persist returned results explicitly with RunStore.at(".yoke/runs").record(result).

Status

Yoke is an early alpha. Everything shown above is built and smoke-tested against live providers. The API may still change before 1.0; durable workflow execution and typed coverage of every provider-specific option remain future work.

Apache-2.0. See LICENSE.md.