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

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