Tool learning: descriptions that improve with use¶
A tool's entry in toolbox memory is written once, from its docstring. Three things then go wrong that the docstring's author could not have seen:
- Calling. The model reaches the tool and passes arguments it refuses, such as an id without its prefix. It reads the error and tries again, on every request.
- Selection. A request that should reach the tool does not, because the team's word for the task is not in the description.
- Confusion. A request keeps going to this tool and belongs to a neighbour.
MemoRizz handles both in two steps. Augmentation enriches the entry when the tool is registered. Tool learning revises it later, from how the tool was called.
Augmentation at registration¶
from memorizz import Toolbox
toolbox = Toolbox.from_functions(
[lookup_claim, start_recovery],
memory_provider=provider,
llm_provider=model,
agent_id="claims-desk",
augment=True,
)
With augment=True, each tool costs one model call, which writes:
- a description of what the tool is for, in the words people use when they ask;
- six example requests it should handle.
Both are stored beside the docstring, which is kept as written. The argument schema always comes from the function's signature, never from the model. The reply has to be JSON with a description and a list of requests; otherwise the tool is registered with its plain docstring and no examples.
A tool row is embedded from its name, description, argument names and example requests, so routing can find it by any of them.
Changed behaviour
augment=True used to make three model calls and store their replies as
they came, which could put conversational text in the description and
the example requests. It now makes one call and checks the reply. Rows
registered earlier are left as they are. The text a tool row is embedded
from now includes its example requests and prefers the description to
the docstring; rows already embedded keep their vector until they are
next written.
Revising an entry from the tool log¶
review = agent.review_tools() # propose; dry_run=True stores nothing
for revision in review.revisions:
print(revision.tool, revision.notes_added, revision.queries_added)
agent.apply_tool_revision(revision.revision_id, approved_by="operator-7")
agent.undo_tool_revision("lookup_claim", approved_by="operator-7")
agent.tool_revisions() # history, newest first
A review reads the agent's recent tool calls and, for each tool with something to fix, asks the background model for three things. The code adds a fourth itself:
| What it fixes | What it rests on | |
|---|---|---|
| A usage note | A calling error that repeats | At least min_failures (2) failed calls |
| An example request | A wording that should have reached the tool | At least min_misses (2) requests that did not |
| A "Not for" line | Requests that went to this tool and another served | At least min_confusions (2) such requests, all served by the tool it names |
| An example call | The shape of a call that works | The calls that worked, for a tool with min_failures refused ones |
A request "did not reach" a tool in one of two ways, both found by the code:
- the tool served it, although routing would not have offered the tool for it (the agent had to go looking);
- the user asked, the run did not use the tool, and the next request in the same conversation did. The first wording is the one that was missed.
A request counts for the last tool its run called successfully. A lookup made on the way to a change is preparation, not what the request was for.
Requests that went to the wrong tool¶
A request "went to" a tool and was served by another in one of two ways, again both found by the code:
- the run used the tool, the user asked again, and the next request reached the other tool;
- routing ranked the tool first and left the tool that served the request out of those it offered. A tool the run called on the way (a lookup before a change) does not count.
Two such requests bring a tool to review even when it has fewer than
min_calls calls. The line the model writes has one shape: Not for <kind of
request>; use <tool>. It is shown to the model with the tool and kept out of
the text routing searches, because a description that lists what a tool is
not for attracts exactly those requests.
revision.avoid_added # ["Not for large loss referrals; use refer_llu."]
revision.evidence # [{"kind": "confused_request", "served_by": "refer_llu", "how": "re_asked", ...}]
The example call¶
For a tool whose calls keep being refused, the code builds one line from the calls that worked, with no model involved:
Digits are masked. A string is shown as a format only when every successful
call has the same masked form and little of it is fixed (a prefix, a
separator); anything else is <text>. No argument value is kept. The line
is in revision.call_example, and is applied and undone with the rest.
What the code checks¶
The model proposes. The code decides what is written:
- A note cites the failed calls it rests on, and an example cites the missed requests it rests on and shares a word with each.
- Neither may contain an argument value from the calls (an id, a name, an amount), or text aimed at the model.
- A "Not for" line starts with those words, names a registered tool other than its own, cites requests that tool served, and shares a word with each.
- A note or "Not for" line is one line of at most 220 characters. A tool keeps at most 4 notes, 2 "Not for" lines, one example call and 12 example requests.
- Nothing the author wrote is rewritten. Notes go under a "Learned from use" heading after the description, and the schema is never touched.
- When a revision is applied, routing is checked again for the requests
other tools served. If the new examples push another tool out of the tools
offered for one of them, the examples are withdrawn and only the notes
stay (
queries_withdrawn, with the requests indisplaced).
What the model proposed and the code refused is returned with the reason, in
revision.refused and review.refused.
Who reads what¶
| Routing searches | The model is shown | |
|---|---|---|
| The author's docstring | When there is no description | Yes |
| The augmented description and its examples | Yes | No |
| Learned usage notes, "Not for" lines, the example call | No | Yes |
| Learned example requests | Yes | No, unless show_learned_examples is on |
A usage note says how to call the tool, so it is kept out of the text routing searches. An example request is the reverse: it is a retrieval key, written in a user's words, and a user's words do not belong in the prompt as if they were instructions. So by default the model never reads one.
Changed behaviour
Learned example requests used to be shown to the model too, on an "Also asked for as" line. They are now hidden by default. To show them:
The setting is stored with the agent's learning records and applies to every later session. When the model needs to know that a house term belongs to a tool, a "Not for" line on the neighbour it was confused with usually does the job without quoting anybody.
Where an example came from¶
Every example request on a tool is one of three kinds
(ToolLearner(agent).example_sources(tool)):
| Kind | Written by | Can give way |
|---|---|---|
synthetic |
Augmentation at registration | Yes |
learned |
A revision, from real requests | Only by undoing the revision |
unknown |
Something else (a row edited by hand, an old row) | No |
Real wording displaces invented wording:
- a learned example that says the same as a synthetic one replaces it;
- at the cap (
max_queries), a new learned example takes the place of the oldest synthetic one. With no synthetic one left, it is refused asno_room; - one that repeats a learned example is refused as
already_kept.
What gave way is in revision.queries_displaced, and undo puts it back.
Rows registered from now on carry queries_source: "augmentation". An older
row counts as augmented when its description differs from its docstring.
Tools nobody reaches¶
Each turn in which routing chooses among the tools, MemoRizz counts which ones it offered (one small record per agent). With the tool calls, that gives two flags:
agent.tool_usage_flags()
# [{"tool": "set_rvf", "flag": "retrieved_never_called", "offered_turns": 9, "calls": 0, ...},
# {"tool": "request_cat_export", "flag": "never_retrieved", "offered_turns": 0, ...}]
| Flag | Means | Usual cause |
|---|---|---|
retrieved_never_called |
Offered in min_offers (5) turns or more, never called |
A description that matches requests the tool is not for |
never_retrieved |
Never offered and never called, over min_turns (20) routed turns |
A description nobody's wording matches, or a tool nobody needs |
A review carries the same list in review.flags. A flag is for a person to
read: nothing is proposed or changed from it. Nothing is counted while every
tool is sent on every turn (a small toolbox under
ContextPolicy.stable_tool_list_tokens), because routing chose nothing.
Review, apply, undo¶
A revision is stored as proposed and waits for
apply_tool_revision(..., approved_by=...). review_tools(auto_apply=True)
applies it at once as memorizz:tool_learning. Each revision keeps:
evidence: the requests and errors it rests on, with argument names but not argument values;ranks: where routing ranked the tool for each missed request, before and after;approved_byandstatus(proposed,applied,dismissed,undone).
undo_tool_revision(tool, approved_by=...) takes back the newest applied
revision of a tool. It refuses when the entry was edited since. A tool with
a revision waiting is not reviewed again, and a tool with no new calls since
its last review costs no model call.
Settings¶
Pass config= to review_tools, as a dict or a ToolLearningConfig:
| Setting | Default | Meaning |
|---|---|---|
min_calls |
3 | Calls of a tool before it is reviewed |
min_failures |
2 | Failed calls a usage note must rest on |
min_misses |
2 | Missed requests an example request must rest on |
max_notes |
4 | Usage notes kept on one tool |
max_queries |
12 | Example requests kept on one tool |
max_new_queries |
4 | Example requests one review may add |
max_calls_per_tool |
24 | Most recent calls read for one tool |
max_tools |
8 | Tools reviewed in one pass, those with the most failed calls first |
min_confusions |
2 | Wrongly routed requests a "Not for" line must rest on |
max_avoid |
2 | "Not for" lines kept on one tool |
min_offers |
5 | Turns a tool is offered in, uncalled, before it is flagged |
min_turns |
20 | Routed turns observed before a tool never offered is flagged |
show_learned_examples |
False |
Show the model learned example requests |
agent.configure_tool_learning(...) keeps any of these for the agent;
config= on one call overrides what is kept.
What it needs, and its limits¶
- Workflow memory gives each call the request that led to it. An agent
that keeps only the tool log (
ToolResultPolicy(persist_all_results=True)) can learn usage notes, but not example requests. - It revises tools registered through a
Toolboxfor that agent. Tools shared by several agents, the rows saving an agent writes, and a connected MCP server's tools are left alone. - A request that went to the wrong tool and was never corrected leaves no trace. Finding those takes an outcome your application reports; see verified outcomes.
- "The user asked again" also matches a conversation that moved on to a new task. The two-request rule, the shared-word rule and the background model's judgement reduce that; review before applying catches the rest.
- A "Not for" line needs the neighbour to have served the request in the end. Two tools that are confused and never corrected leave no pair.
- The usage flags count what routing offered, not what the model was shown: tools kept visible from an earlier turn are not counted again.
- The refine cycle runs a tool review as one of its
jobs, and proposes revisions that wait for a person (
refine_tool_review). Outside the cycle it is an SDK feature: it has no page in the local UI, CLI command or MCP tool yet. - It has been run on the filesystem provider and on Oracle (register with
augmentation, review, apply, a reload and undo). A revision writes only
the
description,queriesandembeddingof a tool row, which every provider stores, but it has not been run on MongoDB. "Not for" lines, the example call, example sources, the usage flags and the stored settings have been run on the filesystem provider only. Thequeries_sourcemarker is an extra field on the tool row; a provider that drops unknown fields falls back to comparing the description with the docstring.
The notebook
examples/continual_learning/04_toolbox_evolution.ipynb
walks through all of it on a claims-desk toolbox.