Skip to main content
Python API covers embedding: AgentSession.start() and chat(), with the standard ports. This page is for a host — a program that supplies its own output sink, tools, prompts, error reporting or gather progress, and drives one agent across many turns. The gateway (Slack, Discord, Telegram) and the interactive shell are both hosts, and they are the same program with different ports.

Four roles, five API modules

Everything a host or tool author touches comes through five modules — the harness API. Nothing else under core.agent_harness is part of it.

The host loop

Build once, then one call per inbound message:
DefaultHeadlessBuild holds what does not vary per host — reasoning client, run records, the default tool provider and prompt provider — and takes only the inputs those defaults need (console, logger, surface). Every port you pass to agent() replaces that default. TurnBinding is a value, not a set of keyword arguments: a field you omit is this turn’s value (no hooks, no callback), never “leave alone”, so a pooled agent cannot inherit another conversation’s hooks by omission. handle is the only per-message call a host makes; dispatch (one engine turn) sits underneath it. The in-memory family is InMemoryHeadlessBuild — a script or test says InMemoryHeadlessBuild().agent(tools=NullToolProvider()) and has an agent with zero configuration. There is one construction verb, <family>.agent(...); hosts do not call HeadlessAgent(...) directly.

The same loop in the two shipped hosts

The gateway keeps one agent per logical session and calls handle per inbound message:
The interactive shell shares the same TurnRunnerSessionAgentPoolhandle path as the gateway (TTY accounting / is_tty=True). Direct construction for tests or a custom host looks like:
Neither host replaces a stage of the agent (action selection, evidence gathering, answering) and neither writes its own turn loop. They differ only in ports and in what they do with the result. Production gateway/shell submissions go through the pool; expand AgentBuildConfig with resolve_agent_ports.

What you implement vs what you call

Rules a host must keep

  • One agent per logical session; one turn at a time on it. The agent enforces this: a second handle while one is running raises AgentBusyError rather than interleaving. Serialize turns before calling handle (the gateway holds a per-session lock across it); do not catch the error to retry.
  • State the whole turn. Build a fresh TurnBinding per message and pass it to handle; do not call bind_turn/dispatch yourself.
  • Bind on the worker thread you dispatch from. Storage scope is a ContextVar; if you hand the turn to a thread pool, wrap it with config.scope_context.in_current_scope on the calling thread.
  • Do not import past the API. Modules under core.agent_harness.turns, .tools, .session and .prompts are internal; the shipped hosts pin their remaining deep imports in shrink-only tests.