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 -- <=50 lines, top-level imports only.
Runs end-to-end with MockModelProvider + InMemoryDocumentStore.
No ANTHROPIC_API_KEY required. Zero external network calls.
Usage: python -m examples.minimal_agent
"""
import asyncio
from symfonic.core import (
AgentConfig,
AgentGraph,
AgentRuntime,
BaseAgentDeps,
ModelProvider,
)
from symfonic.core.documents.in_memory import InMemoryDocumentStore
from symfonic.core.models import Document
from symfonic.core.protocols import DocumentStore
from symfonic.core.testing import MockModelProvider
async def main() -> None:
# All concrete implementations injected at entry point
config = AgentConfig()
provider = MockModelProvider(response="Hello! I'm a minimal symfonic agent.")
doc_store = InMemoryDocumentStore()
await doc_store.write(Document(id="greeting", content="Welcome to symfonic."))
deps = BaseAgentDeps()
deps.register(ModelProvider, provider)
deps.register(DocumentStore, doc_store)
graph = AgentGraph()
runtime = AgentRuntime(graph=graph, deps=deps, config=config)
result = await runtime.run("Hello, agent!")
print(f"Response: {result['final_response']}")
print(f"Nodes: {result['node_execution_log']}")
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