Run/RunStream took the new turn as a bare string, which had nowhere
to carry ContentPart attachments. Both now take an llm.Message
(Role is forced to RoleUser regardless of what the caller sets), so a
caller building a multimodal turn just fills in Content/Parts on it
instead of the loop needing a second, parallel parameter.
subagent.go and every test call site are updated to wrap their string
prompt as llm.Message{Role: llm.RoleUser, Content: ...} — SubAgent.Run
itself is untouched, it still takes a plain task string.
Message gains an optional Parts []ContentPart alongside the existing
plain-text Content, so a turn can carry text plus image/video
attachments. Content stays the single source of truth for every
existing text-only caller (sidebar.go, memory_tools.go, etc. are
untouched); Parts only matters to a provider client when non-empty.
openai and llamacpp (both OpenAI-compatible) serialize Parts into the
standard text/image_url content-array shape; llamacpp additionally
passes video through as a best-effort video_url part, since llama.cpp
itself has no video support but the whole point of this client is the
user's own OpenAI-compatible server sitting in front of a
video-capable model — the server decides whether it understands it,
not this client. anthropic converts image parts to its base64 image
content block, and rejects a video part outright with a clear error:
the Messages API has no video block type at all, so sending one would
just produce a confusing 400 instead.
ProviderCapabilities gains SupportsVideo, true only for llamacpp.
Two failure modes seen live with Qwen3.6 on llama.cpp ended turns silently
mid-task:
- The model writes its tool call as plain text inside its reasoning, the
server never parses it, and the round ends with nothing executed. The
loop now detects the markers and nudges the model to re-issue the call
for real (max 2 per turn).
- llama.cpp silently ignores the max_thinking_tokens field, so a model in
a reasoning spiral ran until max_tokens (seen live: 25k+ tokens of
nonstop thinking, ~20 min). The llamacpp client now enforces the budget
client-side during Stream: once exceeded while the round is still pure
reasoning, it cuts with FinishThinkingBudget and aborts the request
(freeing the server slot); the loop answers with its own corrective
nudge, on a separate counter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NetworkPolicy validates scheme/host and re-validates resolved IPs at dial
time and on redirects (DNS-rebinding defense), with cloud metadata
endpoints always blocked. Redact masks known credential shapes (OpenAI/
Anthropic/GitHub/AWS/Slack/Google keys, PEM blocks, JWTs) in tool output.
WrapUntrusted fences fetched web content against prompt injection, paired
with UntrustedContentInstruction for the system prompt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fragments now carry a memory type in metadata (legacy fragments count as
procedural) with SearchByType filtering, and EpisodeCapture summarizes a
finished turn with the local LLM and stores it as episodic memory, so the
agent can answer "what did we do yesterday?". Includes an E2E test against
a live llama.cpp server (gated) and taxonomy unit tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Formatting only (struct field alignment, import ordering) across the
files that didn't comply — no semantic changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Run() reported "max iterations reached" even when a valid final answer
arrived exactly on the last allowed iteration, throwing the response
away; a completed flag now distinguishes success from budget exhaustion.
- Tool schemas are marshaled once per Run/RunStream instead of once per
loop iteration — they never change between iterations.
- RunStream's content gate (which hides raw deltas during a tool-call
round) also swallowed that round's token usage, so callers only ever saw
the final round's count and context tracking lagged exactly when the
context grew fastest. Usage is now forwarded in its own chunk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- llamacpp: 4MB SSE scanner buffer (the 64KB bufio.Scanner default killed
streams whose single line exceeded it, e.g. a write tool call carrying a
whole file) and an empty-choices guard in toResponse instead of a panic;
request payload now uses bytes.NewReader (drops a full string copy).
- anthropic: Capabilities() reported a 1M-token context window for any
non-haiku model. Callers use that number to decide when to compact, so
compaction would have fired far too late and requests overflowed the
real window. Default is now the standard 200k, configurable via
Config.ContextWindow for extended-window models/plans.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Stream() path was broken end to end:
- requests always went out with "stream": false, so the SSE parser found
no data lines and every stream ended empty
- Config.Model was discarded at construction, and the agent loop never
sets req.Model, so requests carried an empty model (hard API error)
- tool-call deltas were ignored entirely: the agent never executed tools
over a stream with this provider (which also backs the ollama type)
- usage was neither requested nor parsed, so token tracking stayed at 0
Now mirrors the proven llamacpp client: stream flag + stream_options
.include_usage, per-index tool-call fragment accumulation flushed on
finish_reason, usage passthrough, a 4MB SSE scanner buffer (64KB default
kills the stream on large tool arguments), and an empty-choices guard in
toResponse instead of a panic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>