deepcrew-ai v0.3.0
Ten major features across two releases — a self-improving loop (verifier, adaptive budget, branching, skill distillation, procedural memory) plus bounded nested agent spawning. Every feature is additive — your v0.1.0 code runs unchanged.
Looping
Outer iteration loop for search-refine patterns. Now also: adaptive budget and branching.
Verifier v0.2.1
Structured, LLM-graded critique — score, issues, and a suggestion — drives targeted refinement.
Procedural Memory v0.2.2
ACE-inspired evolving playbook — agents accumulate what worked (and what to avoid) across runs.
APEX Synthesizer
Confidence-scored, citation-aware result synthesis. Know how sure your AI is.
Agent Spawning
Claude Code-style spawning, now with bounded nested delegation and smart tool allocation.
Skills
Reusable capability bundles, now self-evolving from converged loop runs.
Memory Providers
Pluggable context stores auto-injected into each LLM call. InMemory and File backends.
Retry & Fallback
Per-agent exponential backoff with model fallback chains. Never fail on transient errors.
Observability
OpenTelemetry spans for every LLM call, tool execution, and workflow step.
CLI
deepcrew run workflow.yaml — declarative workflow execution from the terminal.
APEX Synthesizer
APEX replaces the original plain synthesizer with an intelligent synthesis engine. It produces a confidence score (0.0–1.0) on every result, optionally cites which agent contributed each fact, and can even call tools mid-synthesis for verification.
How it works
- All agent results are collected after parallel execution
- APEX receives the original query + all agent outputs in a structured prompt
- It synthesizes a unified answer and ends its response with
CONFIDENCE: 0.85 - The confidence line is parsed out and stored on
AgentResult.confidence - When
cite_sources=True, APEX adds[source: agent_name]inline markers
Basic usage
from deepcrew import Agent, Orchestrator, ApexConfig
orch = Orchestrator(
agents=[
Agent("researcher", model="openai/gpt-4o-mini", system_prompt="Research facts."),
Agent("analyst", model="anthropic/claude-haiku-4-5-20251001", system_prompt="Analyze data."),
],
router_model="openai/gpt-4o-mini",
apex_model="openai/gpt-4o",
apex_config=ApexConfig(
cite_sources=True, # [source: researcher] / [source: analyst] inline
confidence_threshold=0.75, # warn if below (future: request more agents)
allow_tools=False, # APEX can call tools mid-synthesis (experimental)
),
)
result = await orch.run("What causes inflation?")
print(result.final_text)
print(f"Confidence: {result.agent_results[-1].confidence:.2%}")
Standalone APEX
from deepcrew import APEXSynthesizer, ApexConfig, AgentResult
# Use APEX outside of Orchestrator — e.g., synthesize cached results
apex = APEXSynthesizer(
model="openai/gpt-4o",
config=ApexConfig(cite_sources=True),
)
results: list[AgentResult] = [...] # your pre-computed results
synthesis = await apex.synthesize(
original_query="Explain quantum entanglement",
results=results,
)
print(synthesis.text)
print(f"Confidence: {synthesis.confidence:.2f}")
# Citations
for citation in apex.build_citations(results, synthesis.text):
print(f"[{citation.agent_id}] {citation.claim[:80]}")
ApexConfig reference
EventType.APEX_DONE includes a below_threshold=True flag for consumers to handle.[source: agent_name] inline markers to attribute facts to specific agents.tool_defs to synthesize().CONFIDENCE: float.APEX events
from deepcrew.types import EventType
async for event in orch.stream("..."):
if event.event == EventType.APEX_START:
agents = event.data["agents"]
print(f"APEX synthesizing from {len(agents)} agents: {agents}")
elif event.event == EventType.APEX_DONE:
conf = event.data["confidence"]
print(f"APEX done — confidence {conf:.2f}")
if conf < 0.7:
print("⚠ Low confidence — consider adding more specialist agents")
Agent Spawning
deepcrew-ai v0.2.0 introduces Claude Code-style dynamic agent spawning. Any running agent can call a built-in spawn_agent tool to create a sub-agent mid-loop, with tools automatically selected from a global pool by the ToolAllocator.
How it works
- You provide a
global_toolspool toOrchestrator - With
enable_spawn=True, every agent gets aspawn_agent(task, tools, model)tool injected - When an agent calls
spawn_agent,ToolAllocatoruses the router LLM to pick the most relevant tools from the global pool for that specific task - A fresh sub-agent is created and runs to completion, its result returned to the parent
- A
SPAWN_AGENTstream event is emitted for observability
Enable spawning via Orchestrator
from deepcrew import Agent, Orchestrator, tool
@tool
def search_web(query: str) -> str:
"Search the web."
...
@tool
def read_file(path: str) -> str:
"Read a local file."
...
@tool
def run_sql(query: str) -> list[dict]:
"Execute a SQL query."
...
@tool
def call_api(url: str, method: str = "GET") -> dict:
"Make an HTTP API call."
...
master = Agent(
name="coordinator",
model="openai/gpt-4o",
system_prompt="""You are a coordinator agent. For complex subtasks,
use the spawn_agent tool to delegate to a specialized sub-agent.""",
)
orch = Orchestrator(
agents=[master],
router_model="openai/gpt-4o-mini",
apex_model="openai/gpt-4o",
global_tools=[search_web, read_file, run_sql, call_api], # pool
enable_spawn=True, # injects spawn_agent tool into master
)
result = await orch.run(
"Research the top 3 AI papers from last month and summarize key findings."
)
print(result.final_text)
Standalone spawning
from deepcrew import spawn_agent, SpawnRequest
request = SpawnRequest(
task="Find all Python files that import pandas and list their names.",
tools=["read_file", "search_web"], # hint: names from global pool
model="openai/gpt-4o-mini",
system_prompt="You are a code analysis assistant.",
max_turns=5,
)
result = await spawn_agent(
request=request,
all_tool_defs=[search_web_def, read_file_def],
parent_queue=my_queue,
router_model="openai/gpt-4o-mini",
parent_agent_id="coordinator",
)
print(result.text)
ToolAllocator
from deepcrew import ToolAllocator
allocator = ToolAllocator(router_model="openai/gpt-4o-mini")
# Given a task description and a large pool of tools,
# returns only the most relevant subset (up to max_tools)
relevant_tools = await allocator.allocate(
task="Analyze sentiment in customer reviews and generate a report",
all_tools=my_tool_defs, # list[ToolDef] — could be dozens of tools
max_tools=5, # return at most 5
)
print([t.name for t in relevant_tools])
ToolAllocator prompt includes each tool's name and description. A good tool description dramatically improves allocation accuracy.SpawnRequest reference
spawn_agent tool wrapper; rarely set by hand.How-to: bounded nested spawning v0.2.6
A spawned sub-agent can itself spawn further sub-agents — useful when a delegated sub-task is still too large to handle directly. This is strictly depth-bounded, never sibling/fan-out-bounded: each level can still spawn as many sub-agents as it wants, but nesting depth is capped by max_spawn_depth. Below that hard cap, an optional spawn_complexity_check gate can skip attaching a nested spawn tool when the sub-task doesn't look worth decomposing further.
from deepcrew import Agent, Orchestrator, Verifier
orch = Orchestrator(
agents=[master_agent],
global_tools=[search_web, read_file],
enable_spawn=True,
max_spawn_depth=3, # up to 3 levels of nested delegation
spawn_complexity_check=Verifier(), # optional: skip nesting for simple sub-tasks
)
When a sub-agent tries to spawn beyond max_spawn_depth, it simply has no spawn_agent tool available — nothing to invoke, so it completes the task directly instead. A defense-in-depth check inside the tool itself also returns "Maximum nesting depth reached; complete this task directly without further delegation." as a plain string result for any caller that bypasses the normal attach logic — never an exception.
Looping Methodology
The outer iteration loop is distinct from the inner per-turn max_turns cycle. The loop runs the entire agent (including all its tool calls) and re-runs it if the result doesn't meet a convergence criterion — ideal for search-refine, draft-critique, and iterative research patterns.
How it works
- Agent runs (all inner turns until no more tool calls)
convergence_fn(result)is called — if it returnsTrue, the loop exits- If not converged,
refine_promptis appended and the agent runs again - Loop exits when
max_iterationsis reached or convergence is achieved result.loop_iterationsrecords how many outer iterations ran
Basic loop with convergence
from deepcrew import Agent, run_agent, LoopConfig, tool
@tool
def search_web(query: str) -> str:
"Search the web for information."
...
agent = Agent(
name="researcher",
model="openai/gpt-4o-mini",
system_prompt="You are a thorough researcher. Use search to gather comprehensive information.",
tools=[search_web],
loop_config=LoopConfig(
max_iterations=4,
# Convergence: result is long enough to be a proper answer
convergence_fn=lambda r: len(r.text) > 800 and r.text.count("\n") > 5,
# What to say to the agent if not converged
refine_prompt="Your answer is incomplete. Search for more details and expand your response significantly.",
),
)
result = await run_agent(
agent,
[{"role": "user", "content": "Explain the mechanism of CRISPR-Cas9 gene editing"}],
)
print(result.text)
print(f"Iterations: {result.loop_iterations}")
Verifier-driven refinement
A Verifier grades each iteration's answer with structured feedback — a score, specific issues, and a suggestion — instead of a boolean, and drives a targeted refinement prompt. See the full Verifier feature guide for a showcase of usage patterns, from a basic quality gate to a fully custom grading function.
run_agent_loop() directly
from deepcrew import run_agent_loop, LoopConfig
result = await run_agent_loop(
agent=my_agent,
messages=[{"role": "user", "content": "Draft an executive summary"}],
tool_defs=None,
queue=my_queue,
agent_id="drafter",
)
search_loop() — confidence-based iteration
from deepcrew import search_loop, Agent, tool
@tool
def search_web(query: str) -> str:
"Search the web."
...
agent = Agent("searcher", model="openai/gpt-4o-mini",
system_prompt="You are a research agent.", tools=[search_web])
# Keeps searching until APEX confidence >= 0.8 or 3 iterations
result = await search_loop(
query="What is the current state of nuclear fusion research?",
search_tool=search_web,
agent=agent,
max_iterations=3,
confidence_threshold=0.8,
)
Stop condition (early exit)
from deepcrew import LoopConfig, LoopConvergedError
def check_done(result):
# Raise LoopConvergedError to stop immediately and return result
if "FINAL ANSWER:" in result.text:
raise LoopConvergedError(result)
return False
agent = Agent("reasoner", model="openai/gpt-4o",
system_prompt="When you have a final answer, prefix it with 'FINAL ANSWER:'.",
loop_config=LoopConfig(
max_iterations=6,
convergence_fn=check_done,
))
LoopConfig reference
AgentResult. Return True to stop. Raise LoopConvergedError(result) for immediate exit with that result.LoopConvergedError on its own — useful for externalizing early-exit logic."Your answer is incomplete. Please search for more information and expand your response."VerifierFeedback (score + issues + suggestion) drives both convergence and the next refinement prompt, replacing the static refine_prompt text.verifier to be set too — see the Procedural Memory guide.procedural_memory. Defaults to agent.name.verifier. Never exceeds max_iterations.verifier score, or merged via APEXSynthesizer when no verifier is set. Multiplies LLM calls per iteration.Skill registered in SkillRegistry — see the Skills guide. Never triggers on plain max_iterations exhaustion.AgentResult.confidence) required to distill a skill.How-to: self-consistency branching v0.2.4
Instead of one linear refinement path, run several candidate continuations per iteration in parallel and keep the best — a lightweight tree-search/self-consistency pattern. Each parallel call already samples independently from the model, so branches naturally diverge without any extra seeding logic. This costs branches× the LLM calls per iteration, so pair it with a low max_iterations.
from deepcrew import Agent, run_agent, LoopConfig, Verifier, VerifierConfig
agent = Agent(
name="researcher",
model="openai/gpt-4o-mini",
tools=[search_web],
loop_config=LoopConfig(
max_iterations=3,
verifier=Verifier(VerifierConfig(threshold=0.85)),
branches=3, # 3x the LLM calls per iteration, in exchange for picking the best
),
)
result = await run_agent(agent, [{"role": "user", "content": "Explain CRISPR"}])
Without a verifier, branching still works — the branches candidates are merged into one cohesive answer via the same APEXSynthesizer used for multi-agent orchestration, rather than picking a single winner.
Loop events
from deepcrew.types import EventType
while True:
event = await queue.get()
if event is None: break
if event.event == EventType.LOOP_ITERATION:
i = event.data["iteration"]
converged = event.data["converged"]
print(f"Loop iteration {i} — {'converged' if converged else 'refining...'}")
elif event.event == EventType.VERIFIER_SCORED: # v0.2.1
print(f"Verifier score: {event.data['score']} — issues: {event.data['issues']}")
elif event.event == EventType.BRANCH_SELECTED: # v0.2.4
print(f"Branch {event.data['winning_index']} won with score {event.data['winning_score']}")
elif event.event == EventType.SKILL_EXTRACTED: # v0.2.5
print(f"Distilled new skill: {event.data['skill_name']} (score {event.data['score']})")
Verifier
A Verifier grades an agent's result against the original query and returns structured feedback — a score, specific issues, and a concrete suggestion — instead of a plain boolean. Attached to a LoopConfig, it drives both convergence and a targeted refinement prompt built from its critique, replacing the static default refine message. This page is a showcase of the ways to use it, end to end.
How-to: basic quality gate
The simplest use — stop refining once the built-in LLM grader is confident enough.
from deepcrew import Agent, run_agent, LoopConfig, Verifier, VerifierConfig
agent = Agent(
name="researcher",
model="openai/gpt-4o-mini",
tools=[search_web],
loop_config=LoopConfig(
max_iterations=4,
verifier=Verifier(VerifierConfig(threshold=0.85)),
),
)
result = await run_agent(agent, [{"role": "user", "content": "Explain CRISPR"}])
print(result.text)
How-to: a task-specific rubric
Pass grading criteria specific to your domain so the verifier checks for what actually matters — e.g. code review, not just "is this a complete sentence."
code_reviewer = Agent(
name="code_reviewer",
model="openai/gpt-4o",
system_prompt="Review the given diff for bugs, security issues, and style.",
loop_config=LoopConfig(
max_iterations=3,
verifier=Verifier(VerifierConfig(
threshold=0.9,
rubric=(
"1. Every changed function must be covered by the review.\n"
"2. Security issues (injection, auth, secrets) must be called out explicitly.\n"
"3. Style nits are optional but bugs are not."
),
)),
),
)
result = await run_agent(code_reviewer, [{"role": "user", "content": diff_text}])
How-to: fully custom grading (no LLM call)
evaluate_fn replaces the built-in LLM grader entirely — useful when you have a deterministic check (schema validation, a unit test, a regex) that's cheaper and more reliable than asking another model.
import json
from deepcrew import VerifierFeedback
async def json_schema_grader(query: str, result) -> VerifierFeedback:
try:
data = json.loads(result.text)
except json.JSONDecodeError:
return VerifierFeedback(score=0.0, issues=["Output is not valid JSON"], suggestion="Return valid JSON only.")
missing = [k for k in ("summary", "action_items") if k not in data]
if missing:
return VerifierFeedback(score=0.4, issues=[f"Missing key: {k}" for k in missing], suggestion="Include all required keys.")
return VerifierFeedback(score=1.0, converged=True)
agent = Agent(
name="extractor",
model="openai/gpt-4o-mini",
loop_config=LoopConfig(
max_iterations=3,
verifier=Verifier(VerifierConfig(evaluate_fn=json_schema_grader)),
),
)
How-to: adaptive compute budget v0.2.3
By default the loop always runs max_iterations times unless it converges early. With adaptive=True, it also tracks the verifier score across iterations and stops as soon as improvement plateaus — saving compute once refinement stops paying off. max_iterations is still a hard ceiling; adaptive can only shorten the loop, never lengthen it.
agent = Agent(
name="researcher",
model="openai/gpt-4o-mini",
tools=[search_web],
loop_config=LoopConfig(
max_iterations=8,
verifier=Verifier(VerifierConfig(threshold=0.9)),
adaptive=True,
min_improvement=0.02, # minimum score delta to still count as "improving"
plateau_patience=2, # stop after this many non-improving iterations in a row
),
)
result = await run_agent(agent, [{"role": "user", "content": "Explain CRISPR"}])
print(f"Stopped after {result.loop_iterations} iterations (cap was 8)")
When the loop stops early on a plateau, it returns the highest-scoring result seen so far — not necessarily the very last one — and emits a LOOP_ITERATION event with {"early_stop": "plateau"} in its data. Adaptive is a no-op without a verifier configured (there's no score to track).
Verifier reference
VerifierFeedback.converged to be True.(query, result) -> VerifierFeedback function that replaces the built-in LLM grader entirely.score >= threshold.See Looping for how LoopConfig.verifier integrates with convergence and refinement, and Procedural Memory for how verifier feedback also feeds the evolving playbook.
Skills
Skills are higher-level capability bundles. They look identical to tools from the LLM's perspective (both become ToolDef), but can wrap multi-step logic, sub-agents, or external APIs internally. Three built-ins are included; you can also create custom skills with the @skill decorator.
Built-in skills
from deepcrew import Agent, run_agent
from deepcrew import WebSearchSkill, SummarizeSkill, CodeExecutionSkill
agent = Agent(
name="assistant",
model="openai/gpt-4o",
system_prompt="You are a versatile AI assistant.",
skills=[
WebSearchSkill(), # DuckDuckGo Instant Answer API
SummarizeSkill(model="openai/gpt-4o-mini"), # LLM-backed summarization
CodeExecutionSkill(timeout=15.0), # sandboxed Python subprocess
],
)
result = await run_agent(agent, [
{"role": "user", "content": "Search for Python async best practices, summarize them, then write a demo script and run it."}
])
print(result.text)
@skill decorator — custom skills
from deepcrew import skill, Agent
@skill(name="translate", description="Translate text to another language")
async def translate(text: str, target_language: str) -> str:
"""
Args:
text (str): The text to translate.
target_language (str): Target language code, e.g. 'es', 'fr', 'ja'.
"""
import httpx
async with httpx.AsyncClient() as client:
r = await client.post(
"https://libretranslate.de/translate",
json={"q": text, "source": "auto", "target": target_language},
)
return r.json()["translatedText"]
@skill(name="send_slack", description="Send a message to a Slack channel")
async def send_slack(channel: str, message: str) -> str:
"""
Args:
channel (str): Slack channel name (without #).
message (str): Message text to send.
"""
import os
import httpx
async with httpx.AsyncClient() as client:
await client.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"},
json={"channel": channel, "text": message},
)
return f"Message sent to #{channel}"
agent = Agent(
"comms",
model="openai/gpt-4o",
system_prompt="Help with communications and translations.",
skills=[translate, send_slack],
)
Skill class — full custom implementation
from deepcrew.skills.base import Skill
class DatabaseQuerySkill(Skill):
name = "database_query"
description = "Execute a read-only SQL query against the production database"
parameters = {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "SQL SELECT query to execute"},
"limit": {"type": "integer", "description": "Max rows to return", "default": 100},
},
"required": ["sql"],
}
def __init__(self, connection_string: str):
self._conn_str = connection_string
async def execute(self, sql: str, limit: int = 100) -> str:
import asyncpg
conn = await asyncpg.connect(self._conn_str)
try:
rows = await conn.fetch(f"{sql} LIMIT {limit}")
return str([dict(r) for r in rows])
finally:
await conn.close()
# Use it:
agent = Agent("db_agent", model="openai/gpt-4o",
skills=[DatabaseQuerySkill("postgresql://...")])
SkillRegistry
from deepcrew import SkillRegistry
from deepcrew import WebSearchSkill, SummarizeSkill
# Register globally
SkillRegistry.register(WebSearchSkill())
SkillRegistry.register(SummarizeSkill())
# Retrieve by name
search = SkillRegistry.get("web_search")
summarize = SkillRegistry.get("summarize")
# List all registered skills
for skill in SkillRegistry.list_all():
print(f"{skill.name}: {skill.description}")
Built-in skill reference
| Class | Tool name | Description | Config |
|---|---|---|---|
WebSearchSkill | web_search | DuckDuckGo Instant Answer API. Returns top results. | None |
SummarizeSkill | summarize | LLM-backed text summarization. | model="openai/gpt-4o-mini" |
CodeExecutionSkill | execute_code | Runs Python in an isolated subprocess. | timeout=10.0 |
How-to: self-evolving skills (auto_extract_skill) v0.2.5
With LoopConfig.auto_extract_skill=True, a loop run that genuinely converges (via convergence_fn or verifier) with a quality signal at or above skill_confidence_threshold is distilled into a reusable, replayable Skill and registered in SkillRegistry — Voyager-style. The distilled skill doesn't just memoize the one answer; it re-runs the original agent's system_prompt/tools/mcps against whatever new task text it's called with, so it generalizes to similar future tasks. This never triggers on plain max_iterations exhaustion without real convergence, and is off by default.
from deepcrew import Agent, run_agent, LoopConfig, Verifier, VerifierConfig, SkillRegistry
researcher = Agent(
name="researcher",
model="openai/gpt-4o-mini",
tools=[search_web],
loop_config=LoopConfig(
max_iterations=4,
verifier=Verifier(VerifierConfig(threshold=0.85)),
auto_extract_skill=True,
skill_confidence_threshold=0.85,
),
)
result = await run_agent(researcher, [{"role": "user", "content": "Explain CRISPR"}])
# Later, a completely different agent can reuse the distilled skill by name.
distilled = [s for s in SkillRegistry.list_all() if s.name.startswith("researcher_")][0]
writer = Agent(name="writer", model="openai/gpt-4o-mini", skills=[distilled])
Listen for EventType.SKILL_EXTRACTED ({"skill_name": ..., "score": ...}) to know when a new skill was registered. See Looping and Verifier for how convergence and scoring feed into this.
Memory Providers
Memory providers let agents maintain context across turns, runs, and even restarts. They auto-inject into the LLM call (relevant memories added as a system message), and auto-store tool results for future retrieval.
InMemoryProvider — short-term context
from deepcrew import Agent, run_agent, InMemoryProvider
memory = InMemoryProvider()
agent = Agent(
name="assistant",
model="openai/gpt-4o-mini",
system_prompt="You are a helpful assistant with memory.",
memory=memory,
)
# First interaction
result1 = await run_agent(agent, [
{"role": "user", "content": "My name is Alice and I'm building a Python library."}
])
# Memory automatically stores tool results and LLM context
# Second interaction — agent will remember Alice's project
result2 = await run_agent(agent, [
{"role": "user", "content": "What was my project about again?"}
])
FileMemoryProvider — persistent context
from pathlib import Path
from deepcrew import Agent, run_agent, FileMemoryProvider
# Persists across process restarts
memory = FileMemoryProvider(Path.home() / ".deepcrew" / "my_agent_memory.json")
agent = Agent(
name="persistent_bot",
model="openai/gpt-4o-mini",
memory=memory,
)
# All tool results are atomically written to the JSON file
# On next startup, memories are loaded and injected into context
Custom MemoryProvider
from deepcrew.memory.base import MemoryProvider
class RedisMemoryProvider(MemoryProvider):
"""Redis-backed memory for distributed agent deployments."""
def __init__(self, redis_url: str, ttl: int = 3600):
import redis.asyncio as redis
self._client = redis.from_url(redis_url)
self._ttl = ttl
async def store(self, key: str, value: str) -> None:
await self._client.setex(key, self._ttl, value)
async def retrieve(self, key: str) -> str | None:
val = await self._client.get(key)
return val.decode() if val else None
async def search(self, query: str, top_k: int = 5) -> list[str]:
# Simple prefix scan — production should use semantic search
keys = await self._client.keys(f"*{query[:20]}*")
results = []
for key in keys[:top_k]:
val = await self._client.get(key)
if val:
results.append(val.decode())
return results
async def clear(self) -> None:
await self._client.flushdb()
agent = Agent("distributed", model="openai/gpt-4o",
memory=RedisMemoryProvider("redis://localhost:6379"))
Memory events
from deepcrew.types import EventType
async for event in run_agent(agent, messages, queue=queue):
if event.event == EventType.MEMORY_RETRIEVE:
n = event.data["count"]
print(f"Injected {n} memories into context")
elif event.event == EventType.MEMORY_STORE:
key = event.data["key"]
print(f"Stored tool result to memory: {key}")
MemoryProvider ABC
class MemoryProvider(ABC):
@abstractmethod
async def store(self, key: str, value: str) -> None: ...
@abstractmethod
async def retrieve(self, key: str) -> str | None: ...
@abstractmethod
async def search(self, query: str, top_k: int = 5) -> list[str]: ...
@abstractmethod
async def clear(self) -> None: ...
Procedural memory (evolving playbook)
ProceduralMemory is an opt-in, durable "the system learns from its own past runs" store, built on top of any MemoryProvider. See the full Procedural Memory feature guide for a showcase of usage patterns, from a single agent that gets smarter over time to sharing one playbook across a whole agent pool.
Procedural Memory
ProceduralMemory is an opt-in, durable "the system learns from its own past runs" store, inspired by ACE (Agentic Context Engineering, ICLR 2026). It's built on top of any MemoryProvider as its backing store and adds structure: each entry is a "helpful" or "harmful" bullet with a usage count and last-seen score. It's read on every run of an agent it's attached to (looped or single-shot), and curated — incrementally merged, never wholesale rewritten — whenever a loop with a Verifier converges. This page is a showcase of the ways to use it, end to end.
How-to: a research agent that gets smarter over time
Same agent, same task type, run repeatedly — each run's high-confidence result and each failure's specific issue become durable strategy bullets injected into the next run's context.
from deepcrew import (
Agent, run_agent, LoopConfig, Verifier, VerifierConfig,
FileMemoryProvider, ProceduralMemory,
)
playbook = ProceduralMemory(FileMemoryProvider("playbook.json"), max_entries=30)
agent = Agent(
name="researcher",
model="openai/gpt-4o-mini",
tools=[search_web],
procedural_memory=playbook, # read on every run, even single-shot
loop_config=LoopConfig(
max_iterations=4,
verifier=Verifier(VerifierConfig(threshold=0.85)),
procedural_memory=playbook, # curated after a converged loop
),
)
result = await run_agent(agent, [{"role": "user", "content": "Explain CRISPR"}])
# Run the same agent again later (even a new process, with FileMemoryProvider) and
# it will already know what worked and what to avoid for this task.
Curation requires a verifier on the same LoopConfig — without one, procedural_memory there is a no-op (there's no VerifierFeedback to grade the run against). Reading the playbook (Agent.procedural_memory) works independently of looping.
How-to: a shared playbook across an agent pool
Multiple agents that handle the same kind of task (e.g. every "support_triage" agent spawned by an Orchestrator) can share one ProceduralMemory instance and namespace it with an explicit task_tag instead of the default (which is keyed by agent.name), so lessons pool together regardless of which specific agent instance ran.
shared_playbook = ProceduralMemory(FileMemoryProvider("support_playbook.json"))
def make_support_agent(name: str) -> Agent:
return Agent(
name=name,
model="openai/gpt-4o-mini",
procedural_memory=shared_playbook,
loop_config=LoopConfig(
verifier=Verifier(VerifierConfig(threshold=0.8)),
procedural_memory=shared_playbook,
task_tag="support_triage", # shared namespace, not tied to agent.name
),
)
agent_a = make_support_agent("triage_shift_1")
agent_b = make_support_agent("triage_shift_2")
# Both read from and write to the same "support_triage" playbook.
How-to: inspect the playbook directly
You don't need to run an agent to read or seed a playbook — ProceduralMemory is usable standalone for debugging, exporting, or manual curation.
entries = await playbook.load("researcher")
for e in entries:
print(f"[{e.kind}] {e.content} (used {e.uses}x, last score {e.last_score})")
print(playbook.render(entries)) # the exact text block injected into context
ProceduralMemory reference
MemoryProvider as the backing store; caps the playbook at max_entries, pruned by usage/score.Playbook events
from deepcrew.types import EventType
async for event in run_agent_loop(agent, messages, queue=queue):
if event.event == EventType.PLAYBOOK_UPDATED:
print(f"Playbook now has {event.data['entry_count']} entries")
See Verifier for how VerifierFeedback is produced, and Looping for the full outer-loop lifecycle this plugs into.
Retry & Fallback Policies
Configure per-agent retry behavior with exponential backoff, and model fallback chains that activate when all retries fail.
Basic retry
from deepcrew import Agent, RetryPolicy
agent = Agent(
name="resilient",
model="openai/gpt-4o",
system_prompt="Be helpful.",
retry_policy=RetryPolicy(
max_retries=3, # try up to 3 more times after the first failure
backoff_seconds=1.0, # base wait between retries
exponential=True, # 1s, 2s, 4s, 8s ... (doubles each time)
retry_on=(Exception,), # retry on any exception (default)
),
)
Retry specific exceptions only
import litellm
from deepcrew import RetryPolicy
# Only retry on rate limit and connection errors
agent = Agent(
"selective_retry",
model="openai/gpt-4o",
retry_policy=RetryPolicy(
max_retries=5,
backoff_seconds=2.0,
retry_on=(
litellm.RateLimitError,
litellm.APIConnectionError,
TimeoutError,
),
),
)
Fallback chain
from deepcrew import Agent, RetryPolicy, FallbackChain
agent = Agent(
name="fault_tolerant",
model="openai/gpt-4o", # primary model
retry_policy=RetryPolicy(
max_retries=2,
backoff_seconds=1.0,
),
fallback_chain=FallbackChain(models=[
"anthropic/claude-haiku-4-5-20251001", # try first if gpt-4o fails all retries
"gemini/gemini-2.0-flash", # try second
"ollama/llama3.2", # local fallback
]),
)
# Flow: gpt-4o → retry 1 → retry 2 → claude-haiku → retry 1 → retry 2 → gemini → ...
Retry events
from deepcrew.types import EventType
while True:
event = await queue.get()
if event is None: break
if event.event == EventType.RETRY_ATTEMPT:
data = event.data
print(f"Retry {data['attempt']} on {data['model']} — waiting {data['delay']:.1f}s")
elif event.event == EventType.FALLBACK_TRIGGERED:
print(f"Falling back from {event.data['from_model']} → {event.data['to_model']}")
RetryPolicy reference
max_retries=3 means up to 4 total calls per model.exponential=True, this doubles each retry.litellm.RateLimitError to avoid retrying logic errors.Observability (OpenTelemetry)
deepcrew-ai emits OpenTelemetry spans for every LLM call, tool execution, and workflow step. When observability=None (the default), all span context managers are nullcontext() — absolutely zero overhead.
Installation
pip install "deepcrew-ai[otel]"
# Installs: opentelemetry-api, opentelemetry-sdk, opentelemetry-exporter-otlp
Quick start with Jaeger
from deepcrew import Agent, Orchestrator, ObservabilityConfig, ApexConfig
obs = ObservabilityConfig(
otel_endpoint="http://localhost:4317", # gRPC endpoint
service_name="my-ai-app",
enabled=True,
export_format="grpc", # or "http" for HTTP/protobuf
)
orch = Orchestrator(
agents=[
Agent("researcher", model="openai/gpt-4o-mini", system_prompt="Research."),
Agent("writer", model="anthropic/claude-haiku-4-5-20251001", system_prompt="Write."),
],
apex_model="openai/gpt-4o",
apex_config=ApexConfig(cite_sources=True),
)
# Pass observability to run() — propagated to all agents automatically
# (Note: Orchestrator.run() accepts **kwargs forwarded to run_agent)
result = await orch.run("Explain blockchain technology")
With run_agent()
from deepcrew import Agent, run_agent, ObservabilityConfig
obs = ObservabilityConfig(otel_endpoint="http://localhost:4317")
result = await run_agent(
agent,
[{"role": "user", "content": "Hello!"}],
observability=obs, # that's it
)
# Spans emitted:
# deepcrew.agent.run → covers entire agent lifecycle
# deepcrew.llm.call → each LLM request (includes model, agent_id, tokens)
# deepcrew.tool.call → each tool execution (includes tool_name, agent_id)
With WorkflowBuilder
from deepcrew import WorkflowBuilder, ObservabilityConfig
obs = ObservabilityConfig(otel_endpoint="http://localhost:4317", service_name="workflow-app")
workflow = (
WorkflowBuilder(observability=obs) # pass at construction time
.add_agent("step1", agent1, task="{input}")
.add_agent("step2", agent2, task="Refine:\n{step1}")
.then("step1", "step2")
)
result = await workflow.run("My query")
# Spans emitted per step:
# deepcrew.workflow.step → covers each DAG node
# deepcrew.agent.run → the agent within the step
# deepcrew.llm.call
# deepcrew.tool.call
Start Jaeger locally (Docker)
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 6831:6831/udp \
-p 16686:16686 \
-p 4317:4317 \
jaegertracing/all-in-one:latest
# Open http://localhost:16686 to view traces
ObservabilityConfig reference
http://localhost:4317. For HTTP: http://localhost:4318/v1/traces."grpc" for port 4317, "http" for port 4318.Span attributes
| Span | Attributes |
|---|---|
deepcrew.agent.run | agent.id, llm.model |
deepcrew.llm.call | agent.id, llm.model, llm.input_tokens, llm.output_tokens |
deepcrew.tool.call | agent.id, tool.name |
deepcrew.workflow.step | step.name |
CLI — deepcrew run
Run declarative YAML workflow files from the terminal. No Python code required for simple workflows.
Installation check
pip install deepcrew-ai
deepcrew --version
# deepcrew-ai 0.2.0
Write a workflow YAML
agents:
- name: researcher
model: openai/gpt-4o-mini
system_prompt: Research the topic thoroughly using all available information.
tools:
- web_search # built-in skill by name
- name: analyst
model: anthropic/claude-haiku-4-5-20251001
system_prompt: Critically analyze the research findings. Identify gaps and strengths.
- name: writer
model: openai/gpt-4o
system_prompt: Write a clear, well-structured executive summary report.
tools:
- summarize # built-in summarize skill
workflow:
- step: research
agent: researcher
task: "{input}"
- step: analysis
agent: analyst
task: |
Analyze this research:
{research}
depends_on:
- research
- step: report
agent: writer
task: |
Write an executive summary based on:
Research: {research}
Analysis: {analysis}
depends_on:
- research
- analysis
Run it
# Stream output to terminal
deepcrew run workflow.yaml --input "The future of autonomous vehicles"
# Non-streaming (prints only final result)
deepcrew run workflow.yaml --input "Quantum computing in 2026" --no-stream
# List all agents in a config
deepcrew agents list --config workflow.yaml
YAML schema
web_search, summarize, execute_code.{step_name} in subsequent task templates.name.{input} is the CLI --input value. {step_name} is the text output of that step.Supported built-in tool names
| YAML name | Skill class | Description |
|---|---|---|
web_search | WebSearchSkill | DuckDuckGo search |
summarize | SummarizeSkill | LLM-backed summarization |
execute_code | CodeExecutionSkill | Python subprocess execution |