A conversational agent on the harness with the OpenAI Agents SDK in the ReAct pattern: it reasons, then acts by calling a tool, and loops on the result until it can answer. It's the harness form of workshop demo2 + demo3 (OpenAI Agents SDK + Temporal integration, then MCP): the Agents SDK drives the reason-act loop, Temporal makes it durable, and the harness standardizes it behind the same contract, streaming path, and (later) approval policy as every other harness agent.
Ask it a question and it chains tools to find the answer:
- Weather by city —
get_coordinates(city)→get_weather(lat, lon) - Weather where you are —
get_ip_address()→get_location_info(ip)→get_weather(lat, lon) - Formula 1 data — tools from an external F1 MCP server (schedules, results, drivers, …)
- Ask you a question — when a request is ambiguous, it calls
ask_userand waits for your answer
| Tool | Source | Purpose |
|---|---|---|
get_ip_address |
icanhazip.com | The caller's public IP |
get_location_info |
ip-api.com | City / country / lat-lon for an IP |
get_coordinates |
Open-Meteo Geocoding | Lat-lon for a city name |
get_weather |
Open-Meteo Forecast | Current temperature (°F), weather code, wind speed |
| F1 tools | f1-data MCP server |
Formula 1 schedules, results, and driver/constructor data |
ask_user |
the human (callback tool) | Ask the user a clarifying question and use their answer |
- A real ReAct loop. The model reasons about the question, calls a tool, reads the result, and
decides the next action — chaining several real API calls (and picking the right chain) until it
can answer, all driven by the SDK's
Runner. - Harness-owned, activity-backed tools. The weather/geo/IP tools are durable
@agent.activity_tool_defns (they do network I/O, so they run as Temporal activities, never inline), adapted onto the SDK withas_openai_agent_tools(...). Every call flows through the harness'srun_tool, so the harness keeps the approval policy and each tool'stool_start/tool_end/tool_errorevents. Registering the tools through the harness now is what makes a future HITL demo a policy change, not a rewiring. - A durable MCP server. The F1 tools come from an external MCP server registered on the worker
with
StatelessMCPServerProviderand referenced in the workflow withstateless_mcp_server("f1-data"). Each MCPlist_tools/call_toolruns as a Temporal activity — durable, retryable, and visible in the Temporal Web UI.- MCP tools are under harness governance too. Pass
runner=self._runnerto the factory. The SDK callsMCPServer.call_tooldirectly rather than the harness'srun_tool, so the factory wraps the server with the runner it needs. Each MCP call is then approval-gateable and publishestool_start/tool_end/tool_errorunder the sametool_idas itstool_requested, so its turn-stream card resolves like any other tool's.
- MCP tools are under harness governance too. Pass
- Streaming is a toggle. By default (
REACT_AGENT_STREAMunset/1) the turn runsRunner.run_streamed(...), so model calls route through the streaming activity and the harness observer translates raw OpenAI events into the live turn stream (model_interaction_started→reply_delta…tool_requested…tool_start/tool_end→model_interaction_ended). SetREACT_AGENT_STREAM=0(in the worker's environment) and the turn runsRunner.run(...)instead: it completes and returns one reply. Tool cards, theask_userflow, and the final reply still appear on the turn stream; token-by-tokenreply_deltaand themodel_interaction_*brackets do not. (The terminal client renders either mode.) - Human-in-the-loop via a callback tool.
ask_useris an@agent.callback_tool_defntool: it has no server-side body. When the model calls it, the harness publishes acallback_requestedevent and parks the turn in-workflow (durably — no activity timeout) until an external client returns the user's answer, which becomes the tool result and the same turn continues. This is the participation form of HITL (the model asks the human a question), distinct from tool approval (authorizing an action). Crucially, fulfillment is decoupled from any UI — it's a Temporal update (provide_callback_result), reachable over HTTP atPOST /api/callback-result. Our terminalclient.pyis one such fulfiller; the packaged web UI does not implement one (see below). - No approvals yet. The approval policy is
dangerously_skip_all()— tool calls run without a human gate. A future HITL demo flips this to gate the harness-wrapped tools. (ask_useris markedinherently_safe, so even then, asking a question is never itself gated.)
| File | Role |
|---|---|
workflow.py |
ReactAgent — the harness agent; one ask handler, local tools + ask_user adapted onto the SDK plus the F1 MCP server, driven by Runner.run_streamed. |
tool_activities.py |
The four location/weather tools as @agent.activity_tool_defn activities (httpx), plus ALL_TOOLS — the list the worker hands to AgentHarnessPlugin(tools=...). |
human_tools.py |
The ask_user human-in-the-loop callback tool (@agent.callback_tool_defn), plus HUMAN_TOOLS. No activity body — fulfilled by a client. |
worker.py |
Worker hosting the workflow. Declares no activities: AgentHarnessPlugin(tools=...) registers the four tool bodies (skipping the bodiless ask_user) and the OpenAI plugin registers the model activities. Also registers the F1 MCP provider and wires the harness streaming seam. |
client.py |
A terminal client: a session picker that shows which sessions are waiting on an ask_user, then lets you answer open questions, chat, or create a session — all over HTTP. |
agents.toml, ReactAgent.schema.json |
Registry entry that makes this agent selectable in the shared web UI, and the agent's schema it points at; generated from BUILD.bazel (bazel run :agents_update). |
The agent is driven by the shared example stack — the packaged SessionManagerWorkflow worker plus
the FastAPI app and web UI (temporal-agent-harness serve); registering it (its harness_agent in BUILD.bazel, which generates agents.toml) is all that takes.
Unlike the simpler examples, it also ships a terminal client.py — needed to answer ask_user,
since the packaged web UI has no affordance for fulfilling a callback tool.
Prereqs, from the repo root:
cp .env.example .env.localand setOPENAI_API_KEY(and your Temporal connection profile).- Install the F1 MCP server locally (see below) and point the worker at it via
F1_MCP_SERVER_HOME.
An external dependency, not bundled here — this example is the harness port of the workshop's
demo3-mcp,
and the F1 tools come from rakeshgangwar/f1-mcp-server.
It's a Node.js (TypeScript) MCP server that shells out to python3 for FastF1 data, so it needs
both a built Node entrypoint and a Python venv. Prereqs: Node.js 18+, Python 3.10+, uv.
git clone https://github.com/rakeshgangwar/f1-mcp-server.git
cd f1-mcp-server
npm install && npm run build # produces build/index.js
uv venv # the Python side FastF1 shells into
source .venv/bin/activate
uv pip install fastf1 pandas numpy
deactivateThen point the worker at that checkout:
export F1_MCP_SERVER_HOME=/absolute/path/to/f1-mcp-server # or set it in the repo-root .env.localThe worker (worker.py) launches it per the workshop:
bash -c "source $F1_MCP_SERVER_HOME/.venv/bin/activate && node $F1_MCP_SERVER_HOME/build/index.js".
F1_MCP_SERVER_HOME defaults to ~/Projects/Temporal/AI/MCP/f1-mcp-server if unset. Verify the
exact steps against the workshop README linked above.
Then, each in its own terminal:
just temporal # 1. local Temporal dev server (or bring your own)
just session-manager # 2. packaged session-manager worker
just server # 3. builds the Svelte UI, then serves API + UI on http://localhost:8000
just worker # 4. the agent worker
just client # 5. terminal client that chats + answers ask_userjust server builds the web UI first, so it needs pnpm on your PATH. (If the build reports
missing modules, run just app-install once. For UI hot-reloading, just ui-dev runs it on Vite
with /api proxied to the server on :8000.)
Two ways to drive it. The web UI (open http://localhost:8000, pick ReAct Agent) is
great for the tool-chaining demo — each tool call appears live and the reply streams in token by
token; weather tools show as harness tool cards, F1 MCP calls appear as activities in the Temporal
Web UI (see the caveat above). But the web UI cannot answer ask_user — it has no fulfillment
affordance — so use the terminal client (just client) to exercise the human-in-the-loop tool.
The terminal client opens a session picker that lists the ReAct agent's open sessions and
marks which are waiting on an ask_user (⏳). From it you can:
- Create a new session (
n) and chat. Try "What's the weather in Tokyo?" (tools chain), then "What's the weather?" — ambiguous, so the model callsask_user; the client prompts you and your answer (e.g. "Tokyo") flows back and the turn completes. "When is the next Formula 1 race?" exercises the F1 MCP tools. - Answer a waiting session — including one where the question was raised from the web UI (which can't answer it): pick the ⏳ session, its open questions are listed, and your answers are submitted; the client then shows the agent's continued reply.
--session <id>opens a session directly, surfacing any already-pending question.
Inside a session, client-local navigation is keyed off : so it never collides with message
text: :questions re-checks for open questions, :sessions returns to the picker, and :quit
exits.
Without just, the equivalent commands (from the repo root):
uv run --group examples temporal-agent-harness session-manager
uv run --group examples temporal-agent-harness serve examples/react_agent/agents.toml --host 0.0.0.0 --port 8000
uv run --group examples python -m examples.react_agent.worker
uv run --group examples python -m examples.react_agent.client