minimal_agent¶
Level 1 · Fundamentals — the absolute minimum to run an agent end-to-end.
This example wires the four required pieces of the low-level symfonic.core
layer — a config, a model provider, a document store, and a graph — and runs a
single turn. No API key, no network calls: it uses MockModelProvider and
InMemoryDocumentStore.
- Lines: ~42
- Prerequisites: none
- Key concepts:
AgentConfig,AgentGraph,AgentRuntime,BaseAgentDeps,ModelProvider
Run it¶
Installed via pip? Copy this example into your project with the CLI (no checkout needed):
Or, from a source checkout (run from the repo root):
Expected output:
Full code¶
"""Minimal greenfield example -- the smallest thing that runs end to end.
Runs on ``MockModelProvider``. No ANTHROPIC_API_KEY required, zero network.
Usage: python -m examples.minimal_agent
Public surface used, and nothing else::
from symfonic import Agent
from symfonic.capabilities.knowledge import StoredDocument, knowledge_sources
from symfonic.capabilities.prompting import PromptingCapability
The previous version built an ``AgentGraph``, an ``AgentRuntime`` and a
``BaseAgentDeps`` container, registered two implementations against it by
type, and called that minimal in forty-two lines. What it was minimal
*about* was assembly.
The assembly is gone. ``Agent(provider, instructions=...)`` is the whole
agent, and the part worth keeping -- a document the deployment pins into the
prompt -- is one capability rather than a dependency container. The document
store stays a port: two methods, no inheritance, so a dict here and Postgres
in your deployment are the same shape to the framework.
"""
from __future__ import annotations
import asyncio
from symfonic import Agent
from symfonic.capabilities.knowledge import StoredDocument, knowledge_sources
from symfonic.capabilities.prompting import PromptingCapability
from symfonic.core.testing import MockModelProvider
HANDBOOK = {
"greeting": StoredDocument(
document_id="greeting",
title="House greeting",
text="Welcome to symfonic. Answer warmly and briefly.",
)
}
class Handbook:
"""A ``DocumentStore``: one method, nothing inherited.
``None`` means "not here", never "empty" -- a store answering with a
blank document for a missing id makes a deleted document and an empty
one indistinguishable, and the prompt renders a heading over nothing.
"""
def fetch(self, document_id: str) -> StoredDocument | None:
return HANDBOOK.get(document_id)
async def main() -> None:
agent = Agent(
MockModelProvider(response="Hello! I'm a minimal symfonic agent."),
instructions="You are a minimal symfonic agent.",
capabilities=[
PromptingCapability(
sources=list(
knowledge_sources(store=Handbook(), document_ids=["greeting"])
)
)
],
)
result = await agent.run("Hello, agent!")
print(f"Response: {result.text}")
print(f"Duration: {result.duration_ms:.1f}ms")
if __name__ == "__main__":
asyncio.run(main())
Step by step¶
1. Create the config¶
AgentConfig holds the runtime knobs (model selection, token limits, loop
policy). The defaults are enough to run; you override fields only when you need
to. It is immutable — build a new one to change behavior.
2. Pick a model provider¶
ModelProvider is the seam between the framework and an LLM. MockModelProvider
returns a canned string, which makes the example deterministic and free to run.
Swapping in a real model is a one-line change — see
basic_agent and real_agent.
3. Set up a document store¶
doc_store = InMemoryDocumentStore()
await doc_store.write(Document(id="greeting", content="Welcome to symfonic."))
DocumentStore is the persistence protocol the agent reads context from.
InMemoryDocumentStore keeps everything in a dict — perfect for tests and
demos. Every store method is async, so writes are awaited.
4. Register dependencies¶
deps = BaseAgentDeps()
deps.register(ModelProvider, provider)
deps.register(DocumentStore, doc_store)
BaseAgentDeps is the dependency-injection container. The agent's nodes ask for
capabilities by protocol (ModelProvider, DocumentStore), never by
concrete class. This is what lets you substitute mock for real without touching
graph code — the core Dependency-Inversion pattern the framework is built on.
5. Compile the graph and run¶
graph = AgentGraph()
runtime = AgentRuntime(graph=graph, deps=deps, config=config)
result = await runtime.run("Hello, agent!")
AgentGraph defines the topology (the node wiring); AgentRuntime binds that
topology to your deps and config and compiles it into an executable graph.
run() executes one turn and returns a state dict.
6. Read the result¶
final_response is the agent's answer. node_execution_log lists which nodes
fired — useful for understanding the execution path and for debugging.
What to try next¶
- Replace
MockModelProviderwith a real one → basic_agent - Give the agent tools → tool_agent
- Load stored documents into the prompt → memory_agent
See also¶
- The Agent — the runtime model in depth
- Architecture — the full module map