Skip to content

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):

pip install "symfonic-core[cli]"
symfonic examples add minimal_agent
python -m minimal_agent

Or, from a source checkout (run from the repo root):

python -m examples.minimal_agent

Expected output:

Response: Hello! I'm a minimal symfonic agent.
Nodes: [...]

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

config = AgentConfig()

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

provider = MockModelProvider(response="Hello! I'm a minimal symfonic agent.")

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

print(f"Response: {result['final_response']}")
print(f"Nodes: {result['node_execution_log']}")

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

See also