Ratel Docs
Features

Self-improving agent

Adaptive usage ranking feeds what your agent actually invokes back into retrieval. Experimental opt-in, off by default.

BM25 and dense retrieval score text similarity only — they have no memory. Some gaps no description rewrite can close: "why is the build broken" ranks docker_build first on the token build, even when every such query ends in gh_run_list.

Adaptive usage ranking closes that gap with real usage (ADR 0014). A search followed by an invoke becomes a weighted edge in an in-memory IntentGraph; when a later query matches one of the graph's clusters, the capabilities that history supports get a boost.

Experimental and off by default. The entry points carry an experimental prefix and may change until the capability graduates. IntentGraph and its wire form are stable protocol/v1.

How the boost works

  • Matched clusters add a sub-unit RRF arm fused beside the BM25/dense arms. The arm never overrides a strong lexical/dense match — it lifts capabilities usage history supports, and cannot conjure ones the base ranker did not retrieve.
  • A query that matches no cluster ranks exactly as if no graph were attached.
  • Every hit now carries rank (0-based position — order and threshold on it) and fused (whether score switched to an RRF scale). Treat score as a within-list hint.
  • Any method learns: a BM25 catalog clusters queries lexically, reaching repeats and near-repeats; a semantic or hybrid catalog clusters by meaning, at no extra embedding cost.

Turn it on

Attach a graph and the catalog learns from every search that leads to an invoke — a normal agent loop is all the training there is. Pass the same graph to your tool and skill catalogs so both learn into one set of clusters.

from ratel_ai import IntentGraph

graph = IntentGraph()
catalog.experimental_enable_adaptive_ranking(graph)

# The graph listens to each search and the invoke that follows it.
catalog.search("why is the build broken", 5)
await catalog.invoke("gh_run_list", {})
import { IntentGraph } from "@ratel-ai/sdk";

const graph = new IntentGraph();
catalog.experimentalEnableAdaptiveRanking(graph);

// The graph listens to each search and the invoke that follows it.
catalog.search("why is the build broken", 5);
await catalog.invoke("gh_run_list", {});

experimental_disable_adaptive_ranking() / experimentalDisableAdaptiveRanking() turns the arm off; the graph keeps what it learned.

Persist the learning

The graph lives in memory and you own storage: serialize to the protocol/v1 JSON wire form and write it wherever you keep state — file, DB, blob.

saved = graph.to_json()

# A later process: reload, attach, keep learning.
restored = IntentGraph.from_json(saved)
catalog.experimental_enable_adaptive_ranking(restored)
const saved = graph.toJson();

// A later process: reload, attach, keep learning.
const restored = IntentGraph.fromJson(saved);
catalog.experimentalEnableAdaptiveRanking(restored);
  • graph.rev is a monotonic write counter. Save only when it moved since your last write, and compare it to a stored graph's rev before overwriting to detect a concurrent writer — single-writer is the supported model.
  • A saved graph contains the raw text of past queries. Treat it like your telemetry log: restrict permissions, keep it out of version control.

Changing the embedding model

Cluster centroids are tied to the model that built them. Load a graph under a different embedding model and the usage arm pauses — base ranking is untouched — rather than compare across incompatible vector spaces. (There is also a one-time stderr warning; suppress it with warn_on_model_mismatch=False / warnOnModelMismatch: false.)

catalog.experimental_adaptive_ranking_status  # "paused: model mismatch"
await catalog.experimental_rebuild_intent_graph()  # re-embed under the current model
catalog.experimentalAdaptiveRankingStatus.status; // "paused: model mismatch"
await catalog.experimentalRebuildIntentGraph(); // re-embed under the current model

The rebuild re-embeds the clusters' members; support and edges survive, only the centroids move. For zero-touch recovery, enable with rebuild_on_model_change=True ({ rebuildOnModelChange: true }): the next dense search re-embeds a paused graph before ranking. Off by default — a rebuild is an embedding pass: cost, possible failure, and a graph mutation.

Try it

Next steps

On this page