basic_agent¶
Level 1 · Fundamentals — the high-level SymfonicAgent in under 10 lines.
Where minimal_agent wires the low-level symfonic.core
primitives by hand, this example uses the symfonic.agent orchestration layer.
SymfonicAgent composes the graph, the 5-layer memory system, and the model
provider for you — you construct it, call run(), and read a typed
AgentResponse.
- Lines: ~43
- Prerequisites: none to run as-is (uses
MockModelProvider); a real provider needspip install symfonic-core[anthropic]+ an API key - Key concepts:
SymfonicAgent,FrameworkConfig,FrameworkTenantScope,AgentResponse
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 basic_agent
python basic_agent/basic_agent.py
Or, from a source checkout (run from the repo root):
Expected output:
Full code¶
"""An agent that remembers, in about ten lines.
Usage:
python -m examples.agent.basic_agent
Requires:
pip install symfonic-core[anthropic]
export ANTHROPIC_API_KEY=sk-... # only for the real provider
Public surface used, and nothing else::
from symfonic import Agent
from symfonic.agent import FrameworkTenantScope
from symfonic.capabilities.memory import GraphBackedHms, memory_capabilities
from symfonic.capabilities.prompting import PromptingCapability
from symfonic.memory.backends import InMemoryGraphBackend
The previous version of this file was titled "multi-memory agent" and turned
memory off -- ``auto_hydrate``, ``auto_consolidate`` and ``lazy_tooling`` all
False, because nothing registered a layer for them to use. It demonstrated
construction.
Composing two capabilities is what makes the title true. ``memory_capabilities``
records and recalls against a store; ``PromptingCapability`` is what renders a
recollection into the next prompt. Either alone is a no-op you cannot see:
without prompting the agent stores perfectly and the model reads none of it.
The scope is the isolation boundary and it is passed, not discovered. Every
read and write below is checked against it.
"""
from __future__ import annotations
import asyncio
from symfonic import Agent
from symfonic.agent import FrameworkTenantScope
from symfonic.capabilities.memory import GraphBackedHms, memory_capabilities
from symfonic.capabilities.prompting import PromptingCapability
from symfonic.core.testing import MockModelProvider
from symfonic.memory.backends import InMemoryGraphBackend
def build(store: GraphBackedHms, scope: FrameworkTenantScope) -> Agent:
"""One agent, bound to one tenant, that remembers its own turns."""
# Swap MockModelProvider for AnthropicProvider() and nothing else changes:
# from symfonic.core.providers import AnthropicProvider
return Agent(
MockModelProvider(response="You mentioned the walnut allergy."),
instructions="You are a helpful assistant. Be brief.",
capabilities=[
*memory_capabilities(store, scope, limit=5),
PromptingCapability(sources=[]),
],
)
async def main() -> None:
store = GraphBackedHms(InMemoryGraphBackend())
scope = FrameworkTenantScope.root("org", "demo-tenant")
await build(store, scope).run("Remember that I am allergic to walnuts.")
second = await build(store, scope).run("What do you remember?")
print(f"Response: {second.text}")
print(f"Duration: {second.duration_ms:.1f}ms")
# No token count here on purpose: MockModelProvider reports none, and
# an example printing "0 tokens" teaches that accounting is broken.
# Rung 9 (journey_observability) measures spend with a provider that
# reports real usage.
# Two agents, one store: the second turn recalled what the first was told,
# and nothing in this file retrieved it. The capability did.
if __name__ == "__main__":
asyncio.run(main())
Step by step¶
1. Choose a provider¶
Same ModelProvider seam as minimal_agent. To go live,
swap the two commented lines:
from symfonic.core.providers import AnthropicProvider
provider = AnthropicProvider() # reads ANTHROPIC_API_KEY from env
Nothing else on the page changes — that is the point of the provider protocol.
2. Construct the agent¶
agent = SymfonicAgent(
model_provider=provider,
config=FrameworkConfig(auto_hydrate=False, auto_consolidate=False),
)
SymfonicAgent is the batteries-included orchestrator. FrameworkConfig layers
framework behavior (memory hydration, consolidation, lazy loading) on top of the
core AgentConfig. Here auto_hydrate=False and auto_consolidate=False keep
the demo self-contained — no memory backends are read or written. Flip them on
once you attach real stores.
3. Define a tenant scope¶
Every run is scoped to a tenant. FrameworkTenantScope is how the framework
isolates one customer's memory, documents, and skills from another's. Multi-tenant
isolation is a first-class concept — see
Hierarchical Tenant Scope.
4. Run and read the typed response¶
response = await agent.run("What do you remember?", scope=scope)
print(f"Response: {response.final_response}")
print(f"Run ID: {response.run_id}")
print(f"Duration: {response.duration_ms:.1f}ms")
print(f"Memory entries used: {response.memory_entries_used}")
Unlike the low-level runtime (which returns a raw state dict), SymfonicAgent.run
returns a typed AgentResponse. Beyond final_response it carries observability
fields (run_id, duration_ms, memory_entries_used) and — when you ask for it
— a validated structured payload. See
Structured Output.
Low-level vs high-level¶
minimal_agent (core) |
basic_agent (agent) |
|
|---|---|---|
| Entry type | AgentRuntime |
SymfonicAgent |
| You wire | graph, deps, stores | just the provider |
| Memory layers | none by default | 5-layer HMS available |
| Return | state dict |
typed AgentResponse |
| Best for | learning internals, custom graphs | building applications |
What to try next¶
- Add persistent memory and turn
auto_hydrateon → memory_agent - Delegate work to child agents → sub_agents
- Ask for a validated object back → Structured Output