The fine-grained-tool-streaming opt-out is what actually fixes the
"Streaming completed but tool call never finished" crash loop: under
that beta an output cutoff mid-tool_use ends the stream without
content_block_stop, fast-agent's tool tracker leaves the block open,
and _raise_for_incomplete_anthropic_tools raises a RuntimeError that
bypasses the graceful stop_reason=max_tokens path and burns the retry
ladder. That shim costs no output length and is kept.
The companion max_tokens clamp is not. Its 20 000 ceiling was an
empirical observation, never a documented Mantle limit, and
re-investigation could not establish what enforces it: fast-agent
carries no 20 000 default anywhere (the matching TASK_BUDGET_MIN_TOKENS
is a validation floor for a different, unconfigured feature), no model
overlay is configured, and ModelDatabase reports
max_output_tokens=128000 for opus-4-8. Installing it would cement a
ceiling we cannot prove and silently truncate turns that might
otherwise complete.
install_max_tokens_clamp() is left in place, unwired, so it can be
re-enabled if the limit is ever confirmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two new Mantle shims, auto-installed alongside the existing pair:
- Opt out of the fine-grained-tool-streaming beta. Under it, an
output-token cutoff mid-tool_use ends the stream without
content_block_stop; fast-agent raises "Streaming completed but tool
call never finished" and burns its retry ladder against the same
wall (the observed ~700s Alan revise_workspace_file failures on
Taurus).
- Clamp default maxTokens to Mantle's observed 20 000-token server
ceiling, so the model stops gracefully (proper block close +
stop_reason=max_tokens) instead of being cut off by the gateway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every agent's MCP tool advertised the generic "Send a message to the {agent}
agent" fallback, because register_agent_tools only had the @fast.agent
decorator's description to fall back on — and no agent in the estate sets one
(0 of 34 agent modules across kottos, iolaus, mentor and dodona).
Meanwhile the description an operator actually wrote already sits in
agents.yaml and is published in the registry; _start_agent had it in scope
and simply never passed it through. Wire it to tool_description.
Fixes every agent in every deployment at once, with no per-repo edits:
scotty's tool description becomes "Systems administration expert —
infrastructure diagnostics, security hardening, and keeping everything
running" instead of "Send a message to the scotty agent".
Adds tests/test_tool_description.py pinning the resolution order
(agents.yaml > decorator > fallback) and the {agent} templating, including
that prose containing other braces is not passed through .format().
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_build_registry built one capabilities dict from the global default_model and
attached it to every entry, so any agent with an `agents.<name>.model` or
`model_capabilities` override was advertised under the wrong model — even
though server.py applies those overrides at startup. Resolve capabilities per
agent, mirroring _register_unknown_models.
Also stop emitting null context_window / max_output_tokens when
model_capabilities is absent: _register_one_model registers the model with
131072 / 16384, so the registry now advertises those effective values. The
defaults are hoisted to module constants in server.py so the two cannot drift.
Verified against mentor's config: capabilities go from
{"context_window": null, "max_output_tokens": null} to {131072, 16384}, model
name unchanged. No checked-in deployment uses per-agent overrides today, so no
currently-published model changes.
Adds tests/test_registry.py (9 tests, first coverage for registry.py) and
documents agents.<name>.model / model_capabilities, which the agents.yaml
field table omitted entirely.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fast-agent's agent.send() returns only the final assistant text, so
ImageContent produced by downstream tools during the agentic loop
(playwright screenshots, rommie desktop captures) reached the agent's own
vision model but never crossed the MCP boundary — Daedalus and lead agents
saw text-only results.
A per-request after_tool_call hook (pallas.image_passthrough, same
composition pattern as assistant_stream / loop_guard) collects every
ImageContent block from the turn's tool results; send_message then returns
a FastMCP ToolResult of [final text, *images]. Turns with no images return
the plain string — wire shape unchanged (the str-only output schema is
dropped so the union return passes through cleanly; no consumer read
structuredContent). Images cascade hop-by-hop up delegation chains with no
extra wiring: verified live playwright → dolores → harper → MCP client,
image intact at each hop.
New per-agent agents.yaml knob max_result_images (default 8, keeps most
recent, 0 disables) and pallas_result_images_total counter. Version 0.7.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Introduces `pallas.loop_guard` module that detects and halts agentic loops
where the same `(tool, args) → result` repeats consecutively, preventing
wasted LLM turns when upstream MCP servers return contradictory data.
- Add per-request `ToolRunnerHooks` tracking rolling tool-call signatures
- Halt loop after `loop_repeat_threshold` consecutive repeats (default 3)
- Collapse `max_iterations` on halt to terminate without further LLM call
- Append user-facing explanation to the turn with `stop_reason=endTurn`
- Expose `pallas_agent_loop_aborted_total{agent,reason}` counter
- Add per-agent `max_iterations` and `loop_repeat_threshold` config
- Document guard behavior, metric, and alerting query
Install a per-request `after_llm_call` hook that emits each intermediate
assistant turn as an MCP `notifications/message`, so users see
substantive text from earlier loop iterations instead of only the final
`agent.send()` return value.
Add tests covering the hook's payload shape, error handling, and
lifecycle via `install_for_request`.