Skip to content

The interactive tier

In this guide you'll take an agent you already register for compiled workers and run it as an in-process, streaming chat agent: build an InteractiveAgentSpec, bind it to a LangChain runnable (or the framework's own tool loop), stream events, get structured output, and dispatch heavy work back to compiled workers. For why the two tiers exist and when to use which, read execution tiers first — this page is the how. For an architectural description of the runtime layer (the spec / binding / loop split and each piece's contract), see LangChain runtime agents.

1. Install the extra

The core package stays dependency-light; the bindings are extras, imported lazily. Pick your realtime framework:

pip install "open-agent-compiler[langchain]"     # LangChain binding (this guide)
pip install "open-agent-compiler[pydantic-ai]"   # PydanticAI binding (alternative)

The PydanticAI binding (open_agent_compiler.interactive.bindings.pydantic_ai_binding) mirrors the LangChain one over the same spec: bind(spec, tool_runner=...) returns a ready pydantic_ai.Agent; pass output_type= for native structured output. Everything below applies to either — only step 4's import changes.

2. Point a live profile at your interactive provider

Workers and the live tier usually use different providers (e.g. workers through opencode, live tier on a local OpenAI-compatible vLLM). The live provider is just another SplitProfile — a ModelPreset whose provider_options carry the base_url / api_key_env the binding needs:

from open_agent_compiler import ModelPreset, SamplingDefaults, SplitProfile

LOCAL = ModelPreset(
    name="local-default", provider="local-vllm", model_id="my-local-model",
    sampling=SamplingDefaults(temperature=0.7),
    provider_options={
        "base_url": "http://localhost:8000/v1",
        "api_key_env": "LOCAL_API_KEY",   # optional; placeholder sent if unset
    },
)
LIVE_PROFILE = SplitProfile(name="live", preset=LOCAL,
                            class_map={"default": LOCAL})

3. Build the spec from a registered agent

from open_agent_compiler import build_interactive_spec

spec = build_interactive_spec(agent=my_agent_definition, live_profile=LIVE_PROFILE)

build_interactive_spec renders the agent's core intent — system prompt, workflow as guidance, skills as capabilities — without the worker scaffolding (bash/todo/security blocks), because interactive tools bind natively. It collects every tool reachable from the agent (extra_tools plus workflow-step tools) into runtime-agnostic ToolSpecs.

4. Bind to LangChain and stream

from open_agent_compiler.interactive.bindings.langchain_binding import bind

def my_tool_runner(name: str, args: dict) -> str:
    ...  # route to your ScriptTool subprocess, an API call, etc.

runnable = bind(spec, tool_runner=my_tool_runner)   # streaming LCEL runnable

for chunk in runnable.stream({"messages": [("user", "make it punchier")]}):
    print(chunk, end="", flush=True)

bind returns prompt | chat_model with the spec's tools attached via bind_tools; for a full tool-calling loop drop the pieces into your own LangGraph/AgentExecutor, or use the framework-owned loop below.

5. Stream tool events to your UI

Pass a sink and every tool call brackets itself with tool.start / tool.end / tool.error events; a runner that widens its signature to (name, args, emitter) can push mid-run progress on the same stream, and emitter.child("subagent-x") attributes a dispatched subagent's events separately:

import asyncio
from open_agent_compiler import QueueSink   # also: CallbackSink, CollectingSink

sink = QueueSink(asyncio.Queue())           # drain in your SSE/websocket loop
runnable = bind(spec, tool_runner=my_tool_runner, sink=sink)

The framework-owned loop (below) emits on the same sink, and adds a message event per assistant turn — so a UI draining the queue sees each reply as it lands, interleaved with the tool events.

6. Or use the framework-owned loop (records tool calls)

run_interactive runs one full turn in-process — model call, tool execution rounds, history threading — and returns a RunResult whose tool_calls are ToolCallRecords, so interactive runs are scoreable by the same evaluators and improvement loop as workers:

from open_agent_compiler.interactive import run_interactive

result = run_interactive(spec, "summarize today's alerts",
                         tool_runner=my_tool_runner, max_tool_rounds=8)
print(result.output_text, result.tool_calls)

Tool failures are recorded and fed back to the model as the tool result — never raised — so the agent can recover.

From an async server (FastAPI handler, Telegram callback), use run_interactive_async — the same turn executed off-thread via asyncio.to_thread, so concurrent chats don't block the event loop:

from open_agent_compiler.interactive import run_interactive_async

result = await run_interactive_async(spec, user_text,
                                     tool_runner=my_tool_runner,
                                     sink=sink, history=history)

Identical signature and RunResult; token-level streaming remains the job of the bindings (bind(...).astream(...)) — this wrapper is for turn-level concurrency with full tool-loop recording.

7. Structured output

Pass a JSON schema when building the spec and run_interactive merges a format instruction into the system message and parses the reply:

spec = build_interactive_spec(agent=my_agent, live_profile=LIVE_PROFILE,
                              output_schema={"type": "object",
                                             "properties": {"intent": {"type": "string"}}})
result = run_interactive(spec, "route this message")
print(result.structured)        # parsed dict, or result.error on parse failure

8. Thinking-off for local models

Qwen3.x-style thinking models on vLLM return empty content unless thinking is disabled per call. Set it on the preset — provider_options values are strings, so extra_body is a JSON string that OpenAICompatClient.from_spec decodes:

LOCAL = ModelPreset(
    name="local-default", provider="local-vllm", model_id="my-local-model",
    provider_options={
        "base_url": "http://localhost:8000/v1",
        "extra_body": '{"chat_template_kwargs": {"enable_thinking": false}}',
    },
)

Structured output plus thinking-off is the reliable combination for a snappy local first-contact tier.

9. Dispatch heavy work to compiled workers

The interactive agent should stay fast; long, side-effecting jobs belong to compiled workers. Give the interactive agent the bundled SpawnAgentTool (a typed ScriptTool) and it returns a TaskHandle the chat can track:

from open_agent_compiler import SpawnAgentInput, SpawnAgentTool

out = SpawnAgentTool().execute(SpawnAgentInput(
    agent_name="report-builder-primary",   # a compiled agent in the build tree
    prompt="Build the weekly report.",
    spawn_async=True,                      # return immediately with a handle
))
print(out.task.run_id, out.task.status)

See examples/31_spawn_agent for the parent-spawns-worker composition and examples/35_fastapi_dispatch for awaiting handles over HTTP. The worker side is covered by the opencode dialect.