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 where cached_input_tokens <= input_tokens on every provider
  • 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
    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

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

Release files for agentshim 0.6.1

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.6.1
File Size Uploaded
agentshim-0.6.1.tar.gz 78.8 kB Details

Built distribution (wheel)

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

Total release size:180.4 kB

Release files / agentshim-0.6.1.tar.gz

Download URL agentshim-0.6.1.tar.gz
Size 78.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ffc82a169709ac838700bfec72d39ec49fa7ecb659f0dfbf9c9f785a4e12ff94
BLAKE2b-256 checksum
How to use checksums
d5065c16b00fc50f5dc82e172f0f1cb11dd2402214209c6bbc263dff5e67e889
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 Sep 10, 2026.

Transparency log

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

Download URL agentshim-0.6.1-py3-none-any.whl
Size 101.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c15c428ac4b3fdba8b665bbb97ae70fb57c59f6fd38807827525377df80a4633
BLAKE2b-256 checksum
How to use checksums
88290980ec9fb52b89816ff4370e59fe156f8e985579fc2559c5fc8a30541367
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 Sep 10, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

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