docs / agents-sdk / api referencegithub.com/Fareground/agents-sdk ↗

API reference

The headline public symbols, all importable from fg_agents. This covers the surface most apps touch; the full export list is in src/fg_agents/__init__.py.


One-shot: ask / stream

Free functions for one-shot use — no client object. Each constructs an ephemeral Agent, runs the prompt, and closes it.

from fg_agents import ask

print(await ask("What's 2+2?"))
async def ask(
    prompt: str,
    *,
    model: str | None = None,        # omit to auto-detect from the environment
    tools: list[Callable | RegisteredTool] | None = None,
    system_prompt: str = "",
    memory: str | BaseRepository = "memory",
    **kwargs,                        # anything else Agent accepts
) -> AgentRunResult

str() of the result is the answer text, so print(await ask(...)) prints just the answer.

stream(prompt, ...) takes the same arguments and yields StreamEvents as they happen; the ephemeral agent is closed when the stream is exhausted or the generator's aclose() runs:

from fg_agents import stream

async for event in stream("Tell me a story"):
    print(event.type, event.data)

If you might exit early (break/return mid-iteration), wrap the stream in contextlib.aclosing so cleanup runs deterministically instead of waiting for garbage collection:

import contextlib

async with contextlib.aclosing(stream("Tell me a story")) as events:
    async for event in events:
        if enough(event):
            break

The ladder: ask() for one-shots → Agent for conversations → AgentEngine and friends for full control.


Model auto-detection

resolve_default_model() -> str picks a provider-qualified default model from the environment. It runs whenever ask(), stream(), or Agent() is called without a model. resolve_default_model_async() is its async twin — same order and error, but the Ollama probe never blocks the event loop; it's what ask()/stream() use internally. The probe result is cached briefly so repeated constructions don't re-pay the connect timeout. Detection order:

  1. ANTHROPIC_API_KEY → anthropic:claude-sonnet-4-6
  2. OPENAI_API_KEY → openai:gpt-5.2
  3. GOOGLE_API_KEY or GEMINI_API_KEY → google:gemini-2.5-flash
  4. A local Ollama server on localhost:11434 → ollama:qwen3:8b

If none apply it raises ModelDetectionError (importable from fg_agents) with instructions. An explicit model= always wins — detection never overrides a caller's choice.


Agent (facade)

One object that wires AgentLLM + ToolRegistry + a repository + AgentEngine. The fastest way to run an agent; graduate to the low-level objects at any point.

from fg_agents import Agent

agent = Agent(model="anthropic:claude-sonnet-4-6", tools=[my_func])
result = await agent.run("hello")

Constructor

Agent(
    model: str | None = None,                     # provider-qualified, e.g. "openai:gpt-5.2"; None = auto-detect
    *,
    tools: list[Callable | RegisteredTool] | None = None,
    system_prompt: str = "",
    name: str = "agent",
    memory: str | BaseRepository = "memory",      # "memory" | "sqlite" | "sqlite:<path>" | "postgres:<url>" | repository
    llm: AgentLLM | None = None,                  # pre-built client (e.g. with api_keys)
    api_keys: dict[str, str] | None = None,       # provider -> key; ignored when llm= is given
    session_id: str | None = None,                # default conversation id; fresh per instance otherwise
    **agent_kwargs,                               # extra AgentDefinition fields: max_turns, temperature, fallback_models, ...
)

tools accepts @tool-decorated functions, plain callables (auto-wrapped — schema derived from the signature, description from the docstring), or RegisteredTool instances. Duplicate tool names raise ValueError.

The repository is initialized lazily on the first run()/stream() call — forgetting to initialize is never an error. (The low-level tier requires an explicit await repo.initialize().)

Methods

Method Returns Notes
await run(message, *, session_id=None, metadata=None, variables=None) AgentRunResult Runs to completion. Continues the instance's conversation unless session_id is passed.
stream(message, *, session_id=None, metadata=None, variables=None) AsyncIterator[StreamEvent] Yields raw events as they happen.
await close() None Closes the repository; the Agent is unusable afterwards.

Agent is also an async context manager — async with Agent(...) as agent: closes it on exit:

async with Agent(tools=[greet]) as agent:
    result = await agent.run("hello")

Failure contract

run() converts any failure — an engine-emitted error event or an exception raised mid-run — into a single AgentRunError carrying the session_id and the partial events collected so far; the original exception (when there is one) is chained as __cause__.

stream() is lower-level by design: engine failures surface either as error events in the stream or as raw exceptions from the iterator — handle both, or use run() for the uniform contract.

Graduation properties

agent.engine, agent.llm, agent.tools (the ToolRegistry), agent.repository, agent.definition, agent.session_id — the exact objects the facade built, for when you outgrow it.


AgentRunResult

Frozen dataclass returned by Agent.run:

Field Type Meaning
text str Final assistant output (str(result) returns it too).
session_id str Conversation the run belonged to.
events list[StreamEvent] Every event emitted during the run.

AgentRunError

AgentRunError(message, session_id, events) — subclass of AgentFrameworkError (the root of the framework's exception hierarchy). Attributes: .session_id, .events (partial events up to the failure), .__cause__ (original exception, if any).


AgentDefinition

Declarative agent configuration (Pydantic model). Key fields:

AgentDefinition(
    name="assistant",
    model="anthropic:claude-sonnet-4-6",   # required — no default model
    system_prompt="...",
    tools=["search", "greet"],             # names registered in the ToolRegistry
    # plus: max_turns, temperature, fallback_models, sub_agents, skills, ...
)

See fg_agents.core.types.AgentDefinition for the full field list.


AgentEngine

The ReAct loop: model → tools → model → ..., emitting StreamEvents at each step. Runs are serialized per session (per-session lock) and support cooperative cancellation.

AgentEngine(
    llm: AgentLLM,
    tool_registry: ToolRegistry,
    repository: BaseRepository,
    skills_manager: SkillsManager | None = None,
    middleware: list[BaseMiddleware] | None = None,
)

async for event in engine.run(
    session_id: str,
    user_message: str,
    agent_def: AgentDefinition,
    metadata: dict | None = None,
    variables: dict | None = None,
):
    ...

run() creates or resumes the session, appends the user message, and runs the loop until completion or max_turns. The repository must be initialized (await repo.initialize()) before the first run.


AgentLLM

Provider-agnostic LLM client with streaming and tool-use support. Native Anthropic/OpenAI/Google clients plus 20+ OpenAI-compatible providers (including ollama: for local models). Provider clients are created lazily.

AgentLLM(
    api_keys: dict[str, str] | None = None,        # provider -> key; falls back to {PROVIDER}_API_KEY env vars
    custom_providers: dict[str, str] | None = None # provider name -> base URL, for providers not built in
)

Model ids are always provider-qualified: "anthropic:claude-sonnet-4-6", "openai:gpt-5.2", "ollama:qwen3:8b".


Tools

@tool decorator

from fg_agents import tool

@tool(name="search_db", description="Search the case database")
async def search_db(query: str, limit: int = 10) -> dict:
    ...
tool(
    name: str | None = None,              # default: function name
    description: str | None = None,       # default: first docstring line
    tool_type: ToolType = ToolType.FUNCTION,
    permission_level: str = "auto_approve",
    timeout_seconds: int = 30,
    retry_max: int = 0,
    tags: list[str] | None = None,
    json_schema: dict | None = None,      # explicit schema; otherwise derived from the signature
)

Sync functions are wrapped to run in a thread; async functions run natively. The decorated function gains a .tool_definition attribute (a RegisteredTool). A parameter named ctx/context typed ExecutionContext is injected by the framework and hidden from the JSON schema.

ToolRegistry

Central tool store handed to the engine or create_app.

Method Purpose
register_function(fn) Register a @tool-decorated function.
register(registered_tool) Register a RegisteredTool directly.
register_many([...]) Bulk registration.
has(name) / get(name) Lookup.
.tools dict[str, RegisteredTool] of everything registered.

Persistence

from fg_agents import create_repository

repo = create_repository("memory")                                   # tests / throwaway
repo = create_repository("sqlite", db_path="agents.db")              # local dev (needs fg-agents[sqlite])
repo = create_repository("postgres", db_url="postgresql+asyncpg://…")# production (needs fg-agents[postgres])
await repo.initialize()   # required before use (the Agent facade does this for you)

All backends implement BaseRepository (async session/message CRUD). Details and dependency requirements: persistence.md.


Web layer

Requires the web extra (python -m pip install --upgrade "fg-agents[web]"); without it, create_app/create_agent_router raise ImportError with the install hint.

from fg_agents import create_app

app = create_app(
    agents: dict[str, AgentDefinition],       # named agents exposed by the API
    db_url: str | None = None,                # postgres URL for production; SQLite file if None
    api_keys: dict[str, str] | None = None,
    tool_registry: ToolRegistry | None = None,
    cors_origins: list[str] | None = None,
    prefix: str = "/api/agent",
    title: str = "Fareground Agent API",
    admin_only: bool = True,                  # POST /tools and /agents return 403 when True
    **fastapi_kwargs,
) -> FastAPI

Returns a FastAPI app with sessions, messages, and SSE streaming wired; persistence is initialized in the app lifespan. For mounting into an existing app, use create_agent_router(...) with your own authenticator for multi-tenant isolation (see fg_agents/api/router.py).

Routes (under prefix):

POST /sessions                    -> { "session_id": "..." }
POST /sessions/{id}/messages      -> text/event-stream (SSE)

Streaming

StreamEvent (Pydantic model) is the single event shape flowing Engine → iterator → SSE → frontend:

Field Type
id str (uuid)
type EventType (e.g. llm.text_delta)
session_id str
data dict (per-type payload)
timestamp UTC datetime
turn_number int \| None

event.to_sse() renders the SSE wire frame; event.to_dict() a plain dict. Every event type and its payload: streaming.md.


Middleware

Hooks into the agent loop, passed to AgentEngine(middleware=[...]). Implement the Middleware protocol or subclass BaseMiddleware (pass-through defaults) and override what you need:

Hook Signature (self omitted) Can
before_llm_call (messages, tools, context) -> (messages, tools) Inject context, filter tools.
after_llm_call (response, context) -> response Inspect/modify the LLM response.
before_tool_call (tool_call, context) -> tool_call \| None Modify, or return None to block the call.
after_tool_call (tool_call, result, context) -> result Inspect/modify the tool result.
on_turn_complete (turn_number, context) -> None Per-turn bookkeeping.
on_error (error, context) -> None Observe loop errors.

Middleware runs in registration order. Shipped implementations: AuditMiddleware, TokenTrackingMiddleware, PermissionMiddleware, RateLimitMiddleware, LoopGuardMiddleware, ContextCompressionMiddleware.


Streaming: the SSE wire format

This is the contract between the framework and your frontend. Every event the engine emits is a StreamEvent (see src/fg_agents/streaming/events.py), serialized as one Server-Sent Event frame.

The wire frame

id: <event uuid>
event: <event type, e.g. llm.text_delta>
data: <full event as JSON>

The data: JSON always has the same envelope:

{
  "id": "6f6c…",
  "type": "llm.text_delta",
  "session_id": "session-1",
  "data": { "text": "Hel" },
  "timestamp": "2026-08-10T17:02:11.412Z",
  "turn_number": 1
}

The stream also carries periodic heartbeat comments to keep proxies from closing idle connections; SSE clients ignore them automatically.

Event types

Listed in the order a typical run emits them.

Event type Payload (data) fields When emitted
session.started agent_id, agent_name Once, when a run begins on a session.
turn.started turn At the start of each ReAct turn.
llm.thinking text Streaming chunk of model reasoning/thinking (models that expose it).
llm.text_delta text Streaming chunk of assistant text. Concatenate these to render the reply.
llm.tool_call tool_name, tool_call_id, arguments The model requested a tool call.
tool.executing tool_name, tool_call_id Tool execution is starting.
tool.result tool_name, tool_call_id, status, duration_ms, output_preview; optional card_data, result Tool execution finished (success or error — see status).
subagent.started child_session_id, agent_name, task (truncated to 200 chars) Orchestrator delegated work to a sub-agent.
subagent.completed child_session_id, agent_name, status A sub-agent finished.
context.compacting messages_before, messages_after The conversation history was compressed to fit the context window.
turn.completed turn, stop_reason End of a ReAct turn. stop_reason: end_turn, tool_use, max_turns, max_tokens, error, cancelled.
session.completed total_turns, input_tokens, output_tokens, final_output The run finished successfully. final_output is the complete assistant reply.
error error_type, message (truncated to 1000 chars), recoverable Something failed. If recoverable is true the session can continue.

A minimal frontend loop

// POST /api/agent/sessions            -> { session_id }
// POST /api/agent/sessions/{id}/messages  (SSE response)
let reply = "";
for await (const event of sseEvents(response)) {   // your SSE parser
  const e = JSON.parse(event.data);
  switch (e.type) {
    case "llm.text_delta":    reply += e.data.text; render(reply); break;
    case "tool.executing":    showSpinner(e.data.tool_name); break;
    case "session.completed": render(e.data.final_output); break;
    case "error":             showError(e.data.message); break;
  }
}

Ready-made clients live in examples/frontend/: sse-client.js (vanilla) and use-agent.ts (React hook).

Consuming events in Python

The same StreamEvent objects come out of Agent.stream(...) and AgentEngine.run(...) directly — no SSE involved:

async for event in agent.stream("hello"):
    if event.type == EventType.LLM_TEXT_DELTA:
        print(event.data["text"], end="")

event.to_sse() produces the wire frame above; event.to_dict() the JSON envelope as a dict.


Persistence

Sessions and message history are stored through one async interface, BaseRepository, with three interchangeable backends. Pick by environment:

Backend Class Use for Extra dependency
memory InMemoryRepository Tests, throwaway scripts. Lost on process exit. none
sqlite SQLiteRepository Local dev, single-process apps. One file on disk. python -m pip install --upgrade "fg-agents[sqlite]" (aiosqlite)
postgres PostgresRepository Production, multi-process. python -m pip install --upgrade "fg-agents[postgres]" (SQLAlchemy + asyncpg)

Choosing a backend

Agent facade (memory= argument)

Agent(model=..., memory="memory")                                # default
Agent(model=..., memory="sqlite")                                # ./fg_agents.db
Agent(model=..., memory="sqlite:./my_agent.db")                  # explicit path
Agent(model=..., memory="postgres:postgresql+asyncpg://user:pass@host/db")
Agent(model=..., memory=my_repository)                           # any BaseRepository instance

The postgres form requires the URL after the colon; a bare "postgres" raises ValueError.

Low-level (create_repository)

from fg_agents import create_repository

repo = create_repository("memory")
repo = create_repository("sqlite", db_path="agents.db")     # default path: "fg_agents.db"
repo = create_repository("postgres", db_url="postgresql+asyncpg://user:pass@host/db", echo=False)

Web app (create_app(db_url=...))

create_app picks the backend from the URL string:

Initialization semantics

initialize() creates schema (tables) on SQL backends, so first-run against a fresh database needs no separate migration step. PostgreSQL URLs must use the async driver: postgresql+asyncpg://....

Notes


Web integration

A local service

Install the web extra and a model provider:

python -m pip install --upgrade "fg-agents[web,anthropic]"

Save as app.py:

import os
from fg_agents import AgentDefinition, create_app

assistant = AgentDefinition(
    name="assistant",
    model=os.environ["AGENT_MODEL"],
    system_prompt="Help the user reason about inventory decisions.",
    tools=[],
    max_turns=6,
)
app = create_app(agents={"assistant": assistant}, db_url="memory")
uvicorn app:app --host 127.0.0.1

This is a local, ephemeral demonstration. db_url="memory" avoids a database dependency. Omitting db_url selects a local SQLite file and requires the sqlite extra.

HTTP and streaming

The default prefix is /api/agent. Create a session with POST /sessions, then send messages to POST /sessions/{id}/messages; responses stream as Server-Sent Events. The generated FastAPI OpenAPI schema documents request bodies for the configured application.

Read streaming before writing a frontend parser. A network chunk is not necessarily a complete SSE event. Handle reconnects and errors deliberately.

Authentication for an exposed service

The default router uses unauthenticated, shared-tenant access. admin_only=True restricts certain registration endpoints; it is not a replacement for request authentication.

For an exposed or multi-tenant application, mount create_agent_router(..., authenticator=...) in your own FastAPI app. An authenticator validates a request and returns AuthContext from fg_agents.api.auth, or raises HTTP 401. Derive the tenant from verified credentials, not a user-supplied request body.

HeaderAuthenticator is for a trusted proxy that strips incoming identity headers and sets verified ones. It is not safe to trust those headers from arbitrary clients.

Application lifecycle

Initialize persistence before handling requests and close it during shutdown. Configure CORS for the actual frontend origins, enforce request limits, and ensure session access follows your tenant boundary. See production integration and persistence.


Production integration

The SDK supplies execution components. Your application supplies the trust boundary and operational policy.

Before exposing an agent

External side effects

A tool that sends an order, changes a record or makes a payment needs application-level idempotency. A timeout may leave an external request in progress. Store an operation ID and reconcile its outcome before retrying. Do not treat generic model retries as an exactly-once guarantee.

Multiple workers

Per-session engine locks apply within one engine instance. If several workers can run the same session, route ownership or coordinate explicitly. Database persistence alone does not provide distributed execution serialization.

Limits and observability

Track model calls, tokens, errors, tool outcomes, latency and abandoned streams. Per-turn output limits do not cap total spend. Application budgets should account for retries and delegated agents.

Use deterministic test clients for execution contracts and real-provider tests for transport compatibility. Keep live-provider tests opt-in and avoid putting credentials in recorded fixtures.

Deployment readiness

Pin a release, validate your critical workflows on the built package, rehearse restart and recovery, and retain a rollback version. The documentation describes available interfaces; it does not certify every deployment configuration.


Troubleshooting

Symptom First check
ModelDetectionError Configure a provider or pass model= explicitly
Provider import error Install the relevant provider extra
SQLite import error Install fg-agents[sqlite] or choose memory
Tool never called Definition's tool names, registered schema and prompt
Tool rejected before execution Malformed or truncated arguments; inspect the tool event
Conversation disappears In-memory storage or a different session ID
Streaming stops without final text Error events and exceptions from the iterator
Duplicate external action Retry behavior and application idempotency
Unauthorized shared access Default authenticator; configure the HTTP trust boundary
Missing skill Registered IDs and the definition's skills list

Capture a useful failure

AgentRunError carries the session ID and partial events. Keep the original cause where available. Record the installed version, provider, configured model and a minimal tool schema. Remove credentials and private user data before sharing a reproduction.

Close streams

If you stop reading early, explicitly close the async generator with contextlib.aclosing. Otherwise cleanup may wait for garbage collection. See streaming.

Inspect the actual public surface

Use API reference for the common interfaces and generated reference for exported symbols. Do not infer an implemented feature from a roadmap ticket or an old example.