Skip to content

Level 1: Your First Agent

Build a running symfonic agent in under 10 lines of code. No external services, no API keys, no infrastructure.

Prerequisites

  • Python 3.11 or later
  • pip (or uv, poetry, etc.)

Install

pip install symfonic-core

Your First Agent (10 Lines)

Create a file called my_agent.py:

import asyncio
from symfonic.agent.config import FrameworkConfig
from symfonic.agent.engine import SymfonicAgent
from symfonic.agent.types import FrameworkTenantScope
from symfonic.core.testing import MockModelProvider

async def main():
    agent = SymfonicAgent(
        model_provider=MockModelProvider(response="Hello! I'm your first symfonic agent."),
        config=FrameworkConfig(auto_hydrate=False, auto_consolidate=False),
    )
    scope = FrameworkTenantScope(tenant_id="quickstart")
    response = await agent.run("Hi there!", scope=scope)
    print(response.final_response)
    print(f"Duration: {response.duration_ms:.1f}ms")

asyncio.run(main())

Run it:

python my_agent.py

Output:

Hello! I'm your first symfonic agent.
Duration: 12.3ms

What Just Happened

The 10 lines above wire a complete agent pipeline:

User query
  -> FrameworkTenantScope (tenant isolation)
  -> AgentGraph (state machine with react_loop topology)
  -> ModelProvider (LLM call -- mocked here)
  -> AgentResponse (final_response, duration_ms, node_execution_log)

Each component plays a specific role:

Component Role
SymfonicAgent High-level facade composing graph runtime + memory
FrameworkConfig All configuration in one frozen Pydantic model
FrameworkTenantScope Multi-tenant isolation (every operation is scoped)
MockModelProvider Deterministic test double for the LLM
AgentResponse Structured result with response text, timing, and metadata

The two config flags we disabled:

  • auto_hydrate=False -- Skip memory context injection (no memory backends yet)
  • auto_consolidate=False -- Skip memory extraction after the response

Switch to a Real LLM

Install the Anthropic extra and set your API key:

pip install symfonic-core[anthropic]
export ANTHROPIC_API_KEY="sk-ant-..."

Replace the mock provider with a real one:

import asyncio
from symfonic.agent.config import FrameworkConfig
from symfonic.agent.engine import SymfonicAgent
from symfonic.agent.types import FrameworkTenantScope
from symfonic.core.providers import AnthropicProvider

async def main():
    agent = SymfonicAgent(
        model_provider=AnthropicProvider(),  # uses ANTHROPIC_API_KEY env var
        config=FrameworkConfig(auto_hydrate=False, auto_consolidate=False),
    )
    scope = FrameworkTenantScope(tenant_id="quickstart")
    response = await agent.run("What is dependency injection?", scope=scope)
    print(response.final_response)

asyncio.run(main())

Everything else stays the same. The ModelProvider protocol means you can swap backends (Anthropic, OpenAI, local models) without changing any other code.

Fluent construction with AgentBuilder (9.2+)

AgentBuilder is a chainable facade over the same primitives (FrameworkConfig, SubAgentSpec, HMSFactory) — no new behaviour, just a more readable assembly for agents with several knobs. It's the recommended way to construct anything beyond the two-argument SymfonicAgent(...) form.

import asyncio
from symfonic.agent import AgentBuilder, SubAgentSpec
from symfonic.core.providers import AnthropicProvider
from symfonic.core import symfonic_tool

@symfonic_tool()
def add(a: int, b: int) -> int:
    """Add two integers."""
    return a + b

async def main():
    agent = (
        AgentBuilder()
        .provider(AnthropicProvider())
        .model("claude-sonnet-4-5", temperature=0.2)
        .domain("calc", "A calculator agent.")
        .tools(add)
        .sub_agent_spec(SubAgentSpec(
            name="researcher", description="Deep research.", temperature=0.4,
        ))
        .build()
    )
    async with agent:
        reply = await agent.run("What is 17 + 25?")
        print(reply.final_response)

asyncio.run(main())

Chainable methods (each returns the builder): .provider(), .model(name, temperature=, max_tokens=), .domain(name, description), .tools(*tools), .enable_hms(), .lazy_tooling(), .sub_agent(name, agent=) / .sub_agent_spec(spec), .option(**framework_config_overrides), and .memory(embeddings=…) (builds an in-memory HMS for you). .build() re-validates the assembled FrameworkConfig and returns a ready SymfonicAgent.

Three tiers, same engine — pick the smallest that fits:

Tier Use it when
SymfonicAgent(model_provider=…, config=…) A couple of arguments; the common case
AgentBuilder()....build() Several knobs, sub-agents, or you prefer a fluent chain
Core AgentGraph / AgentRuntime (below) You need the graph runtime without the memory-aware facade

Using the Low-Level Core API

If you want direct access to the graph runtime without the high-level SymfonicAgent wrapper, use the core API:

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():
    config = AgentConfig()
    provider = MockModelProvider(response="Hello from the core API!")
    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 executed: {result['node_execution_log']}")

asyncio.run(main())

The core API gives you full control over dependency injection via BaseAgentDeps and protocol-based service registration.

Choosing Your API Level

API Best for Memory? Streaming?
SymfonicAgent Production apps, full HMS memory, plugins Yes Yes
AgentRuntime Custom graph topologies, fine-grained DI Manual Manual

Most users should start with SymfonicAgent and drop to the core API only when they need custom graph topologies.

Next Steps

Your agent works, but it forgets everything between runs. Level 2 adds persistent memory with Postgres.

Next: Memory Persistence

Ready to ship a real app instead of a script? symfonic init scaffolds a full FastAPI + Postgres + auth + Docker project wired to symfonic-core — see Scaffold a Production App.

For a grouped overview of every feature across the v6.0-v7.0.3 sprint, see the Feature Catalog — 11 capability clusters each with opt-in examples.