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 undercore.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 callshandle per inbound
message:
TurnRunner → SessionAgentPool →
handle path as the gateway (TTY accounting / is_tty=True). Direct construction
for tests or a custom host looks like:
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
handlewhile one is running raisesAgentBusyErrorrather than interleaving. Serialize turns before callinghandle(the gateway holds a per-session lock across it); do not catch the error to retry. - State the whole turn. Build a fresh
TurnBindingper message and pass it tohandle; do not callbind_turn/dispatchyourself. - Bind on the worker thread you dispatch from. Storage scope is a
ContextVar; if you hand the turn to a thread pool, wrap it withconfig.scope_context.in_current_scopeon the calling thread. - Do not import past the API. Modules under
core.agent_harness.turns,.tools,.sessionand.promptsare internal; the shipped hosts pin their remaining deep imports in shrink-only tests.