Harness API
The Harness API runs fast-agent from Python without entering the TUI or starting an MCP/ACP transport server.
Use it when you want to embed fast-agent in another Python application, such as a web service, batch worker, test harness, CLI automation layer, or adapter.
async with fast.harness() as harness:
session = await harness.session("support-123", agent_name="support")
response = await session.generate("Help this customer")
The harness is session-oriented:
- each harness session owns one stable
AgentInstancefor that session's lifetime; - the same session ID returns the same
HarnessSessionobject; - different session IDs get isolated
AgentInstanceobjects; - deleting a session disposes its instance;
- exiting the harness context disposes all remaining session instances.
A fast-agent session is an affinity key for a full AgentInstance, not a wrapper
around one agent. The instance contains the active agent map for the app:
regular agents, routers, evaluators, orchestrators, agents-as-tools,
MCP-backed agents, and tool-only helper agents.
Quick start
import asyncio
from fast_agent import FastAgent
fast = FastAgent("Support Bot", parse_cli_args=False)
@fast.agent(
"support",
instruction="You are a concise customer support assistant.",
model="sonnet",
)
async def main() -> None:
async with fast.harness() as harness:
session = await harness.session("customer-123", agent_name="support")
response = await session.generate("Help this customer reset their password.")
print(response.last_text())
if __name__ == "__main__":
asyncio.run(main())
Use parse_cli_args=False when embedding fast-agent in an application that owns
command-line parsing.
Creating a harness
| Parameter | Default | Meaning |
|---|---|---|
model |
None |
Optional global model override, similar to the CLI --model override. |
The harness uses the same initialization path as fast.run():
- app initialization;
- config and model loading;
- AgentCard loading from the active environment's
agent-cards/directory; - Agent Skill discovery and prompt injection;
- MCP server configuration;
- shell/filesystem runtime setup;
- global prompt context;
- provider-key validation.
It does not enter:
- TUI mode;
- CLI
--messagemode; - CLI
--prompt-filemode; - MCP server mode;
- ACP server mode.
If the active environment contains AgentCards, the harness loads them before validating that agents exist:
fast = FastAgent("Support Bot", parse_cli_args=False, environment_dir=".fast-agent")
async with fast.harness() as harness:
session = await harness.session("customer-123", agent_name="support")
Use fast.load_agents(path) when you want to load AgentCards from an additional
non-environment path.
Typing and IDE autocomplete
The public API uses concrete, typed classes:
from fast_agent import AgentHarness, FastAgent, HarnessSession, HarnessSessions
async with fast.harness() as harness:
typed_harness: AgentHarness = harness
sessions: HarnessSessions = harness.sessions
session: HarnessSession = await sessions.get_or_create("demo")
message = await session.generate("hello")
These classes are exported from fast_agent for imports such as:
Sessions
Get or create a session with harness.session():
If no ID is supplied, the default session ID is "default":
Session ID behavior:
Nonemeans"default";- strings are stripped;
- empty strings raise
ValueError; - valid IDs are 1-128 characters, start and end with a letter or number, and contain only letters, numbers, dashes, or underscores.
Session naming
Choose short descriptive IDs such as customer-123, ticket_456, or
repo-review.
The validation rule is exactly the one above: ^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,126}[A-Za-z0-9])?$.
Names with spaces, slashes, dots, colons, or other punctuation are rejected.
The slug-style rule keeps persisted session folders simple when
session_history is enabled.
Explicit session management
The harness exposes a session manager:
session = await harness.sessions.get("demo")
session = await harness.sessions.create("demo")
session = await harness.sessions.get_or_create("demo")
await harness.sessions.delete("demo")
| Method | Behavior |
|---|---|
get(name) |
Return an existing session. Raise if missing. |
create(name) |
Create a new session. Raise if it already exists. |
get_or_create(name) |
Return an existing session or create it. |
delete(name) |
Delete a session if present; no-op if missing. |
Session map operations are protected by a harness-level lock.
Calling agents from a session
Harness sessions reuse the existing fast-agent protocol methods and return types. There is no new result wrapper.
text = await session.send("hello")
message = await session.generate("hello")
data, raw = await session.structured("classify this", MyModel)
data, raw = await session.structured_schema("classify this", schema)
send()
send() returns plain text:
generate()
generate() returns a PromptMessageExtended:
message = await session.generate("Summarize this ticket.")
print(message.last_text())
print(message.stop_reason)
print(message.channels)
Use generate() when an adapter or application needs the richer assistant
message rather than only text.
structured()
Use structured() with a Pydantic model:
from pydantic import BaseModel
class TicketTriage(BaseModel):
priority: str
category: str
needs_human: bool
data, raw = await session.structured(
"Classify this support ticket.",
TicketTriage,
)
if data is not None:
print(data.priority)
else:
print(raw.last_text())
structured_schema()
Use structured_schema() with a JSON Schema dictionary:
schema = {
"type": "object",
"properties": {
"risk": {"type": "string", "enum": ["low", "medium", "high"]},
"reason": {"type": "string"},
},
"required": ["risk", "reason"],
"additionalProperties": False,
}
data, raw = await session.structured_schema(
"Return deployment risk metadata.",
schema,
)
Agent selection
Each call resolves the target agent in this order:
- explicit per-call
agent_name; - the session
default_agent_name; - the app default agent.
session = await harness.session("pr-123", agent_name="reviewer")
# Uses the session default: reviewer.
await session.generate("Review this PR")
# Overrides the session default for this call only.
await session.generate(
"Write release notes",
agent_name="writer",
)
Tool-only agents are not selected as defaults, but explicit targeting is allowed
when the existing AgentApp rules allow that target.
Session lifecycle
A session owns one stable AgentInstance until it is deleted or the harness
context exits.
async with fast.harness() as harness:
a = await harness.session("customer-a")
b = await harness.session("customer-b")
await a.send("Remember that my name is Alice.")
await b.send("Remember that my name is Bob.")
Behavior:
- the same session ID returns the same session object;
- different session IDs get different
AgentInstanceobjects; - histories, MCP aggregators, and tool runtime objects are isolated between sessions;
- when
session_historyis enabled, the harness creates or loadsenvironment_dir/sessions/<session_id>/; - persisted history for the same session ID is hydrated when a new harness process starts;
- deleting a session disposes its
AgentInstance; - deleting a session removes its persisted session folder when persistence is enabled;
- the harness context disposes any remaining session instances on exit.
For example:
fast = FastAgent("Support Bot", parse_cli_args=False, environment_dir=".fast-agent")
async with fast.harness() as harness:
session = await harness.session("customer-123", agent_name="support")
await session.send("Remember this customer prefers email.")
creates:
Running the program again with the same session_id loads that persisted
history before the next turn. Set session_history: false in config or use
noenv=True to disable persistence.
Delete a session explicitly when you are done with it:
After deletion, the HarnessSession object is closed. Get or create the same ID
again to start a fresh session.
Concurrency
The harness rejects concurrent operations on the same HarnessSession.
import asyncio
session = await harness.session("customer-123")
first = asyncio.create_task(session.generate("first"))
try:
await session.generate("second")
except RuntimeError as exc:
print(exc)
await first
Only one operation may be active for a session at a time. This protects mutable message histories, MCP aggregators, and tool runtime state while surfacing accidental misuse immediately.
Expected error shape:
RuntimeError: Session 'customer-123' is already running generate; start another session for parallel conversation branches.
For independent parallel branches, create separate sessions:
a = await harness.session("customer-a")
b = await harness.session("customer-b")
await asyncio.gather(
a.send("Help customer A"),
b.send("Help customer B"),
)
Deleting an active session also raises:
Deletion does not wait behind a long-running operation.
Clearing history
clear() clears the resolved target agent's history:
To clear a specific agent in the session:
To also clear applied prompts:
clear(agent_name=None) clears only the resolved default target. It does not
clear every agent in the session.
Skills, MCP, and agents-as-tools
Agent Skills work under the harness the same way they work under fast.run().
When the harness starts, default skills are discovered, agent-specific skill
configuration is resolved, and skill manifests are injected into prompts through
{{agentSkills}}.
fast = FastAgent(
"Developer Assistant",
parse_cli_args=False,
skills_directory=".fast-agent/skills",
)
@fast.agent(
"dev",
instruction="""
You help with repository maintenance.
Available skills:
{{agentSkills}}
""",
model="sonnet",
)
async def main() -> None:
async with fast.harness() as harness:
session = await harness.session("issue-492", agent_name="dev")
response = await session.generate(
"Use the relevant repository skills to investigate this failure."
)
print(response.last_text())
Because a session owns a full AgentInstance, multi-agent workflows continue to
work inside the session:
session = await harness.session("analysis-123", agent_name="manager")
response = await session.generate(
"Analyze this issue. Use your helper agents and MCP tools if needed."
)
The selected agent can use configured child agents as tools, MCP servers, and workflow dependencies in the same session-owned instance.
Request parameters
Pass RequestParams to any call:
from fast_agent import RequestParams
response = await session.generate(
"Give me a concise answer.",
request_params=RequestParams(maxTokens=1024),
)
Per-call request parameters are passed to the selected agent method.
Shell and filesystem tools
Use harness.shell() for programmatic shell commands that should not be added
to a conversation:
async with fast.harness() as harness:
result = await harness.shell("pwd")
if result.exit_code == 0:
print(result.stdout)
else:
print(result.stderr)
harness.shell() returns a structured ShellExecutionResult with stdout,
stderr, and exit_code. It runs through the same local shell execution
configuration as the model-facing shell tool, but it does not create a harness
session and does not update agent history.
Use session.shell() when you want shell work serialized with a specific
HarnessSession:
async with fast.harness() as harness:
session = await harness.session("repo-review", agent_name="reviewer")
result = await session.shell("git diff --stat")
session.shell() also returns ShellExecutionResult and does not add the
command or output to chat history. It is still a session operation, so it is
rejected while the same session is already running send(), generate(),
structured(), or another shell() call.
If an agent is configured with shell access, a harness call can also use that tool through the normal model/tool loop:
async with fast.harness() as harness:
session = await harness.session("repo-review", agent_name="reviewer")
response = await session.generate(
"Inspect the current git diff and summarize risky changes."
)
The shell/tool activity belongs to the selected agent in that session's
AgentInstance, so it is part of the normal conversation and tool execution
flow. Use this when the model should decide which commands to run or when the
tool interaction should be part of the agent turn.
Filesystem access remains tool-mediated through configured agents. A future sandbox/session-environment API may expose first-class filesystem operations.
Session IDs are conversation/runtime affinity keys, not security boundaries. A session does not automatically create a per-session filesystem sandbox. For multi-user applications that expose shell or filesystem tools, use separate harnesses, environment roots, process-level sandboxes, or another explicit isolation layer appropriate for your deployment.