You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add synthetic respond tool for chat in tool-equipped sessions (#20)
The model calls respond(message="...") instead of producing bare text,
eliminating the trust_text_intent tradeoff. Three paths:
- WorkflowRunner: respond_tool() factory returns a ready-made ToolDef
- Proxy: auto-injected when tools present, stripped on return (client
sees finish_reason="stop", never knows respond exists)
- Middleware: include "respond" in tool_names, handle in your loop
(see examples/foreign_loop.py Part 3)
Also updates docs for KV cache quantization and multi-slot support.
Closes#15
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Then configure your client to use `http://localhost:8081/v1` as the API base URL.
112
112
113
-
**Note:** The proxy trusts the model's intent when it responds with text instead of calling tools (`trust_text_intent=True`). This eliminates retry latency on conversational turns but means forge won't nudge the model if it *should* have called a tool. In our eval testing, unconditionally trusting intent dropped an 8B model from 100% to as low as 4% on reasoning-heavy workflows — which is why WorkflowRunner and middleware default to `trust_text_intent=False` (full guardrail protection). The proxy accepts this tradeoff for zero-code integration; the client's own agentic loop handles re-prompting. See [ADR-013](docs/decisions/013-text-response-intent.md).
113
+
**Note:** The proxy automatically injects a synthetic `respond` tool when tools are present in the request. The model calls `respond(message="...")` instead of producing bare text, keeping it in tool-calling mode where forge's full guardrail stack applies. The `respond` call is stripped from the outbound response — the client sees a normal text response (`finish_reason: "stop"`) and never knows the tool exists. This eliminates the `trust_text_intent` tradeoff described in [ADR-013](docs/decisions/013-text-response-intent.md) — no retries wasted on conversational turns, no accuracy loss on tool-calling turns.
When tools are present but the user sends a conversational message, small models must choose between calling a tool and responding with text. They frequently choose wrong — producing text when they should call tools, or vice versa. The `trust_text_intent` flag (ADR-013) addressed this for the proxy by trusting the model's finish reason, but this dropped 8B models from 100% to as low as 4% on reasoning-heavy workflows.
1606
+
1607
+
The respond tool eliminates this ambiguity. The model calls `respond(message="...")` instead of producing bare text. From forge's perspective, every response is a valid tool call — no retries wasted on conversational turns, no accuracy loss on tool-calling turns.
1608
+
1609
+
### Three paths
1610
+
1611
+
**WorkflowRunner:** Use `respond_tool()` from `forge.tools` to get a ready-made `ToolDef`. Set it as the terminal tool. The callable returns the message string.
**Proxy:** The respond tool is injected automatically when tools are present in the request. The client never sees it — respond calls are converted to plain text responses (`finish_reason: "stop"`) before returning. This makes `trust_text_intent` irrelevant for the proxy path.
1624
+
1625
+
**Middleware:** Include `"respond"` in `tool_names` when creating a `ResponseValidator` or `Guardrails` instance. The respond call passes validation like any other tool call. See `examples/foreign_loop.py` Part 3 for a complete example.
1626
+
1627
+
### Why this works for small models
1628
+
1629
+
Small models struggle with open-ended decisions ("should I use tools or chat?") but are good at structured choices ("which tool should I call?"). The respond tool converts an open-ended decision into a structured one. The model stays in tool-calling grammar/template at all times, which is where it performs best.
1630
+
1631
+
### KV Cache Quantization
1632
+
1633
+
`ServerManager.start()` and `setup_backend()` accept `cache_type_k` and `cache_type_v` parameters for KV cache quantization. Using Q8 (`cache_type_k="q8_0"`, `cache_type_v="q8_0"`) roughly halves KV cache VRAM, effectively doubling usable context for the same GPU memory. Measured: 36,864 → 68,608 tokens (1.86x) on Ministral 8B Q4 with no eval regression.
1634
+
1635
+
### Multi-Slot Support
1636
+
1637
+
`LlamafileClient` accepts a `slot_id` parameter to route requests to specific llama-server slots. `ServerManager.start()` accepts `n_slots` to start the server with `--parallel N`. This enables multi-agent architectures where each agent targets a dedicated KV cache slot.
0 commit comments