NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →
Intermediate Agent Behavior

Tool Usage Learning

⏱ ~30 min to implement 📦 Requires: Dakera v0.11+

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 →
Prerequisites
  • 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:

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.

Workflow of the tool learning loop. A new task such as production_deploy reaches the DevOps agent, which calls recall() on devops-agent for past outcomes with top_k=10 and min_importance 0.3. The agent scores tools by success rate and latency and the LLM picks, with the top tool suggested and the model free to override. The tool runs, for example kubectl_deploy, the agent captures success or failure and latency_ms, and stores the outcome with store_memory() at an importance set by the outcome and a 30-day TTL. That record feeds the next decision.Workflow of the tool learning loop. A new task such as production_deploy reaches the DevOps agent, which calls recall() on devops-agent for past outcomes with top_k=10 and min_importance 0.3. The agent scores tools by success rate and latency and the LLM picks, with the top tool suggested and the model free to override. The tool runs, for example kubectl_deploy, the agent captures success or failure and latency_ms, and stores the outcome with store_memory() at an importance set by the outcome and a 30-day TTL. That record feeds the next decision.
Every tool run writes an outcome record, and the next selection for the same task type starts by recalling them.Full size ↗Full size ↗
Data flow of how stored outcomes become a tool choice. Outcome memories are stored at importance 0.88 for success, 0.65 for partial and 0.42 for failure. recall() returns the top 10 with min_importance 0.3, so failures stay in as a negative signal. Per tool, the agent computes the success rate as successes over total and a speed score of 1 minus average latency over 60 seconds. The highest score, success rate times 0.7 plus speed times 0.3, is suggested to the LLM.Data flow of how stored outcomes become a tool choice. Outcome memories are stored at importance 0.88 for success, 0.65 for partial and 0.42 for failure. recall() returns the top 10 with min_importance 0.3, so failures stay in as a negative signal. Per tool, the agent computes the success rate as successes over total and a speed score of 1 minus average latency over 60 seconds. The highest score, success rate times 0.7 plus speed times 0.3, is suggested to the LLM.
Failures are stored low but above the 0.3 floor, so they still count against a tool when the scores are computed.Full size ↗Full size ↗

Implementation Steps

Tip: Separate namespaces per tool category

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))
        raise
import { 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.

Try Free →

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:

Before / After Memory State

Before: No Tool Memory
// 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
After: Tool Learning Active
// 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

MethodSDKPurpose in this pattern
store_memory(agent_id, content, importance, memory_type, tags, metadata, ttl_seconds)PythonPersist a tool outcome with structured metadata
recall(agent_id, query, top_k, min_importance)PythonRetrieve past tool outcomes for a task type
storeMemory(agentId, {content, importance, memory_type, tags, metadata, ttl_seconds})TypeScriptPersist a tool outcome with a TTL
recall(agentId, query, {top_k, min_importance})TypeScriptRetrieve historical outcomes before tool selection
client.store_memory(StoreMemoryRequest::new(agent, content).with_importance(..)).await?RustAsync outcome persistence with tags
client.recall(RecallRequest::new(agent, query).with_top_k(10)).await?RustAsync recall of tool history
client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{...})GoStore an outcome with tags
client.Recall(ctx, agent, dakera.RecallRequest{Query, TopK})GoRecall tool history for scoring
batch_forget(BatchForgetRequest(...))PythonRemove a retired tool's outcomes by tag

Edge Cases and Gotchas

Warning: Don't rely solely on memory for safety-critical tool selection

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

1
Recall per tool decision (top_k=10)
0.3
min_importance that keeps failure outcomes (0.42) in the recall
30 days
TTL on outcome records, so stale patterns expire
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.

Try it live

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.