Case-Based Plan Reuse for AI Agents
A reusable case-based reasoning pattern for agents: treat past PlanTraces as searchable cases, rank them by similarity and quality, adapt the best ones, and record reuse lineage.
This solves agent plan reuse. Instead of treating memory as a transcript dump, store past reasoning episodes as cases that can be embedded, scored, adapted, validated, and retained.
Code
from dataclasses import dataclass
from math import exp
@dataclass
class PlanTrace:
trace_id: str
goal_text: str
plan: list[str]
avg_score: float
created_at_seconds: float
reuse_count: int = 0
attempt_count: int = 0
class ScorableRanker:
def __init__(self, embedding_store, weights=None):
self.embedding_store = embedding_store
self.weights = weights or {
"similarity": 0.4,
"reward": 0.3,
"recency": 0.2,
"adaptability": 0.1,
}
def rank(self, goal_text, traces, now_seconds):
goal_emb = self.embedding_store.get_or_create(goal_text)
ranked = []
for trace in traces:
trace_emb = self.embedding_store.get_or_create(trace.goal_text + "\n" + "\n".join(trace.plan))
components = {
"similarity": self.embedding_store.cosine(goal_emb, trace_emb),
"reward": trace.avg_score,
"recency": exp(-(now_seconds - trace.created_at_seconds) / (30 * 24 * 60 * 60)),
"adaptability": trace.reuse_count / max(trace.attempt_count, 1),
}
score = sum(components[k] * self.weights[k] for k in self.weights)
ranked.append((score, components, trace))
return sorted(ranked, key=lambda item: item[0], reverse=True)
class PlannerReuseAgent:
def __init__(self, memory, ranker, llm, top_k=3):
self.memory = memory
self.ranker = ranker
self.llm = llm
self.top_k = top_k
async def run(self, context):
goal_text = context["goal"]["goal_text"]
all_traces = self.memory.plan_traces.get_all(limit=500)
ranked = self.ranker.rank(goal_text, all_traces, context["now_seconds"])
selected = [trace for _score, _parts, trace in ranked[: self.top_k]]
prompt = self._adaptation_prompt(goal_text, selected)
new_plan = self.llm.generate_plan(prompt)
new_trace_id = self.memory.plan_traces.create(goal_text=goal_text, plan=new_plan)
for parent in selected:
self.memory.plan_traces.add_reuse_link(
parent_trace_id=parent.trace_id,
child_trace_id=new_trace_id,
)
context["plan_trace_id"] = new_trace_id
context["plan"] = new_plan
context["reused_trace_ids"] = [trace.trace_id for trace in selected]
return context
def _adaptation_prompt(self, goal_text, traces):
cases = "\n\n".join(
f"Case {i+1}: {trace.goal_text}\nScore: {trace.avg_score}\nPlan:\n"
+ "\n".join(f"- {step}" for step in trace.plan)
for i, trace in enumerate(traces)
)
return f"Adapt the useful patterns from these prior cases to solve:\n{goal_text}\n\n{cases}"
Usage
agent = PlannerReuseAgent(memory=memory, ranker=ScorableRanker(memory.embedding), llm=llm)
context = await agent.run({
"goal": {"goal_text": "Build a RAG evaluator for a new paper corpus"},
"now_seconds": time.time(),
})
How it works
The reusable object is the case lifecycle: retrieve prior traces, rank them with similarity plus value signals, adapt the top cases into a new plan, validate challenger plans against champions, and retain only cases that remain useful.
Source
The implementation is part of Stephanie: planner_reuse.py.
Full explanation
For the complete CBR middleware, retention policy, champion promotion, and PlanTrace monitor design, read: Case Based Reasoning: Teaching AI to Learn From itself.