recall_tool_result: framework tool for re-expanding compacted tool results.
v7.12.0 ships ToolResultLedger which swaps verbose ToolMessage
content for a compact stub on every react iteration past the keep_last_n
threshold. The stub embeds a mem_id (trm_*) the model can pass to
this tool to recover the original payload at full or summary fidelity.
- Framework provides the tool; model authors the args. The
v7.8.3 architectural-line guard: the framework constrains the
choice set (we register this tool), the model decides when to
recall, which mem_id to expand, and at what fidelity.
- Side-effect-free. Calling
recall_tool_result only reads
from the ledger. It does not write or mutate state.
- Defensive on bad input. An LLM that hallucinates a mem_id
(or types one with a typo) gets a structured "not found" message,
not an exception. The framework MUST NOT raise into the tool
invocation path -- the model has to be able to recover.
- Bounded output.
MAX_OUTPUT_CHARS caps the recall payload
at 50KB so a single 5MB tool result can't blow the next iteration's
context window. This is defense-in-depth alongside the v7.12.0
compaction itself.
Recipe (engine registration)::
from symfonic.tools.recall_tool_result import create_recall_tool_result_tool
tool = create_recall_tool_result_tool(ledger=agent._deps.require(ToolResultLedger))
agent._graph.registry.register_runtime_tool(tool)
create_recall_tool_result_tool(ledger: Any) -> Any
Build the recall_tool_result LangChain tool bound to a
specific :class:ToolResultLedger instance.
Parameters:
| Name |
Type |
Description |
Default |
ledger
|
Any
|
A registered :class:ToolResultLedger -- typically
agent._deps.require(ToolResultLedger). The tool
calls ledger.fetch(mem_id, format=...).
|
required
|
Returns:
| Type |
Description |
Any
|
An @tool-decorated async callable suitable for
|
Any
|
agent.register_tool(...) or registry direct insertion.
|
Source code in src/symfonic/tools/recall_tool_result.py
| def create_recall_tool_result_tool(ledger: Any) -> Any:
"""Build the ``recall_tool_result`` LangChain tool bound to a
specific :class:`ToolResultLedger` instance.
Args:
ledger: A registered :class:`ToolResultLedger` -- typically
``agent._deps.require(ToolResultLedger)``. The tool
calls ``ledger.fetch(mem_id, format=...)``.
Returns:
An ``@tool``-decorated async callable suitable for
``agent.register_tool(...)`` or registry direct insertion.
"""
from langchain_core.tools import tool
from symfonic.capabilities.tools.results import RecallService
_service = RecallService(ledger)
@tool
async def recall_tool_result(
mem_id: str,
format: str = "summary",
query: str | None = None,
offset: int | None = None,
max_chars: int | None = None,
json_path: str | None = None,
) -> str:
"""Re-expand or slice a previously-offloaded tool result.
symfonic-core offloads large tool results into compact memory
stubs of the form ``[symfonic.tool_memory v1] tool=... mem=trm_*``
once they age past the ``keep_last_n`` window. The stub carries
a structural index + a short head ``preview``, so you can often
decide WITHOUT recalling. When you need more, call this tool
with the ``mem_id`` from the stub.
Prefer the cheapest retrieval that answers your question:
1. ``format="summary"`` (default) -- the deterministic one-liner
the framework derived from the tool's name/args/shape.
2. A **keyed slice** -- strongly preferred over ``full`` for
large payloads (e.g. a 321K-token catalog), because it
returns only what you need:
* ``query="<substring>"`` -- returns ±windows around matches.
* ``offset=<int>, max_chars=<int>`` -- a char range.
* ``json_path="<dotted.path>"`` -- a JSON sub-structure
(object keys or array indices, e.g. ``"rows.0.price"``).
3. ``format="full"`` -- only when you genuinely need the whole
payload; it may be large and is capped at ~50KB.
What a recalled result is, and is not:
* A recall returns **text**. If the original result was something you
looked at rather than read -- an image, a chart -- its text form is
a description, not the thing. Do not state a value you can only
have obtained by looking.
* A truncated or sliced payload means **unknown, not absent**. If a
slice does not contain a row, a column or a key, that is evidence
about the slice. Widen the read before saying the thing is missing.
* Never restate an exact figure from your own earlier prose. Your
summary of a result is not the result; re-read the payload for the
value.
These are instructions, not a check. Nothing here validates a number
you report, so the discipline is the safeguard.
Args:
mem_id: The ``trm_*`` identifier from the compact stub.
format: ``"summary"`` or ``"full"``. Ignored when a keyed
arg (``query``/``offset``/``json_path``) is supplied.
query: Substring to locate; returns windows around matches.
offset: Start char index for a range read.
max_chars: Length of the range read (with ``offset``).
json_path: Dotted path into a JSON payload.
Returns:
The recalled body or slice, or a structured
``[tool_memory not found: ...]`` / ``[recall: no match ...]``
message when the ``mem_id`` or key resolves to nothing.
"""
# T3.1.3: the recall policy -- mem_id validation, format
# normalisation, the keyed-slice-implies-full rule, the
# pre-v8.6.0 ledger degradation and the output cap -- is
# :class:`symfonic.capabilities.tools.results.RecallService`.
# What stays here is the tool surface itself: the docstring the
# model reads and the argument names it authors.
return await _service.recall(
mem_id,
fmt=format,
query=query,
offset=offset,
max_chars=max_chars,
json_path=json_path,
)
return recall_tool_result
|