Mount agents on your app
This guide is for Python developers who already have a Starlette or FastAPI app. It shows how to add one or more agents to that app. Your app keeps its own server, its own middleware and its own login. Read the SDK quickstart first.
How it works
A Harness is one agent. It connects to Temporal, prepares the agent and starts the worker. It does not open a port. Your app serves the agent at a path that you choose.
Start in four steps (Starlette)
Install tiny-harness:
shpip install tiny_harnessWrite
support.toml. The path inbase_urlis where your app serves the agent:toml[agent] id = "support" [temporal] mode = "embedded" [openai] model = "gpt-6.1-sol" [server] base_url = "http://127.0.0.1:8000/agents/support"Write
app.py:pythonfrom pathlib import Path from starlette.applications import Starlette from starlette.routing import Mount from tiny_harness.config import Settings from tiny_harness.service import Harness, harnesses_lifespan support = Harness(Settings.load(Path("support.toml"))) app = Starlette( routes=[Mount(support.prefix, app=support.asgi_app())], lifespan=harnesses_lifespan(support), )Start your app with your key:
shexport OPENAI_API_KEY=sk-... uvicorn app:app --port 8000
Open http://127.0.0.1:8000/agents/support/.well-known/agent-card.json. You see the agent's card. Talk to the agent with tiny-harness tui --url http://127.0.0.1:8000/agents/support.
FastAPI
Install the FastAPI extra:
pip install 'tiny_harness[fastapi]'Include one router for each agent:
from pathlib import Path
from fastapi import FastAPI
from tiny_harness.config import Settings
from tiny_harness.fastapi import harness_router
from tiny_harness.service import Harness
support = Harness(Settings.load(Path("support.toml")))
app = FastAPI()
app.include_router(harness_router(support))- The router starts the agent when your app starts. It stops the agent when your app stops.
- The agent's routes show in your app's
/docspage, in a group namedA2A: support. - Do not give
include_routeraprefix. The router already has the agent's path. If you add one, the agent refuses every request with the error 500. - If your app has a
root_path(for example behind a proxy), put it inbase_urltoo. Otherwise the agent refuses every request with the error 500, because its card would have the wrong address. - The router does not add CORS and does not serve the web renderer at
/ui. AddCORSMiddlewareorStaticFilesto your app if you need them. tiny-harness writes a warning whencors_originsorui_diris set.
Several agents in one app
Give each agent its own configuration file and its own [agent] id. Mount each agent at its own path. Start all agents with one harnesses_lifespan:
names = ["support", "billing", "ledger"]
agents = [Harness(Settings.load(Path(f"{name}.toml"))) for name in names]
app = Starlette(
routes=[Mount(agent.prefix, app=agent.asgi_app()) for agent in agents],
lifespan=harnesses_lifespan(*agents),
)tiny-harness keeps the agents apart:
- Each agent uses its own Temporal task queue. The queue is the agent id, unless you set
task_queue. - An agent answers only for its own tasks. A task id of another agent gets "task not found".
- Two agents cannot use the same id, the same task queue or the same
sqlite_path. The second agent does not start, and the error names the agent that has it. - Agents with the same Temporal settings share one connection. In embedded mode, set the same
database_pathin each file to use one Temporal server. - Each log line has an
agentfield with the agent id.
Let one agent use another
Add the other agent to [[agents]] with its full address, path included:
[[agents]]
id = "billing"
url = "http://127.0.0.1:8000/agents/billing"The support agent then sends sub-tasks to billing over HTTP, through your app. It builds the card of billing from the billing configuration in the same process. It does not fetch the card, because your app does not listen yet while it starts. A complete example with five agents is in examples/multi_agent.
Use your own login
By default, the harness reads the user's name from the X-Participant-Id header. The harness does not check that header. When your app has its own login, give the harness a function that returns the logged-in user:
from starlette.requests import Request
def logged_in_user(request: Request) -> str | None:
return request.state.user # set by your login middleware
support = Harness(settings, participant=logged_in_user)With this function:
- The harness ignores the
X-Participant-Idheader. - A request for which the function returns
Nonehas no user. The harness refuses to start a task for it and shows it no task. - If the function raises an error, the request fails with the error 500.
- Do not let the function return the id of one of your agents for a person. The harness trusts its agents more than people: an agent can set fields of a new task that a person cannot.
Calls from one agent to another carry only X-Participant-Id. An agent also calls itself this way when it gives a step to a sub-task. If your login refuses these calls, sub-tasks and delegation fail. Let your login accept them, or mount the agents that work together without the function.
Options
Harness(...) argument | What it does | Default |
|---|---|---|
prefix | The path where your app mounts the agent. Use it when base_url has no path | The path of base_url |
participant | A function that returns the logged-in user | The X-Participant-Id header |
routes | A RouteOptions that turns off routes or middleware | Everything on |
client | A Temporal client that you already have. The harness does not close it | A new connection |
with_worker | Run the worker in this process | True |
agent | An AgentDescription for the agent's card: name, description, skills | [agent] |
plugins, commands, builtin_commands | Plugins and commands in code, as for running_harness | none |
RouteOptions has five switches. Each is True by default:
| Switch | Turn it off when |
|---|---|
limits | Your app already limits the request size and the number of requests. Without it, the harness limits neither |
cors | Your app adds its own CORS rules |
monitor | You do not want GET /_monitor |
push | You do not want POST /a2a/push/.... The agent then watches the tasks of other agents itself |
ui | You do not want the web renderer at /ui |
from tiny_harness.service import RouteOptions
support = Harness(settings, routes=RouteOptions(limits=False, cors=False))What goes wrong
| You see | Why | Do this |
|---|---|---|
Error 503 agent support is not started | The app did not run the lifespan, or it is still starting | Pass harnesses_lifespan(...) to your app |
Error 500 mounted at '/x' but configured for '/agents/support' | The mount path is not the path in base_url | Mount at agent.prefix |
HarnessConflictError at start | Two agents have the same id, task queue or store file | Give each agent its own values |
ConfigError about the prefix | prefix and the path of base_url are different | Use one of them |
Next steps
- Deploy: run the agents for real users.
- Agent: the
[agent]section. - Plugins and other agents: the
[[agents]]table.