Skip to content

Executors

A CommandExecutor is the whole of agentshim's process transport. Implement it to run a provider CLI somewhere other than the local host; agentshim keeps argv construction, parsing, session state, usage accounting, and events.

class CommandExecutor(Protocol):
    def find_binary(self, name: str, env: Mapping[str, str]) -> str: ...
    def check_binary(self, path: str, env: Mapping[str, str], *, timeout: float) -> None: ...
    def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult: ...

HostCommandExecutor is the default. It starts the process in its own session, writes stdin from a helper thread so a prompt larger than the pipe buffer cannot deadlock, and drains its reader threads on the calling thread so every sink callback is serialized there. On timeout it kills the process group and raises CliTimeoutError.

Wrapping argv

TransformingExecutor rewrites every request before an inner executor sees it, which is how you put the CLI in a sandbox or a container without agentshim knowing about either. The health check goes through run, so the transform applies to it 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),
)

Pass find_binary= when lookup should happen in the target runtime rather than on the host:

TransformingExecutor(inner, in_container, find_binary=lambda name, env: f"/usr/local/bin/{name}")

Writing one from scratch

An executor must call sink.started(handle) once the command is running, write request.stdin and close it, call sink.stdout(line) and sink.stderr(line) for each line (newlines kept), honour cwd, env and timeout, and return a CommandResult. Raise CliNotFoundError from find_binary, CliCheckError from check_binary, and CliTimeoutError from run.

Sink callbacks must all happen on the thread that called run: that is what makes the event-handler thread contract hold.