Skip to content

symfonic.tools.recall_tool_result

recall_tool_result

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.

Architectural contract

  • 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

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

    @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.

        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.
        """
        if not isinstance(mem_id, str) or not mem_id:
            return "[tool_memory error: mem_id must be a non-empty string]"
        fmt = format if format in ("summary", "full") else "summary"
        # Any keyed slice implies we need the underlying payload, so
        # force ``full`` fetch semantics on the ledger side.
        keyed = any(
            v is not None and v != ""
            for v in (query, json_path)
        ) or offset is not None or max_chars is not None
        if keyed:
            fmt = "full"
        try:
            payload = await ledger.fetch(  # type: ignore[arg-type]
                mem_id,
                format=fmt,
                query=query,
                offset=offset,
                max_chars=max_chars,
                json_path=json_path,
            )
        except TypeError:
            # Back-compat: a pre-v8.6.0 custom ToolResultLedger whose
            # ``fetch`` only accepts ``(mem_id, *, format)``.  Degrade
            # to whole-payload fetch (the keyed slice is unavailable on
            # that ledger; the model gets the full body, capped).
            try:
                payload = await ledger.fetch(mem_id, format=fmt)  # type: ignore[arg-type]
            except Exception:
                logger.exception(
                    "recall_tool_result: legacy ledger.fetch raised for "
                    "mem_id=%r format=%r",
                    mem_id, fmt,
                )
                return f"[tool_memory error: ledger fetch failed for {mem_id}]"
        except Exception:
            logger.exception(
                "recall_tool_result: ledger.fetch raised for mem_id=%r "
                "format=%r; returning defensive not-found",
                mem_id, fmt,
            )
            return f"[tool_memory error: ledger fetch failed for {mem_id}]"
        return _truncate_output(payload, mem_id=mem_id)

    return recall_tool_result