Skip to main content

agentshim

agentshim runs coding-agent CLIs (Claude Code, Codex, Gemini CLI, opencode, Copilot CLI) as subprocesses and turns their output into typed events and a typed turn result.

It owns everything that answers "how do I run provider X and understand what it printed". It does not own application policy: which provider to use, when to retire a conversation, how to sandbox the host, or how to render events.

No required runtime dependencies. Python 3.10+.

What it includes

  • CliAgent / AgentSession: one turn at a time, resumable, cancellable
  • typed events delivered on the calling thread
  • normalized token accounting (total input, cache reads, cache writes, uncached input, output and reasoning) on every provider, plus a static, versioned pricing table (see Usage and Pricing)
  • declared capabilities on ProviderProfile, so no caller probes a provider
  • injectable CommandExecutors for running the CLI in a container or over a remote shell
  • MCP server installation and restoration per turn
  • native structured output with per-provider schema dialect checks
  • agentshim.testing: doubles that emit each provider's real stream format

0.6 ships the core, the execution layer, and all five providers: Claude Code, Codex, Gemini CLI, opencode, and Copilot CLI. It is not source-compatible with 0.5; see CHANGELOG.md for what changed and how to migrate.

Install

uv add agentshim

agentshim does not bundle the agent CLIs. Install and authenticate the one you want (claude, codex, gemini, opencode, or copilot) yourself.

One turn

from agentshim import CliAgent

agent = CliAgent("claude", model="sonnet")
result = agent.run("Write a short summary of this codebase.", cwd=".")

print(result.text)
print(result.usage.tokens.input_tokens, result.cost_usd, result.duration_ms)

model is an opaque provider-specific string, passed through to the CLI unchanged; None leaves the CLI's own default.

Binary lookup and the CLI health check run once, in the constructor, so a broken install fails immediately rather than halfway through a turn.

A conversation

session = agent.start_session(cwd=".", timeout=600)

session.turn("What does this project do?")
second = session.turn("Which files should I read first?")

assert second.resumed
print(session.session_id)

adopt(session_id) continues a conversation you checkpointed earlier and returns False if the provider cannot resume or a turn is in flight; forget() starts fresh on the next turn, and likewise returns False while a turn is in flight. cancel() is thread-safe: it terminates the process group, then kills it after a grace period, and works even before the CLI has been spawned.

Per-turn options

from pathlib import Path
from agentshim import OutputSchema, StdioMcpServer, TurnRequest

schema = {
    "type": "object",
    "properties": {"summary": {"type": "string"}},
    "required": ["summary"],
    "additionalProperties": False,
}

result = session.turn(
    TurnRequest(
        prompt="Summarize the failing test.",
        cwd="/workspace",
        timeout=300,                       # None means no limit
        reasoning_effort="high",
        output_schema=OutputSchema(schema=schema, host_dir=Path("/tmp/schemas")),
        mcp_servers=[StdioMcpServer(name="issues", command="python", args=["-m", "board.mcp"])],
        env={"CI": "1"},
        extra_args=("--append-system-prompt", "Be terse."),
    )
)

print(result.structured_output)

MCP servers are installed before the turn and restored after it, including when the turn fails. Asking for something the provider cannot do raises ProviderCapabilityError before the process starts.

Events

from agentshim import AssistantText, CliAgent, EventHandlerBase, ToolCall

class Watcher(EventHandlerBase):
    def on_event(self, event):
        if isinstance(event, ToolCall):
            print("tool:", event.tool)
        elif isinstance(event, AssistantText):
            print(event.text, end="")

agent = CliAgent("claude", event_handler=Watcher())

on_event always runs on the thread that called turn(). The executor reads the CLI's pipes on helper threads but drains them on the calling thread, so a handler needs no locking of its own.

ConsoleEventHandler renders to any text stream; CompositeEventHandler fans out; NullEventHandler drops everything.

Executors

Implement CommandExecutor to run a provider CLI somewhere other than the local host. TransformingExecutor covers the common case of rewriting argv, and applies to the health check too.

from dataclasses import replace
from agentshim import CliAgent, CommandRequest, HostCommandExecutor, TransformingExecutor

def in_container(request: CommandRequest) -> CommandRequest:
    return replace(request, argv=["docker", "exec", "-i", "workspace", *request.argv])

agent = CliAgent("claude", executor=TransformingExecutor(HostCommandExecutor(), in_container))

Testing against agentshim

agentshim.testing emits each provider's real stream format, so your tests never encode a provider's JSON shape.

from agentshim import AssistantText, CliAgent
from agentshim.testing import FakeExecutor, RecordingEventHandler, scripted_turn

events = RecordingEventHandler()
agent = CliAgent(
    "claude",
    executor=FakeExecutor(scripted_turn("claude", text="pong", session_id="s1")),
    event_handler=events,
)

result = agent.run("ping")
assert result.text == "pong"
assert result.session_id == "s1"
assert any(isinstance(event, AssistantText) for event in events.events)

Assert on TurnResult and the typed events, never on internal attributes.

Errors

Everything that escapes turn() is an AgentShimError:

AgentShimError
  CliNotFoundError            binary not on PATH
  CliCheckError               binary found but the health check failed
  CliExitError                nonzero exit: argv, returncode, stdout, stderr,
                              kind (FailureKind), detail (the stream's error text)
    SessionResumeError        the conversation is gone: session_id
  CliTimeoutError             argv, timeout
  ProviderCapabilityError     the provider cannot do what the request asked
    SchemaDialectError        problems: list[str]
  McpConfigError              config file unreadable or not an object

CliExitError.kind says why a turn failed, so a caller can pick a policy without knowing how any CLI words its errors: TRANSIENT (an overload, rate limit or server error that waiting may outlast), USAGE_LIMIT (a quota or billing limit), AUTH (credentials), or OTHER. Claude Code and Codex classify their failures; the other providers report OTHER. agentshim.testing.scripted_failure(provider, kind) scripts each one.

Adding a provider

See docs/extending.md and docs/architecture.md. A provider package holds only argv construction, stream parsing, and provider-specific options; everything shared already lives in agentshim/core/.

Development

uv sync --dev
uv run pytest
./scripts/format_code.sh --check
./scripts/check_errors.sh
./scripts/type_check.sh
./scripts/check_imports.sh

End-to-end tests under tests/e2e/ run the real CLIs. They are skipped unless AGENTSHIM_E2E=1 and the binary is on PATH, so CI never runs them.

AGENTSHIM_E2E=1 uv run pytest tests/e2e -q

Gemini needs a model the account is entitled to, and opencode takes one when the model in your own opencode config is not the one to test:

AGENTSHIM_E2E=1 AGENTSHIM_E2E_GEMINI_MODEL=gemini-2.5-flash \
  uv run pytest tests/e2e/test_gemini_e2e.py -q

See docs/development.md for the full gate list.

uv build          # package
uv publish        # release

uv run --group docs mkdocs build --strict

Metadata

Release files for agentshim 0.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agentshim 0.11.0
File Size Uploaded
agentshim-0.11.0.tar.gz 115.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for agentshim 0.11.0
File Interpreter ABI Platform
agentshim-0.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 255.8 kB

Release files / agentshim-0.11.0.tar.gz

Download URL agentshim-0.11.0.tar.gz
Size 115.5 kB
Tags Source
SHA-256 checksum
How to use checksums
7f637db02bd5a9ef42352797ec50bfd16c1cdc3d836f11d1d12a10ea80372c48
BLAKE2b-256 checksum
How to use checksums
41c4da085015e669edcd59014333c4432a22c7bf7e39f8b0339640f07abfc55d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / agentshim-0.11.0-py3-none-any.whl

Download URL agentshim-0.11.0-py3-none-any.whl
Size 140.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
aa2295a7a791704ccd58edd561f9512d9cabf93cdda917c399e9d6a96d52df3b
BLAKE2b-256 checksum
How to use checksums
44d9e48f2eda3b5b059bd99f59d12d479a0d07880d3b65b206defdd1410c8add
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page