Tool Usage Learning
Agents with access to multiple tools make costly mistakes — calling expensive external APIs when a cached lookup suffices, using slow tools when fast alternatives exist, or repeatedly failing with the wrong tool type. This pattern teaches your agent to remember what works by persisting every tool invocation outcome and recalling successful strategies before each new decision.
Start Building Free →- Dakera instance running (quickstart)
- SDK installed:
pip install dakera/npm i @dakera-ai/dakera - An agent with two or more tools available (search, code execution, database lookup, etc.)
- A mechanism to capture tool execution results (success/failure, latency, output quality)
The Problem
Without memory, multi-tool agents are stateless selectors — they pick tools using only the current prompt and model priors. This leads to three failure modes:
- Repeated failures: The agent tries the same failing tool strategy across sessions because it has no record of prior failures.
- Cost inefficiency: A web search API costs 100x more than a cached knowledge recall, but the agent can't learn which queries are best served locally.
- Latency spikes: Without knowing that
database_lookupreturns in 30ms whileweb_scrapertakes 8 seconds, the agent can't optimize for speed.
The result is an agent that never improves — it makes the same suboptimal choices on day 100 that it made on day 1.
How It Works
Every tool invocation produces a structured outcome record: the tool name, the task type that triggered it, the result quality, and the latency. These outcomes are stored in Dakera with importance scores proportional to the quality of the result. Before choosing a tool for a new task, the agent recalls the most relevant prior outcomes and uses the success-rate pattern to inform selection.
Implementation Steps
-
Instrument tool calls to capture structured outcomesWrap every tool invocation with timing and result capture. Record success/failure, latency in milliseconds, and a short description of the outcome. This is the raw signal your memory will learn from.
-
Store outcomes with importance proportional to result qualitySuccessful outcomes get importance 0.88, partial ones 0.65 and failures 0.42: low enough not to dominate recall, but above the 0.3 floor of the recall, so they still serve as negative signal. Always include the task type in the content for semantic search, and the tool, outcome and latency in the metadata for scoring.
-
Recall tool history before each decisionBefore selecting a tool, call
recall()with a query describing the current task type. Retrieve the top 10 prior outcomes withmin_importance=0.3. Read each memory's metadata to compute per-tool success rates and average latencies. -
Score and rank available toolsCompute a composite score per tool:
score = success_rate * 0.7 + speed * 0.3, wherespeed = max(0, 1 - avg_latency / 60 s). Present the top-ranked tool to the LLM as the recommended selection, but let the model override if the task has unusual characteristics. -
Decay stale outcomes over timeTools change — a previously broken API gets fixed, a once-fast service degrades. Use
ttl_seconds(e.g. 30 days = 2,592,000s) on outcome records so the agent naturally forgets stale patterns and re-evaluates tools after significant time.
Store deployment outcomes under agent_id="devops-deploy", search outcomes under agent_id="devops-search". This prevents cross-contamination where deployment success rates pollute search tool recommendations, and keeps recall fast by limiting the memory pool per query.
Implementation
# Store a successful tool outcome
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer $DAKERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "devops-agent",
"content": "kubectl_deploy tool success for production_deploy task. Deployed v2.4.1 to prod cluster. Latency: 1900ms.",
"importance": 0.88,
"memory_type": "semantic",
"tags": ["tool:kubectl_deploy", "task:production_deploy", "outcome:success"],
"metadata": {"tool": "kubectl_deploy", "task_type": "production_deploy", "outcome": "success", "latency_ms": 1900, "environment": "production"},
"ttl_seconds": 2592000
}'
# Store a failure outcome (lower importance, still useful as negative signal)
curl -X POST http://localhost:3000/v1/memory/store \
-H "Authorization: Bearer $DAKERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "devops-agent",
"content": "ansible_run tool failure for production_deploy task. Timeout after 45s. Latency: 45000ms. Error: SSH_TIMEOUT.",
"importance": 0.42,
"memory_type": "semantic",
"tags": ["tool:ansible_run", "task:production_deploy", "outcome:failure"],
"metadata": {"tool": "ansible_run", "task_type": "production_deploy", "outcome": "failure", "latency_ms": 45000, "error": "SSH_TIMEOUT"},
"ttl_seconds": 2592000
}'
# Recall tool outcomes before choosing a tool for a deployment task
curl -X POST http://localhost:3000/v1/memory/recall \
-H "Authorization: Bearer $DAKERA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"agent_id": "devops-agent", "query": "tool outcomes for production_deploy tasks", "top_k": 10, "min_importance": 0.3}'import time
from typing import Optional
from dakera import DakeraClient
client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")
IMPORTANCE = {"success": 0.88, "partial": 0.65, "failure": 0.42}
def record_tool_outcome(agent_id: str, tool: str, task_type: str, outcome: str,
notes: str, latency_ms: int, error: Optional[str] = None) -> None:
"""Persist a tool invocation outcome for future learning."""
content = f"{tool} tool {outcome} for {task_type} task. {notes}. Latency: {latency_ms}ms."
if error:
content += f" Error: {error}."
client.store_memory(
agent_id=agent_id,
content=content,
importance=IMPORTANCE.get(outcome, 0.6),
memory_type="semantic",
tags=[f"tool:{tool}", f"task:{task_type}", f"outcome:{outcome}"],
metadata={"tool": tool, "task_type": task_type, "outcome": outcome, "latency_ms": latency_ms},
ttl_seconds=2_592_000, # 30 days: stale patterns expire
)
def get_best_tool(agent_id: str, task_type: str, available_tools: list[str]) -> str:
"""Recall tool history and return the highest-scoring tool for this task type."""
response = client.recall(agent_id=agent_id, query=f"tool outcomes for {task_type} tasks",
top_k=10, min_importance=0.3)
stats = {t: {"successes": 0, "total": 0, "latency": 0} for t in available_tools}
for mem in response.memories:
meta = mem.metadata or {}
if meta.get("tool") not in stats:
continue
s = stats[meta["tool"]]
s["total"] += 1
s["successes"] += meta.get("outcome") == "success"
s["latency"] += meta.get("latency_ms", 5000)
# Score: 70% success rate + 30% speed (normalized to 60 s)
best_tool, best_score = available_tools[0], -1.0
for tool, s in stats.items():
if s["total"] == 0:
continue # no history: let the model decide
success_rate = s["successes"] / s["total"]
speed = max(0.0, 1 - (s["latency"] / s["total"]) / 60_000)
score = success_rate * 0.7 + speed * 0.3
if score > best_score:
best_tool, best_score = tool, score
return best_tool
# --- Usage in a DevOps agent ---
TOOLS = ["kubectl_deploy", "ansible_run", "terraform_apply", "helm_upgrade"]
def deploy_to_production(service: str, version: str) -> dict:
best = get_best_tool("devops-agent", "production_deploy", TOOLS)
start = time.time()
try:
result = execute_tool(best, service=service, version=version) # your tool runner
record_tool_outcome("devops-agent", best, "production_deploy", "success",
f"Deployed {service} v{version} to production cluster",
int((time.time() - start) * 1000))
return result
except Exception as e:
record_tool_outcome("devops-agent", best, "production_deploy", "failure",
f"Failed to deploy {service} v{version}",
int((time.time() - start) * 1000), error=str(e))
raiseimport { DakeraClient } from '@dakera-ai/dakera';
const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });
type Outcome = 'success' | 'failure' | 'partial';
const IMPORTANCE: Record<Outcome, number> = { success: 0.88, partial: 0.65, failure: 0.42 };
async function recordToolOutcome(agentId: string, tool: string, taskType: string,
outcome: Outcome, notes: string, latencyMs: number) {
await client.storeMemory(agentId, {
content: `${tool} tool ${outcome} for ${taskType} task. ${notes}. Latency: ${latencyMs}ms.`,
importance: IMPORTANCE[outcome],
memory_type: 'semantic',
tags: [`tool:${tool}`, `task:${taskType}`, `outcome:${outcome}`],
metadata: { tool, task_type: taskType, outcome, latency_ms: latencyMs },
ttl_seconds: 2_592_000, // 30 days
});
}
async function getBestTool(agentId: string, taskType: string, tools: string[]): Promise<string> {
const { memories } = await client.recall(agentId, `tool outcomes for ${taskType} tasks`,
{ top_k: 10, min_importance: 0.3 });
const stats = new Map(tools.map(t => [t, { successes: 0, total: 0, latency: 0 }]));
for (const mem of memories) {
const s = stats.get(mem.metadata?.tool as string);
if (!s) continue;
s.total++;
if (mem.metadata?.outcome === 'success') s.successes++;
s.latency += (mem.metadata?.latency_ms as number) ?? 5000;
}
let best = tools[0];
let bestScore = -1;
for (const [tool, s] of stats) {
if (s.total === 0) continue; // no history: let the model decide
const speed = Math.max(0, 1 - s.latency / s.total / 60_000);
const score = (s.successes / s.total) * 0.7 + speed * 0.3;
if (score > bestScore) { bestScore = score; best = tool; }
}
return best;
}use dakera_client::{DakeraClient, RecallRequest, StoreMemoryRequest};
let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
let agent = "devops-agent";
// Record an outcome; the first words of the content name the tool and the outcome
let (tool, outcome, latency_ms) = ("kubectl_deploy", "success", 1900);
let importance = match outcome { "success" => 0.88, "partial" => 0.65, _ => 0.42 };
client.store_memory(
StoreMemoryRequest::new(agent, format!(
"{tool} tool {outcome} for production_deploy task. Latency: {latency_ms}ms."))
.with_importance(importance)
.with_tags(vec![format!("tool:{tool}"), "task:production_deploy".into(), format!("outcome:{outcome}")]),
).await?;
// Recall outcomes and compute a success rate per tool from the content
let found = client.recall(
RecallRequest::new(agent, "tool outcomes for production_deploy tasks").with_top_k(10),
).await?;
for tool in ["kubectl_deploy", "ansible_run"] {
let runs: Vec<_> = found.memories.iter().filter(|m| m.content.starts_with(tool)).collect();
let ok = runs.iter().filter(|m| m.content.contains(" tool success ")).count();
if !runs.is_empty() {
println!("{tool}: {}/{} successful", ok, runs.len());
}
}ctx := context.Background()
client := dakera.NewClientWithOptions(dakera.ClientOptions{
BaseURL: "http://localhost:3000",
APIKey: os.Getenv("DAKERA_API_KEY"),
})
agent := "devops-agent"
importance := map[string]float32{"success": 0.88, "partial": 0.65, "failure": 0.42}
// Record an outcome; the first words of the content name the tool and the outcome
recordOutcome := func(tool, outcome string, latencyMs int64) error {
imp := importance[outcome]
_, err := client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
Content: fmt.Sprintf("%s tool %s for production_deploy task. Latency: %dms.", tool, outcome, latencyMs),
Importance: &imp,
Tags: []string{"tool:" + tool, "task:production_deploy", "outcome:" + outcome},
})
return err
}
recordOutcome("kubectl_deploy", "success", 1900)
// Recall outcomes and compute a success rate per tool from the content
found, err := client.Recall(ctx, agent, dakera.RecallRequest{
Query: "tool outcomes for production_deploy tasks",
TopK: 10,
})
if err != nil {
log.Fatal(err)
}
for _, tool := range []string{"kubectl_deploy", "ansible_run"} {
total, ok := 0, 0
for _, m := range found.Memories {
if strings.HasPrefix(m.Content, tool) {
total++
if strings.Contains(m.Content, " tool success ") {
ok++
}
}
}
if total > 0 {
fmt.Printf("%s: %d/%d successful\n", tool, ok, total)
}
}See tool learning in action
Spin up a Dakera instance and watch your agent improve its tool selection as outcomes accumulate.
Real-World Scenario: DevOps Agent
A DevOps automation agent at a mid-size SaaS company manages deployments across 12 microservices. It has access to four deployment tools: kubectl_deploy, ansible_run, terraform_apply, and helm_upgrade.
Without tool learning, the agent defaults to ansible_run (its training prior) even though the company migrated to Kubernetes six months ago. With tool learning:
- Week 1: Agent stores 47 deployment outcomes.
kubectl_deployaccumulates a 91% success rate;ansible_rundrops to 34% after repeated SSH timeout failures. - Week 2: The agent automatically routes all production deploys to
kubectl_deploy. Mean deployment time drops from 28s to 4.2s. - Week 4: A new tool
helm_upgradeis added for chart-based services. The agent starts routing helm chart deployments correctly within 8 invocations as it learns the new specialization. - Month 3:
terraform_applyrecords fail-then-recover patterns on Fridays (change freeze). The agent learns to prefer other tools on Friday afternoons without any manual configuration.
Before / After Memory State
// Memory store: empty
// Agent must pick tools
// based on LLM priors alone
// Every session resets:
// "Which tool should I use
// for production deploys?"
// No record of:
// - ansible_run failing 66%
// - kubectl_deploy at 91%
// - Friday change freeze
// Result: random/prior-based
// tool selection, repeated
// failures, $2,400/mo in
// failed API retries
// Memory store contains:
{
"content": "kubectl_deploy tool success for production_deploy task. Latency: 1900ms.",
"importance": 0.88,
"tags": ["tool:kubectl_deploy", "outcome:success"]
}
{
"content": "ansible_run tool failure for production_deploy task. Latency: 45000ms. Error: SSH_TIMEOUT.",
"importance": 0.42,
"tags": ["tool:ansible_run", "outcome:failure"]
}
// Agent recalls 10 outcomes,
// computes: kubectl=91% success
// Selects kubectl automatically.
// Deployment time: 4.2s avg
SDK Method Reference
| Method | SDK | Purpose in this pattern |
|---|---|---|
store_memory(agent_id, content, importance, memory_type, tags, metadata, ttl_seconds) | Python | Persist a tool outcome with structured metadata |
recall(agent_id, query, top_k, min_importance) | Python | Retrieve past tool outcomes for a task type |
storeMemory(agentId, {content, importance, memory_type, tags, metadata, ttl_seconds}) | TypeScript | Persist a tool outcome with a TTL |
recall(agentId, query, {top_k, min_importance}) | TypeScript | Retrieve historical outcomes before tool selection |
client.store_memory(StoreMemoryRequest::new(agent, content).with_importance(..)).await? | Rust | Async outcome persistence with tags |
client.recall(RecallRequest::new(agent, query).with_top_k(10)).await? | Rust | Async recall of tool history |
client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{...}) | Go | Store an outcome with tags |
client.Recall(ctx, agent, dakera.RecallRequest{Query, TopK}) | Go | Recall tool history for scoring |
batch_forget(BatchForgetRequest(...)) | Python | Remove a retired tool's outcomes by tag |
Edge Cases and Gotchas
- Cold start (no history yet): When an agent has fewer than 5 outcomes per tool, success rates are statistically unreliable. Default to uniform random selection or your configured prior until you have at least 5 data points per tool.
- Environment drift: A tool that worked in staging may fail in production. Always include the environment (
staging,production) in both the content and metadata. Query separately:"tool outcomes for production_deploy tasks"vs"staging_deploy". - Correlated failures: Multiple tools can fail simultaneously due to an infrastructure outage (not tool quality). If all tools show failure outcomes within a 30-minute window, flag this as an infrastructure event rather than updating individual tool scores.
- Tool retirement: When you decommission a tool, use
forget()orbatch_forget()to remove its historical outcomes. Stale memories of a retired tool will pollute recall results until their TTL expires. - Importance inflation: If every successful outcome gets the same high importance, the memory store fills with high-importance entries and recall becomes less discriminating. If you need finer signal, vary importance within the success band by outcome quality: a 500ms success scores higher than a 55-second success.
Tool learning improves efficiency, but for operations with irreversible consequences (database drops, production deletions), always require explicit human confirmation regardless of historical success rates. Memory tells you what worked before — it cannot predict novel failure modes.
Performance Considerations
- Each decision costs one recall before the tool runs, usually small next to the tool's own execution time.
- Success rates from a handful of outcomes are noisy; see the cold start note above.
- Use
min_importance=0.3to include failure signals; a higher floor drops the failures (0.42) and the rates turn optimistic. - Keep
top_kmodest for tool selection: the score needs recent outcomes per tool, not the whole history. - Set
ttl_seconds=2_592_000(30 days) on all tool outcome memories to prevent outdated patterns from persisting indefinitely.
Advanced Configuration: Weighted scoring and A/B testing
For production DevOps agents, you can implement multi-armed bandit-style exploration to occasionally test lower-ranked tools and discover improvements:
import random
EXPLORATION_RATE = 0.1 # 10% of the time, try a non-optimal tool
def select_tool_with_exploration(agent_id: str, task_type: str, tools: list) -> str:
if random.random() < EXPLORATION_RATE:
# Explore: pick a random tool (excluding the known best)
best = get_best_tool(agent_id, task_type, tools)
others = [t for t in tools if t != best]
return random.choice(others) if others else best
# Exploit: use the historically best tool
return get_best_tool(agent_id, task_type, tools)
# Separate scoring weights for latency-sensitive tasks
def get_latency_optimized_tool(agent_id: str, task_type: str, tools: list) -> str:
"""When speed matters more than reliability, shift the scoring weight."""
# Modify scoring: 40% success rate + 60% speed (vs default 70/30)
# Implement by adjusting your score calculation in get_best_tool
pass
# Use a separate agent per team (avoid cross-team contamination)
def record_team_outcome(team: str, **kwargs):
record_tool_outcome(
agent_id=f"devops-{team}", # "devops-platform", "devops-backend"
**kwargs
)
With an exploration rate of 10%, one decision in ten tries another tool, so improvements in lower-ranked tools still get noticed while the other nine use the historically best one.
Run this pattern against a real Dakera.
The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.