> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Compact Tool Output

> Feed the LLM a terse view of a tool result while keeping the full payload for display, hooks, and tracing

Compact tool output lets a tool return a rich full result while the model only sees a short summary — fewer tokens per tool call, no change in default behaviour.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Compact Tool Output"
        Tool[🛠️ Tool run] --> Full[📦 Full result]
        Full --> Hooks[🪝 AFTER_TOOL + tracing]
        Full --> Resolve{🔍 model_output<br/>or to_model_output?}
        Resolve -->|Yes| Compact[✂️ Compact view]
        Resolve -->|No| Passthrough[📄 Full string]
        Compact --> Fence[🛡️ wrap_if_external]
        Fence --> LLM[🤖 LLM sees compact]
        Passthrough --> LLM
        Hooks --> Display[🖥️ Display / downstream]
    end

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class Tool,LLM,Display input
    class Full,Hooks,Compact,Fence,Passthrough process
    class Resolve decision
```

## Quick Start

**1. @tool decorator — the simplest surface**

Pass `to_model_output` to shrink what the LLM sees.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, tool

def _weather_summary(result: dict) -> str:
    return f"{result['city']}: {result['temp_c']}°C, {result['condition']}"

@tool(to_model_output=_weather_summary)
def get_weather(city: str) -> dict:
    return {
        "city": city,
        "temp_c": 22,
        "condition": "sunny",
        "hourly": [{"h": h} for h in range(24)],  # ~2 KB — NOT sent to the LLM
        "raw_provider_payload": {"noise": "x" * 5000},  # huge — NOT sent
    }

agent = Agent(
    name="Weather",
    instructions="Report the weather concisely.",
    tools=[get_weather],
)
agent.start("What's the weather in Paris?")
```

**2. BaseTool subclass — override the hook**

Override `to_model_output` on the class.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.tools import BaseTool

class SearchTool(BaseTool):
    name = "search"
    description = "Search the web and return the top results"

    def run(self, query: str) -> dict:
        results = [{"title": f"Result {i}", "html": "<html>" * 500}
                   for i in range(50)]
        return {"query": query, "results": results}

    def to_model_output(self, result: dict) -> str:
        titles = [r["title"] for r in result["results"][:5]]
        return f"Top 5 for '{result['query']}':\n- " + "\n- ".join(titles)

agent = Agent(name="Researcher", tools=[SearchTool()])
agent.start("Find recent papers on retrieval-augmented generation")
```

**3. Return a ToolResult carrying its own compact view**

Set `model_output` on the `ToolResult` directly.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, tool
from praisonaiagents.tools import ToolResult

@tool
def analyse_logs(path: str) -> ToolResult:
    full = {"errors": 3, "warnings": 12, "lines": ["..."] * 9000}
    summary = f"{full['errors']} errors, {full['warnings']} warnings"
    return ToolResult(
        output=full,          # display / hooks / downstream get this
        model_output=summary, # LLM sees only this
    )

agent = Agent(name="LogTriage", tools=[analyse_logs])
agent.start("Triage today's logs at /var/log/app.log")
```

***

## How It Works

The executor snapshots the full result for tracing and hooks, then swaps in the compact view for the LLM.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Tool
    participant Exec as tool_execution
    participant Hooks as AFTER_TOOL / tracing
    participant LLM

    Agent->>Tool: call(**kwargs)
    Tool-->>Exec: full result (or ToolResult)
    Exec->>Exec: snapshot full result
    Exec->>Hooks: emit(full)
    Hooks-->>Exec: additional_context / loop-guard
    Exec->>Exec: _resolve_model_facing_result()
    alt Compact view declared
        Exec->>Exec: wrap_if_external(compact)
        Exec->>Exec: mirror annotations onto compact
        Exec-->>LLM: compact view
    else No compact view
        Exec-->>LLM: str(full) / json.dumps(full)
    end
    Exec-->>Agent: full result (display / downstream)
```

| Step | What happens |
| - | - |
| 1 | Tool runs, returns full output (or a `ToolResult`) |
| 2 | Executor snapshots the full result for tracing and the `AFTER_TOOL` hook |
| 3 | `_resolve_model_facing_result` picks `result.model_output`, else `tool.to_model_output(output)` |
| 4 | External tools: the compact override passes through `wrap_if_external()` |
| 5 | Loop-guard / additional-context notices are mirrored onto the compact view |
| 6 | LLM sees the compact view; the full payload stays with display, hooks, downstream tools |

***

## How to Choose

Pick the surface that matches how your tool is defined.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Q{How is your tool defined?}
    Q -->|Function with @tool| A[Use @tool to_model_output=fn]
    Q -->|Class inheriting BaseTool| B[Override to_model_output on the class]
    Q -->|Anything else, one-off shape| C[Return ToolResult output=... model_output=...]

    classDef q fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef opt fill:#189AB4,stroke:#7C90A0,color:#fff
    class Q q
    class A,B,C opt
```

***

## Configuration Options

| Surface | Signature | Default | Notes |
| - | - | - | - |
| `ToolResult(model_output=)` | `model_output: Any = None` | `None` | Highest precedence when the tool returns a `ToolResult`. Surfaced in `to_dict()`. |
| `@tool(to_model_output=fn)` | `Callable[[Any], Any]` | `None` | `result → compact_view`. Raise → falls back to full output. |
| `BaseTool.to_model_output(result)` | `(self, result: Any) -> Optional[Any]` | returns `None` | Override on a subclass. Receives the raw output value, not the `ToolResult`. |
| `resolve_model_output(result)` | `(Any) -> Optional[Any]` | — | Helper reading `result.model_output`. Does **not** invoke the hook. |

> [!NOTE]
>
> No compact view declared → behaviour is exactly as before. Fully opt-in.

### Resolution order

The executor picks the first hit, then falls back to the full string.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[📦 Tool result] --> D1{model_output set?}
    D1 -->|Yes| Use1[✂️ Use result.model_output]
    D1 -->|No| D2{to_model_output hook?}
    D2 -->|Yes| Use2[✂️ Use hook output]
    D2 -->|No| Full[📄 Full str / json.dumps]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class Start input
    class D1,D2 decision
    class Use1,Use2,Full ok
```

### Behavioural guarantees

* **Backward compatible.** No compact view → `str(result)` / `json.dumps(...)` reaches the LLM exactly as before.
* **Full result survives.** The compact view swaps in **after** tracing, the `AFTER_TOOL` hook, result-aware loop detection, and the loop-guard have all seen the full payload.
* **Annotations preserved.** `AFTER_TOOL` `additional_context` and loop-guard messages are mirrored onto the compact view so notices always reach the model.
* **Prompt-injection fence still applies.** For external tools, `wrap_if_external()` wraps the compact override too — it never bypasses security markers.
* **Multimodal untouched.** If the result has `content`, the compact view is not resolved — image/file parts flow through unchanged.
* **Overflow spill untouched.** `_output_overflow` (see [Tool Output Spill](https://praison.ai/docs/features/tool-output-spill.md)) still works; `model_output` composes with it.
* **Failure is soft.** A raising compact-view builder degrades to the full output — never crashes the tool call.

***

## Total-size budget on dict results

After per-field truncation, if the serialised dict is still over the limit, it falls back to the head/tail string that already carries the artifact reference.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent, tool

@tool
def search(query: str) -> dict:
    # Many medium snippets, each just under the per-field limit, still
    # overflow the context in aggregate. The agent now receives the
    # head/tail string plus the artifact pointer, not the oversized dict.
    return {"results": [{"content": "a" * 5000} for _ in range(50)]}

agent = Agent(name="Researcher", tools=[search])
agent.start("Search for retrieval-augmented generation papers")
```

The per-field pass (`_truncate_dict_fields`) only shortens fields that each exceed the field limit. A dict of many medium-sized fields each just under that limit stays unbounded, so a second check compares `len(str(result))` against the budget and falls back when the dict is still too large.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    Start[📦 Tool returns dict] --> Field[✂️ Per-field pass<br/>shortens fields > max_field_chars]
    Field --> Over{len str result<br/>> limit?}
    Over -->|No| Attach[🔗 Attach _artifact_ref<br/>return dict]
    Over -->|Yes| Fallback[📄 Return head/tail string<br/>artifact_ref already in it]

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef decision fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff

    class Start input
    class Field process
    class Over decision
    class Attach,Fallback ok
```

| Setting | Source | Default | Role |
| - | - | - | - |
| `tool_output_limit` | `self.tool_output_limit` (falls back to `DEFAULT_TOOL_OUTPUT_LIMIT`) | module default | Total-size and per-field budget when no context manager is set |
| `context_manager` | agent-level | — | When set, per-field pass runs with `max_field_chars=None`, and only the total-size budget applies |

A tool returning `{"results": [{"content": "a" * (limit - 10)} for _ in range(50)]}` previously returned an \~800 KB dict against a 16 KB budget; it now returns the head/tail string plus the artifact ref.

> [!NOTE]
>
> The total-size check is the same budget used to compute the head/tail `truncated` string, enforced in the same `tool_execution.py` post-truncation block — see [Tool Output Spill](https://praison.ai/docs/features/tool-output-spill.md) for the `execute_command` disk-artifact side.

***

## Common Patterns

Turn a heavy result into a headline the model can plan on.

> [tab] Big JSON → 3-line status
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents import Agent, tool
>
> def _status(result: dict) -> str:
>     return f"{result['count']} records, {result['anomalies']} anomalies"
>
> @tool(to_model_output=_status)
> def fetch_metrics(day: str) -> dict:
>     return {"count": 142, "anomalies": 3, "rows": [{"i": i} for i in range(142)]}
>
> agent = Agent(name="Metrics", tools=[fetch_metrics])
> agent.start("Summarise today's metrics")
> ```

> [tab] Web-scrape → head + tail
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents import Agent, tool
>
> def _head_tail(text: str) -> str:
>     head, tail = text[:200], text[-200:]
>     return f"{head}\n...[{len(text)} bytes]...\n{tail}"
>
> @tool(to_model_output=_head_tail)
> def read_page(url: str) -> str:
>     return "PAGE " * 5000  # long HTML — LLM only sees head + tail
>
> agent = Agent(name="Scraper", tools=[read_page])
> agent.start("Read the landing page and describe it")
> ```

> [tab] ToolResult + multimodal
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents import Agent, tool
> from praisonaiagents.tools import ToolResult
>
> @tool
> def render_report(name: str) -> ToolResult:
>     full = {"sections": ["intro", "body", "appendix"], "bytes": 40000}
>     return ToolResult(
>         output=full,
>         model_output=f"Report '{name}' ready: {len(full['sections'])} sections",
>     )
>
> agent = Agent(name="Reporter", tools=[render_report])
> agent.start("Render the quarterly report")
> ```

Opt-out is the default — no hook means the LLM sees the full string, as today.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import tool

@tool
def echo(text: str) -> str:
    return text  # no to_model_output → LLM sees the full string
```

***

## User-Interaction Flow

1. A user asks the agent to summarise a large API response.
2. Without `to_model_output`: the whole 20 KB JSON enters the LLM context on every turn → token cost and slow re-planning.
3. With `to_model_output`: the LLM sees `"142 records, 3 anomalies"`; the full JSON stays available for the display panel and any downstream tool the agent chains next.
4. Same answer, a fraction of the tokens, unchanged code elsewhere.

***

## Best Practices

> [accordion] Summarise, don't truncate blindly
>
> Return the headline the model needs to decide the next step — counts, status, top items — not a raw byte-slice that hides the signal.

> [accordion] Keep the full payload for downstream tools
>
> The full `output` still reaches display, hooks, and the next tool. Never fold data the agent chains on into the compact view only.

> [accordion] Make the builder total and cheap
>
> The hook runs on every call. Keep it fast and side-effect free — a raise falls back to the full output, but that silently loses the token savings.

> [accordion] Prefer ToolResult(model\_output=) when you already emit a ToolResult
>
> It has the highest precedence and keeps the compact view next to the data that produced it. Reach for `@tool(to_model_output=fn)` for plain-return functions.

***

## Related

> [card] [Tool Output Spill](https://praison.ai/docs/features/tool-output-spill.md)
>
> Persist large execute\_command output to disk artifacts

> [card] [Multimodal Tool Output](https://praison.ai/docs/features/multimodal-tool-output.md)
>
> Rich image/file parts alongside the text output

> [card] [Runtime Tool Result Middleware](https://praison.ai/docs/features/runtime-tool-result-middleware.md)
>
> Cross-cutting transforms on tool results

> [card] [Tool Output Store](https://praison.ai/docs/features/tool-output-store.md)
>
> Recover full outputs for arbitrary tool return values


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.