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.

Three roles, four import doors

Everything a host touches comes through four modules. Nothing else under core.agent_harness is public.

The host loop

Build once, then one call per inbound message:
DefaultPorts 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 HeadlessPorts — a script or test says HeadlessPorts().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 builds one agent at startup and calls handle per prompt:
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.

What you implement vs what you call

Rules a host must keep

  • One agent per logical session; one turn at a time on it. bind_turn mutates per-turn state; the gateway holds a per-session lock across dispatch. Do not share one agent between concurrent turns.
  • 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 reach past the doors. Modules under core.agent_harness.turns, .tools, .session and .prompts are internal; the shipped hosts pin their remaining deep imports in shrink-only tests.