SDK v0.5.0 — trajectory cache, discovery cache, and a tool-call-reduction benchmark, backed by the gateway instead of a client-only index. Browse all docs →
SDK Guide

Tool-Call Efficiency

Most of an agent's tool-call budget isn't reasoning — it's re-discovering things it already asked about, and re-running task sequences it already solved. This guide covers Smartflow's four gateway-backed answers: a discovery cache for tool schemas, a trajectory cache for whole multi-step sequences (with a one-line decorator), a tool-call-reduction benchmark, and a verified compression quality delta.

SDK Version
0.5.0
Install
pip install smartflow-sdk
Requires
Runtime v1.7.78+
API surface
Async only (v1)
Updated
August 2026

Overview

Two different kinds of waste show up in an agentic tool-calling loop:

Smartflow's existing per-call semantic cache already helps with individual repeated tool calls. This guide covers four additions layered on top:

Discovery cache

Fetch a server's cached tool schemas without a live tools/list round trip.

Trajectory cache

Cache a whole sequence of tool calls per task, with a one-line @smartflow_tool decorator.

Reduction benchmark

A live "% fewer tool calls" number computed from the gateway's own cache counters.

Compression quality delta

An HHEM-verified faithfulness score next to your compression ratio — not just a claim of losslessness.

ℹ️
Server-side, not client-only

All four are backed by gateway-side state (Redis-persisted where noted), so the benefit is shared across every process and every language calling the gateway — not just processes that imported this SDK. The SDK just gives Python callers a one-line ergonomic wrapper.

Quick Start

from smartflow import SmartflowClient
from smartflow.agent_tools import smartflow_task, smartflow_tool

# Decorate any tool function — sync or async, plain or already wrapped
# by LangChain / LangGraph / CrewAI's own @tool decorator.
@smartflow_tool(name="search_docs")
async def search_docs(query: str) -> str:
    return await my_search_backend(query)

async with SmartflowClient("http://your-smartflow:7775") as sf:
    async with smartflow_task(sf, task_key="daily-report:2026-08-07") as task:
        if task.cached:
            results = task.replay()  # zero live tool calls made
        else:
            results = [await search_docs("Q2 revenue")]
            # ... more tool calls as needed ...
        # commit happens automatically on clean exit from the `async with` block

Trajectory Cache

The trajectory cache memoizes an entire sequence of tool calls that a repeated task historically resolved to — not just one call. smartflow_task is a context manager around it:

  1. On entry, it calls lookup_trajectory(task_key). A hit sets task.cached = True and loads the recorded steps — task.replay() returns every step's result in order, with no underlying tool calls made.
  2. On a miss, each @smartflow_tool-decorated call inside the block runs normally and is recorded as the next step.
  3. On clean exit, the recording is committed — the next call with the same task_key will hit. If the block raises, the recording is discarded instead, so a broken sequence is never cached or replayed.
MethodPurpose
lookup_trajectory(task_key)Check for a cached sequence before running the agent loop.
start_trajectory(task_key, task_label=None)Begin recording (called automatically by smartflow_task).
record_trajectory_step(task_key, tool_name, params, result, server_id=None)Append one step.
commit_trajectory(task_key, ttl_seconds=None)Finalize — only after the task succeeded end-to-end.
discard_trajectory(task_key)Abandon a recording (task failed partway).
get_trajectory_stats()Hit/miss counters and estimated tool calls avoided.

Deriving a good task_key is the one thing the caller controls — use task_key_for(*parts) for a stable hash of whatever makes two runs "the same task" (a normalized request, a date bucket, a customer ID):

from smartflow.agent_tools import task_key_for

key = task_key_for("daily-report", customer_id, today.isoformat())
⚠️
v1 scope

Matching is exact on task_key — there is no semantic/fuzzy task matching yet. Two runs with slightly different keys are two different cache entries. Manual control via the six methods above is available if smartflow_task's conventions don't fit your loop.

Discovery Cache

Every tool schema Smartflow's MCP gateway indexes for semantic tool search is now also available as a direct discovery-cache read — a client can ask "what tools does this server have, and what are their input schemas?" and get an answer without the gateway issuing a live tools/list call.

hit = await sf.discover_tools("github-tools")
if hit["served_from_cache"]:
    tools = hit["tools"]   # each has name, description, input_schema
else:
    # miss — fall back to a live tools/list; it will populate the index
    pass

get_discovery_cache_stats() returns hit/miss counters and how many servers/tools are indexed.

Tool-Call Reduction Benchmark

A live, gateway-computed answer to "how many fewer tool calls are we making," derived directly from the MCP cache's hit/miss counters — a cache hit is a call that never reached the live server, i.e. one fewer round trip.

bench = await sf.get_tool_call_benchmark()
print(f"{bench['pct_calls_avoided']:.0%} fewer tool calls, "
      f"~{bench['estimated_tokens_saved']:,} tokens saved")
print(bench["top_tools"])       # per-tool breakdown
print(bench["methodology"])    # exactly how each number is derived
Always shows its methodology

The response includes a methodology string spelling out exactly how tool_calls_avoided and estimated_tokens_saved were computed (the latter uses a configurable average-tokens-per-call estimate, not a per-call measurement) — so the number is auditable, not just a headline.

Compression Quality Delta

Smartflow's semantic compression pipeline can rewrite text — deduplicating repeated concepts, abbreviating, stripping filler. That's lossy by design, and lossy rewrites of enterprise content deserve evidence, not just a compression-ratio number. Pass quality_check=True to also score the compressed output against the original with the HHEM hallucination-eval sidecar:

res = await sf.compress_text(long_text, quality_check=True)
print(f"{res['compression_ratio']:.1f}x compression, "
      f"faithfulness={res.get('quality_delta')}")

quality_delta is 1.0 − hallucination_score — 1.0 means the compressed text stayed fully faithful to the original; lower values mean the rewrite likely introduced unsupported or altered content. It comes back None whenever the check is disabled or the HHEM service is unreachable — never a fabricated default.

⚠️
Requires the HHEM sidecar

Set HHEM_QUALITY_CHECK_ENABLED=true and HHEM_SERVICE_URL on the Smartflow deployment (see the HHEM deployment guide). Without it, compression still works — quality_delta is simply None.

Raw HTTP Endpoints

Every SDK method above is a thin wrapper — call these directly from any language.

MethodPathNotes
GET/api/mcp/tools/discover/{server_id}Discovery cache read.
GET/api/mcp/tools/discover/statsDiscovery cache hit/miss counters.
GET/api/mcp/cache/benchmarkTool-call-reduction benchmark.
GET/api/mcp/trajectories/lookup?task_key=...Trajectory read.
POST/api/mcp/trajectories/startBody: {task_key, task_label?}
POST/api/mcp/trajectories/recordBody: {task_key, step: {tool_name, server_id?, params, result}}
POST/api/mcp/trajectories/commitBody: {task_key, ttl_seconds?}
POST/api/mcp/trajectories/discardBody: {task_key}
GET/api/mcp/trajectories/statsTrajectory cache hit/miss counters.
POST/api/metacache/compression/compressBody adds optional quality_check: bool.

Scope & Roadmap

Live on runtime v1.7.78+ SDK v0.5.0 Async-only (v1)