Adapter contract

Every mode talks to the agent through one class, AgentAdapter in aicodebox/adapters/base.py. You subclass it once per agent and name it in AICODEBOX_ADAPTER. What the HTTP API, MCP, Telegram and cron can do is exactly what your build_argv knows how to drive.

A minimal adapter

# your_adapter/adapter.py
from aicodebox.adapters.base import (
    AgentAdapter,
    RunRequest,
    RunResult,
    StreamEvent,
)


class YourAdapter(AgentAdapter):
    name = "your-agent"
    binary = "your-agent"
    available_models = ["fast", "smart"]
    available_thinking_levels = ["off", "low", "high"]

    def build_argv(self, req: RunRequest) -> list[str]:
        argv = [self.binary, "-p", req.prompt]
        if req.model:
            argv += ["--model", req.model]
        return argv

    def parse_output(self, stdout: str, req: RunRequest) -> RunResult:
        # The runner fills raw_stdout, raw_stderr and exit_code afterwards.
        return RunResult(
            text=stdout.strip(),
            raw_stdout="",
            raw_stderr="",
            exit_code=0,
        )

    def parse_stream_event(self, line: str, req: RunRequest) -> StreamEvent | None:
        return StreamEvent(type="delta", text=line + "\n") if line else None

The runner starts the agent with the request’s workspace as its working directory, writes req.prompt to its stdin, and collects stdout and stderr. An agent that reads the prompt from its arguments can ignore stdin.

What to implement

Member Required Default Purpose
name yes "" Shown in logs, /healthz, Telegram and event envelopes. An OpenAI request whose model equals this name falls back to the agent’s default model.
binary no "" Used by the default interactive_argv and passthrough_argv.
available_models no [] Model list for /openai/v1/models and the Telegram /model picker. AICODEBOX_AVAILABLE_MODELS overrides it.
available_thinking_levels no [] Effort list for the Telegram /effort picker. AICODEBOX_AVAILABLE_EFFORTS overrides it.
build_argv(req) yes raises NotImplementedError Turn a RunRequest into the agent’s command line.
parse_output(stdout, req) no plain text: text=stdout.strip() Turn the agent’s stdout into a RunResult. Set text, and usage, session_id, parsed, parse_error or provider_error when the agent reports them.
parse_stream_event(line, req) no one delta per non-empty line Turn one stdout line of a streaming run into a StreamEvent, or None to skip it.
validate(req) no checks output_format and event_mode Raise ValueError for a request the agent cannot honor. The HTTP API turns it into a 422.
translate_auth(env) no {} Return extra variables to add to the agent’s environment, for example a provider key under the name the agent expects.
build_env(req) no process environment, then translate_auth, then req.env The full environment for the agent process.
post_validate_json(result, req) no   Helper: parse result.text as JSON and validate it against req.json_schema. Call it from parse_output if your agent does not enforce schemas itself.
interactive_argv, passthrough_argv, auth_paths no   Hooks child wrappers can use. The base modes do not call them.
parse_events no [] Legacy hook. The runner no longer calls it.

RunRequest

Every mode builds a RunRequest from its own input. Your adapter decides which fields it supports.

Field Type Meaning
prompt str The instruction. Also written to the agent’s stdin.
workspace str Absolute working directory for the run.
model str or None Requested model. When a mode passes none, the runner uses ANTHROPIC_MODEL from the environment if set.
system_prompt str or None Replace the agent’s system prompt.
append_system_prompt str or None Append to the system prompt. Telegram and cron always use this for their own hints.
json_schema dict or None The caller wants JSON matching this schema.
output_format str text, json or json-verbose. A hint for how the agent should print its answer.
no_continue bool Start a fresh session instead of continuing the last one in this workspace.
resume str or None Resume a specific session ID.
extra_args list[str] Extra command-line arguments from the caller.
env dict[str, str] Extra environment variables, applied last.
timeout_seconds int or None The runner kills the agent after this many seconds and reports exit code 124.
thinking str or None Reasoning level, such as off, low, high.
tools_allowlist list[str] or None Restrict the agent’s own tools.
no_tools bool Disable the agent’s own tools.
event_mode str none or full. The runner handles full itself (see below).
proc_hook callable or None Called with the Popen object right after start. The modes use it for cancellation.

RunResult

Field Meaning
text The final answer.
raw_stdout, raw_stderr, exit_code Filled in by the runner after parse_output returns. A missing binary gives exit code 127. A timeout gives 124 and the stderr text timeout after <n>s.
session_id The agent’s session or conversation ID, when it reports one.
usage Token counts as a dictionary. The OpenAI endpoint reads input_tokens, inputTokens or input, and output_tokens, outputTokens or output.
parsed, parse_error A decoded JSON answer, or why decoding failed.
provider_error An upstream error such as a content-policy refusal. Schema retries stop on it, and the OpenAI endpoint returns it as a 400.
attempts, events, event_attempts Filled in by the runner for schema retries and event retention.

Stream events

parse_stream_event returns a StreamEvent(type, text, data). The OpenAI streaming path reads these types:

Type Fields Effect
delta text Sent as a content chunk.
error text Sent as a [error: ...] content chunk, and the finish reason becomes error.
stop data["reason"] Sets the finish reason.
session, usage, raw data Available to modes. The OpenAI stream ignores them.

The runner adds its own stop event when the process exits, with reason stop, error (non-zero exit, preceded by an error event holding up to 500 characters of stderr) or timeout.

Native events

When a caller asks for eventMode: "full" or streamed native events, the runner keeps every non-empty stdout line itself. Your adapter does nothing for this. A line that parses as a JSON object stays as that object. Diagnostics, malformed JSON and JSON values that are not objects become {"line": "<the raw line>"} records. See HTTP API for the envelope.

JSON helpers

parse_json_response(text, schema) decodes an answer that should be JSON. It tries the whole text, the text with code fences stripped, each fenced block from last to first, and the first balanced {...} or [...], and returns the first candidate that parses and validates. It returns (value, None) on success and (None, error) otherwise. strip_json_fences and parse_native_event_lines are exported too.

aicodebox.shared.json_envelope.parse_envelope(raw) reads the JSON output formats of Claude Code, pi and opencode and returns (text, usage, stop_reason). Use it if your agent prints one of those shapes.

Testing an adapter

aicodebox.adapters.base.reset_adapter_cache() clears the cached instance, so a test can switch AICODEBOX_ADAPTER between cases. The base package’s own tests use a stub adapter in aicodebox/tests/conftest.py.