128 Commits

Author SHA1 Message Date
Ettore Di Giacinto 6eece18a6b fix(prefill): reject start-with-action, document the fragment-dependent gaps
WithStartWithAction made the first loop iteration take the startingActions
branch and skip tool selection entirely, so ExecuteTools sent no
tool-selection request at all. Prefill still succeeded, still burned a full
prefill, and primed a prefix nobody would ask for -- with no runtime symptom.
Reject it alongside forceReasoning and autoPlan, and pin the premise with a
test asserting ExecuteTools issues zero completions in that configuration.

Also document the two gaps the denylist cannot express because whether they
diverge depends on the fragment rather than the option: compaction firing
between the AutoImprove prepend and usableTools, and pickTool stashing
PendingNativeParts on NativePartsAware LLMs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:24:12 +00:00
Ettore Di Giacinto e796e1dfcf fix(prefill): reject options whose first request Prefill does not model
A Prefill that primes a different prefix than the real turn still returns
success, still burns the full prefill (54s on the target hardware), and
leaves no runtime symptom. Three options diverged silently:

- forceReasoning: the real first request is a reasoning call with an
  appended user prompt and only the reasoning tool. Rejected.
- autoPlan: the real first request is the planning decision. Rejected.
- autoImproveState: prepends a stored system prompt to the fragment
  before selection. Cheap to mirror, so supported instead of rejected.

Both rejections happen before the LLM call and name the option, matching
the unsupported-combination error ExecuteTools already returns. The doc
comment records the limitation and corrects the "exactly one LLM call"
claim, which is conditional: guidelines/guidedTools add a
GetRelevantGuidelines call, symmetrically on both sides, so prefix
fidelity holds.

Tests: the equivalence test now compares req.Tools and req.Messages with
reflect.DeepEqual rather than a role+content summary, so schemas and
ToolCalls/ToolCallID are locked too (structural compare surfaced no
existing divergence). Adds a rejection test, an autoImprove equivalence
test, and a direct no-mutation test, which the old equivalence test could
not catch since Prefill runs first on the same fragment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 12:17:03 +00:00
Ettore Di Giacinto 67f3ff4027 feat(prefill): add Prefill for one-token prompt-prefix priming
Sends the exact prefix an ExecuteTools run would send (same messages,
same normalization, same tools) capped at one output token, so a server
with prompt-prefix caching serves the next real turn from cache.

A Prefill that primes a different prefix than the real turn still returns
success, still costs the full prefill and leaves no symptom, so the tests
assert equivalence against a real ExecuteTools run rather than "it ran".
That test caught two divergences in the drafted implementation: the
sink-state tool (appended by toolSelection, not usableTools) and the
guidelines/MCP-prompt/manipulator message assembly. The latter is now a
shared buildToolSelectionMessages helper used by both paths so the two
cannot drift apart again.
2026-07-21 12:08:51 +00:00
Ettore Di Giacinto 2f48049914 refactor(tools): extract prepareAgentTools from ExecuteTools
Pure extraction, no behavior change. Prefill (next commit) needs the
identical agent-tool set, and a second copy of this construction would
drift silently.
2026-07-21 11:57:08 +00:00
Ettore Di Giacinto b618670a2f fix(mcp): boolean schemas no longer silently drop a whole tool
JSON Schema 2020-12 allows a boolean wherever a schema is allowed:
`true` means "anything allowed", `false` means "nothing allowed".
google/jsonschema-go emits exactly that for a Go `any` — an empty schema
marshals as `true` — so a [][]any field advertises "items": true.

mcpToolsFromTransport unmarshals every listed tool's properties into
langchaingo's jsonschema.Definition, which models nested schemas as a
*Definition. A bare bool fails that unmarshal:

  json: cannot unmarshal bool into Go struct field
    Definition.items.properties.items.items of type jsonschema.Definition

and the failure is a `continue`. The ENTIRE tool is dropped from the
model's tool list, leaving one xlog line behind. The MCP server is
healthy, its tool list is correct, and the model simply never sees the
tool — it just answers that it cannot do the thing. Any MCP server can
trip this, including third-party ones, since a boolean schema is
perfectly legal.

This is the same failure class as the type-array coercion already in
this file, so it is fixed the same way and in the same walk: rewrite a
boolean schema into its object equivalent before conversion. `true`
becomes {} and `false` becomes {"not": {}} — collapsing both to {} would
silently widen a deliberately-closed schema.

Covered at every schema location a boolean may appear: the property bag
itself, single-schema keywords (items, additionalProperties, contains,
not, if/then/else, propertyNames) and schema arrays (oneOf/anyOf/allOf,
prefixItems, tuple items). Each case asserts the result converts into
langchaingo Definitions, which is the thing that actually matters.

Found via dante-desktop, whose create_file tool vanished from the model's
tool list: its spreadsheet/presentation rows are [][]any.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:45:30 +00:00
Ettore Di Giacinto 3c908637a6 test(tools): lock step-content/tool-result ordering for parallel calls and sequential steps
Follow-up to #67: one step-content fire per decision step — before that
step's tool results, never per tool call — across a step with two
parallel tool calls and a subsequent sequential step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 22:46:39 +00:00
LocalAI [bot] 6c76f4bf1b fix(tools): stop leaking raw tool results and model content through the status callback (#67)
The status callback is meant for short human one-liners, but ExecuteTools
pushed three other things through it, flooding consumer chat UIs (e.g.
dante-desktop rendered every tool result twice — once as an unbounded,
unstyled "status" wall of text, once as its styled tool block; notetaker
carries a looksLikeToolResultStatus() workaround for the same leak):

- every raw tool result (statusCallback(execResult.result)) — already
  delivered on the dedicated WithToolCallResultCallback channel
- the no-tool reply/reasoning in toolSelection — already delivered via
  the reasoning callback one line below
- selectedToolFragment.LastMessage().Content — empty in the normal path
  (toolSelection strips content), so pure noise
- the "Selected N tool(s)" counter line

The assistant content that accompanies a tool selection ("I'll search
for X now…") was previously discarded outright; it now has a dedicated
channel: WithStepContentCallback fires at the step boundary, before the
selected tools execute, so consumers can render the commentary in
chronological order relative to tool results. It never fires for empty
content, for the guided (forceReasoning) path (which carries no message),
or for the turn's final reply.

Regression-tested in status_channel_test.go: the status channel stays
clean, the step content arrives on its own channel before the tool
result, and silent steps don't fire.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:45:17 +02:00
Ettore Di Giacinto b4d85ec71b fix(tools): clamp WithMaxAttempts below 1 to 1 — a 0 silently skipped all tool execution
The tool-execution loops run `for range o.maxAttempts`, so maxAttempts=0
iterated zero times: the tool was never called and an EMPTY result was handed
back to the model with no error and no warning. A caller that forwards an unset
config field (e.g. AgentOptions.MaxAttempts) straight into WithMaxAttempts hits
this — the model then flies blind and hallucinates, with nothing in the logs to
explain why. Clamp anything below 1 to 1 (the documented default single attempt).

Found while smoke-testing nib's browser tool: the agent got empty tool results
because the test harness set MaxRetries but not MaxAttempts. Real callers that
set MaxAttempts>0 (e.g. Dante = 3) were unaffected, but the silent-no-op is a
footgun worth removing at the option boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8Z5dz1nVHAQUpjb5pgGCZ
2026-07-16 11:16:45 +00:00
Ettore Di Giacinto dd07cc7f09 fix(tools): dedupe identical system messages in normalizeSystemMessages
Callers (nib) re-append the same system prompt to a persistent fragment every
turn; merging N identical copies into the position-0 block grew the prompt
prefix each turn and defeated the server's KV prefix cache — full re-prefill of
the whole conversation every message (measured 24s@1808tok, 82s@2451tok on CPU).
Dedupe identical system contents so the prefix stays stable; distinct system
parts (e.g. the force-text-reply directive) are still preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:45:35 +00:00
Ettore Di Giacinto 013116cb8f feat(tools): forward tool-result images to the model when enabled; preserve raw result in parallel path 2026-07-12 08:26:16 +00:00
Ettore Di Giacinto 8501a329e3 feat(tools): helper to forward MCP tool-result images as a follow-up user turn 2026-07-12 08:21:56 +00:00
Ettore Di Giacinto b249a9239d feat(tools): add WithToolImageForwarding option (default off) 2026-07-12 08:18:26 +00:00
LocalAI [bot] 588db3bcba fix(tools): don't mask empty streaming tool-selection as %!w(<nil>) (#65)
When the streaming tool-selection decision returned no tool calls and empty
content on every attempt, the retry loop exhausted with lastErr still nil, so
the final fmt.Errorf("...: %w", maxRetries, lastErr) rendered the literal
"%!w(<nil>)" — hiding the real cause.

This reliably bites image turns with reasoning models (e.g. gemma-4-e2b via
LocalAI): the model streams reasoning but its reasoning exhausts the output
budget before any visible content, finishing with finish_reason=length and no
content. The empty-content path then retried three identical truncations and
reported %!w(<nil>).

Capture finish_reason from StreamEventDone and, in the empty-content branch:
  - finish_reason=length: fail fast with an actionable error (retrying
    truncates identically) instead of looping into a nil wrap.
  - otherwise: keep retrying, but always record a real lastErr so the final
    error is never a nil wrap.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 01:09:16 +02:00
LocalAI [bot] a15985a850 feat: native image/audio/video content parts (LocalAI) (#63)
* feat(fragment): typed Multimedia + native audio/video parts (send-once)

* feat(localai): serialize native input_audio/video_url parts (send-once stash)

* feat(loop): set native-parts stash at streaming + tool-decision seams (leak-free)

* fix(loop): forward SetPendingNativeParts through the usage-counting wrapper (+ marshal robustness)

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-05 12:27:58 +02:00
LocalAI [bot] 028ba66a8b fix: don't abort a turn when one MCP session can't list tools (#61)
usableTools returned an error if ANY mcpSession's ListTools/ListPrompts failed,
so a single session that is momentarily unavailable (mid-reconnect, or being
closed/rebuilt by a config reload — surfaced by the go-sdk as "client is closing:
hanging GET: failed to reconnect") aborted the whole turn. This is what broke
/learn in dante-desktop. Skip the unhealthy session for this turn and log a
warning; its tools return once it is healthy again.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.11.0
2026-07-04 23:59:04 +02:00
Ettore Di Giacinto 0518bfb859 feat(client): SetTemperature on LocalAIClient
Completes parity with OpenAIClient so wiz's per-agent-type LLM
factory (which passes temperature) can switch client
implementations without losing the override.
2026-06-30 22:52:59 +00:00
Ettore Di Giacinto f046d8855b feat(client): SetReasoningEffort on LocalAIClient
Gives LocalAIClient parity with OpenAIClient's reasoning_effort
passthrough so callers can switch between the two client
implementations without losing that lever — needed because
LocalAIClient is the client that actually parses LocalAI/vLLM's
"reasoning" response field (OpenAIClient only knows go-openai's
deprecated "reasoning_content" key, so it silently drops it).
2026-06-30 22:49:44 +00:00
LocalAI [bot] bf4010d310 fix(tools): carry the parked reply text in the WithOnPark callback (#59)
The no-tool reply the model produces right before a park gate was only
recorded inside the fragment; embedders had no way to read it at park
time. nib worked around this by capturing the reasoning callback, but
toolSelection fires that callback with resp.ReasoningContent (reasoning
tokens, empty for most models), not the reply text — so parked replies
(e.g. the answer to a mid-run injected user message) were silently
dropped by the UI.

WithOnPark now passes the assistant reply text that preceded the park:
the no-tool reply at the agents-still-running gate, or "" at the
sink-state gate where the reply is produced after the loop.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:23:29 +02:00
LocalAI [bot] 037dd82966 feat(agent): add AgentDispatcher execution seam (#58)
Introduce an opt-in seam so embedders can execute spawned sub-agents
out-of-process (e.g. dispatch to a remote worker) while cogito keeps
owning all lifecycle bookkeeping: registration, status, done, callbacks,
completion injection, and detach. A nil dispatcher preserves the existing
in-process behavior exactly; ErrDispatchFallback lets a dispatcher defer
a given spawn back to the in-process path.

New API: AgentRunSpec, AgentEvent, AgentDispatcher, ErrDispatchFallback,
WithAgentDispatcher. runAgent branches on the dispatcher; spawn builds an
AgentRunSpec (resolved persona/tools/model) and an optional Emit stream.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 22:52:06 +02:00
LocalAI [bot] 7c13c87a21 feat(client): per-request reasoning_effort passthrough (#57)
Add OpenAIOptions.ReasoningEffort, attached to every chat-completion request as
the OpenAI-standard "reasoning_effort" field (Ask, CreateChatCompletion,
CreateChatCompletionStream). Mirrors the existing Metadata passthrough.

Why: Metadata ({"enable_thinking":"false"}) only disables thinking when the
model's chat template honors that flag. Reasoning models whose GGUF template has
no enable_thinking toggle (e.g. LFM2.5) ignore it and keep reasoning — but they
DO honor reasoning_effort:"none". This gives callers the portable lever to
silence reasoning regardless of the template. Unset = field omitted (no behavior
change).

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 14:30:15 +02:00
LocalAI [bot] 77024e447c feat(client): per-request metadata passthrough (disable thinking/reasoning) (#56)
Add an optional metadata object that is attached verbatim to every
chat-completion request as the OpenAI "metadata" field. Backends such as
LocalAI use it for per-request flags, e.g. {"enable_thinking": "false"}
to disable a reasoning model's thinking.

- OpenAIOptions.Metadata + OpenAIClient.metadata, applied on all call
  paths: Ask, CreateChatCompletion, CreateChatCompletionStream.
- AgentDefinition.Metadata for per-agent-type overrides; resolveLLM now
  passes it to the agent LLM factory and fires the factory on a
  metadata-only override (previously only a model override did).
- WithAgentLLMFactory signature gains a metadata map (breaking change to
  the factory type).

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 23:23:12 +02:00
LocalAI [bot] fe7fd5de11 feat: expose cumulative token usage per ExecuteTools run (#55)
* feat(agent): expose AgentState.Background for spawned background agents

Add an exported Background flag on AgentState, set true for background
spawns (spawn_agent background=true) and false for foreground ones. This
lets embedders tell unattended background work apart from a foreground
sub-agent whose result is consumed inline — e.g. to auto-notify on
completion only for background agents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: add per-run token usage accumulator (CumulativeUsage)

Adds a counting LLM decorator and a CumulativeUsage field on Status so a
full ExecuteTools run's token usage can be summed and exposed. Preserves
StreamingLLM so wrapping does not disable streaming.

* refactor: harden counting stream forwarder and document usage limits

Buffer the forwarded stream channel and make the send context-aware to
match the client convention and prevent goroutine leaks. Document that
streaming-path usage is unpopulated by the bundled clients today.

* feat: expose cumulative token usage on the ExecuteTools result

Wraps the run LLM in a counting decorator and stamps the summed usage onto
the returned fragment's Status.CumulativeUsage via a deferred named return,
covering all exit paths. Each sub-agent run reports its own total.

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:23:19 +02:00
Ettore Di Giacinto 81ce500344 feat(agent): expose AgentState.Background for spawned background agents (#54)
Add an exported Background flag on AgentState, set true for background
spawns (spawn_agent background=true) and false for foreground ones. This
lets embedders tell unattended background work apart from a foreground
sub-agent whose result is consumed inline — e.g. to auto-notify on
completion only for background agents.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:16:48 +02:00
Ettore Di Giacinto a8b4503396 fix(agent): keep detached/background sub-agents alive after parent turn ends (#53)
* fix(agent): keep detached/background sub-agents alive after parent turn ends

A sub-agent's context was a child of the parent turn context (r.ctx), so
when an embedder cancelled its per-turn context right after ExecuteTools
returned — the normal end-of-turn cleanup — a just-detached (or
background) agent was cancelled too, defeating the whole point of
detaching.

Parent the sub-agent context off context.WithoutCancel(r.ctx): it keeps
the context's values but no longer propagates the parent's cancellation.
Foreground cancellation is unaffected because the foreground select
already watches r.ctx.Done() and calls cancel() explicitly; detached and
background agents now run to completion independently and remain
cancellable via AgentState.Cancel.

Add TestDetachedAgentSurvivesParentCancel, which fails on the old
behavior (agent cancelled after parent cancel) and passes now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gofmt cancel_inflight_test.go under Go 1.25

Go 1.25's gofmt expands a single-line function body that earlier versions
left inline, so the repo-wide `go fmt ./...` CI check failed on this file
(inherited from #52). No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 19:01:57 +02:00
Ettore Di Giacinto d7c169e20a fix(tools): abort retry loops on context cancellation (#52)
ExecuteTools' retry loops slept on a non-cancellable
time.Sleep(attempts+1 seconds) backoff and re-issued the LLM call even
after the caller's context was cancelled. A Ctrl+C therefore took up to
several seconds to take effect because an in-flight/retrying call kept
running through the backoff before noticing cancellation.

Add a backoffOrCancel(ctx, attempt) helper that waits the backoff but
returns ctx.Err() immediately when the context is done, and check
ctx.Err() at the top of each retry loop. The interrupt now propagates in
~0.3s.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:51:10 +02:00
Ettore Di Giacinto 67811faaf5 feat: subagent enhancements (#51)
* feat(agent): add AgentID to SessionState for sub-agent tool routing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent): route sub-agent tool calls through parent callback + MCPs with AgentID

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent): add AgentDefinition type and WithAgentDefinitions option

* feat(agent): apply agent definition (prompt/tools/limits) on spawn

* feat(clients): add temperature support to OpenAI client

* feat(agent): per-agent model+temperature via WithAgentLLMFactory

* feat(agent): per-agent injection channel and AgentManager.Inject

* feat(agent): add unified send_agent_message resume/inject tool

* feat(agent): detachable foreground spawns + AgentManager.Detach

Register every foreground sub-agent and run it in a goroutine so an
embedder can promote it to the background. spawn_agent now selects on
agent.done (completed -> return result like the old synchronous path),
agent.detach (promoted -> return the ID immediately, goroutine keeps
running), or ctx.Done. Extract the shared goroutine body into runAgent
and add derefFragment so foreground and background share lifecycle
bookkeeping. Stamp withAgentIDStamp(agentID) with the real registry ID
on BOTH foreground and background registered agents so sub-agent tool
calls carry the correct AgentID. Add AgentState.detach and
AgentManager.Detach (errors on unknown / non-detachable agent;
non-blocking send).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(agent): add WithAgentSpawnCallback and AgentState.Type for running-agent signal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(agent): cover definitions, AgentID approval, and spawn callback in Sub-Agent Spawning suite

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:17:40 +02:00
Ettore Di Giacinto f2c20252ba fix(mcp): handle non-text tool result content without panicking (#50)
* fix(mcp): handle non-text tool result content without panicking

An MCP tool result is a slice of mcp.Content, which may contain image,
audio, resource-link or embedded-resource blocks in addition to text.
mcpTool.Execute asserted every block to *mcp.TextContent unconditionally,
so any tool returning media (e.g. osmmcp get_map_image, NASA image tools)
triggered:

  panic: interface conversion: mcp.Content is *mcp.ImageContent, not *mcp.TextContent

The panic was on an agent goroutine with no recover, taking the whole
host process down (mudler/LocalAI#10101).

Extract a contentToString helper that type-switches over every mcp.Content
variant: text is concatenated verbatim (preserving prior behavior), media
and resource blocks are summarized with a descriptive marker, and unknown
types are logged instead of crashing.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(tools): never send consecutive assistant messages to the backend

cogito's tool loop appends an assistant "reasoning" message (toolSelection
no-tool / no-tool-selected paths) on top of a fragment that may already end
with an assistant message. The resulting decision request then ends with two
assistant messages in a row, which llama.cpp via LocalAI rejects:

  500 InvalidArgument: Cannot have 2 or more assistant messages at the end of the list

Because decisionWithStreaming retries the identical request, all attempts hit
the same error and the whole reviewer/tool flow fails (the Test E2E
"refine a content with a search tool" spec, and any agent feeding an
assistant-terminated fragment into a tool loop).

Add mergeConsecutiveAssistantMessages, applied alongside normalizeSystemMessages
at the single boundary where both decision() and decisionWithStreaming() build
the request. It collapses any run of consecutive assistant messages into one,
concatenating content and preserving tool calls, so the list can never end with
two assistant messages.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(e2e): compare tool-call arguments by JSON value, not raw bytes

The "should select a tool" spec re-marshaled the parsed arguments map to
compact JSON and compared it byte-for-byte against the tool-call message's
Arguments via HaveExactElements. The message preserves the model's raw
arguments string, whose whitespace differs ({"city": "San Francisco"} vs
{"city":"San Francisco"}), so the assertion failed deterministically
(all 5 flake attempts) even though the tool and arguments were correct.

Assert length, type and name explicitly and match the arguments with
MatchJSON, which compares JSON semantically and ignores insignificant
whitespace and key ordering.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
v0.10.0
2026-05-31 10:53:16 +02:00
LocalAI [bot] 8b4b19644c feat(agent): add WithOnPark/WithOnResume to signal loop park/resume transitions (#49)
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 18:39:42 +02:00
LocalAI [bot] b946aab73b feat(agent): add WithPendingWork to park the loop for embedder-owned background work (#47)
Embedders running their own background goroutines (which cogito's
AgentManager knows nothing about) previously had no way to keep the
ExecuteTools loop alive while that work was pending: once the LLM
selected no tool / a sink state, the loop returned.

WithPendingWork(fn func() bool) lets such an embedder express "I still
have work in flight". Both park gates in ExecuteTools — the no-tool path
(~tools.go:1343) and the sink-state path (~tools.go:1451) — now broaden
their condition so the loop blocks on the message-injection channel when
EITHER cogito's own AgentManager has running agents OR the embedder's
predicate returns true. The embedder wakes the loop by injecting a
message via WithMessageInjectionChan.

Because the park branch blocks on the injection channel, a
pendingWork-only caller (no agent spawning) would otherwise hit a
nil-channel block releasable only by ctx. The channel is now
auto-created (same buffer size of 16) when pendingWork is set, mirroring
the existing agent-spawning setup.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:20:02 +02:00
LocalAI [bot] fa3b541f25 chore(ci): fix gofmt + gosimple lint failures on main (#48)
CI's fmt gate (`go fmt ./...` must be a no-op) and the golangci-lint
job (gosimple default linter) were red on main, independent of any
feature work:

- gofmt: clients/localai_client.go, prompt/prompt.go, stream.go were
  not gofmt-clean.
- gosimple S1011 (autoimprove.go): a loop appending each message is
  replaced with a single variadic append.

No behaviour change. Verified: `go fmt ./...` no-op, `go vet ./...`
clean, `go build ./...` ok, and golangci-lint v1.64.8 (the version CI
pins) exits 0.

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:19:50 +02:00
LocalAI [bot] c284192af5 feat(agent): add WithAgentCompletionFormatter to control the background-completion wake message (#46)
When a background sub-agent finishes, cogito injects a fixed prose
notification ("Background agent <id> has completed.\nTask: …\nResult: …")
into the parent loop. Host applications that drive their own UI want to
control exactly what the parent LLM sees on wake — a clean structured
summary, or a marker they can intercept and render themselves — rather
than leaving the model to re-parse prose.

This adds WithAgentCompletionFormatter(func(*AgentState) string), threaded
through to the spawn goroutine's injection site. The message-building is
extracted into a pure formatAgentCompletion helper: nil formatter keeps
the existing default prose (backwards compatible), a set formatter fully
controls the injected content (an explicit "" is honoured verbatim).

Tested:
- formatAgentCompletion: default completed/failed prose + custom override
  + explicit-empty honoured (pure unit tests)
- spawnAgentRunner background path: the formatter's output is exactly the
  message injected on the channel (end-to-end via an inline mock LLM)

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-05-29 14:37:04 +02:00
Ettore Di Giacinto b9560ea675 feat(mcp): add WithMCPToolFilter for per-tool gating
mcpToolsFromTransport now accepts an optional filter that's invoked
once per (session, tool) pair during the initial tool-discovery pass;
tools for which the filter returns false are dropped from the agent's
discovered set and the LLM never sees them.

Use case: per-user enable/disable of remote MCP server tools (notary's
customize tab persists per-user toggles in user_mcp_server_tools and
plugs them into the filter at chat time). The filter keys on the
*mcp.ClientSession pointer so each registered server can have its own
enable map without cross-contamination.

A nil filter is the default and is equivalent to "always allow", so
existing WithMCPs callers see no behavior change.
2026-05-08 20:28:27 +00:00
Ettore Di Giacinto 678d2f181f mcp: coerce nullable types recursively into all schema locations
The first pass only walked properties + items. JSON Schema 2020-12
puts schema nodes in many other places — composition keywords
(oneOf/anyOf/allOf), tuple items, additionalProperties / patternProperties,
$defs / definitions, contains, not, if/then/else, propertyNames,
prefixItems. Any of those can carry a nullable Go-typed field that
gets emitted as "type": ["null", "X"] by gomcp v1.4+, and any
unflattened type-array silently fails the langchaingo unmarshal,
dropping the tool.

Refactor into coerceSchema(node) so a single visitor covers every
location uniformly, then recurses. Coverage by tests:
  - properties (object), items (single + tuple), additionalProperties
  - oneOf, anyOf, allOf members
  - $defs / definitions named schemas

Test asserts deep-walk completeness via containsTypeArray helper
(no JSON-array type left anywhere). When the schema shape is
representable in langchaingo's Definition (no oneOf, no tuple items),
also asserts the round-trip into Definition succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:47:06 +00:00
Ettore Di Giacinto 0ced384a0c mcp: export CoerceNullableTypes for downstream contract tests
So callers (e.g. notary) can run the same workaround in their
tool-discovery tests and assert their MCP servers stay importable
into the agent's tool list. Same body as the unexported version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:40:26 +00:00
Ettore Di Giacinto d38d41d11e fix(mcp): coerce nullable JSON-Schema 2020-12 type-arrays
modelcontextprotocol/go-sdk v1.4+ emits "type": ["null", "X"] for
nullable fields like Go []string slices (even with omitempty). Our
downstream langchaingo/jsonschema.Definition.Type is a single string,
so the property unmarshal fails on the type-array, the surrounding
catch silently `continue`s, and the entire tool drops out of the
agent's tool list.

Effect in prod: notary registered `search`, `get_timeframe`,
`create_page`, and `update_page` MCP tools (all four take []string
inputs) but the agent never saw them — chat fell back to list_pages
+ read_page only, references panel went empty.

Add coerceNullableTypes: walk Properties before unmarshal and rewrite
"type": ["null", "X"] → "type": "X" (first non-null member, fallback
to first member). Recurses into nested object properties + array
items so deeper schemas are normalised too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:38:26 +00:00
Ettore Di Giacinto 98bea04d29 feat(clients/localai): add SetMetadata for per-request flags
Adds a metadata map and SetMetadata method on LocalAIClient. When set, the
map is serialized as a top-level "metadata" object in the LocalAI request
body alongside the existing optional "grammar" field. This lets callers
override per-model defaults on a single call (e.g.
{"enable_thinking": "true"}) for both unary and streaming completions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 00:51:07 +00:00
Ettore Di Giacinto ee35d19a4d feat: add sub-agents support (#45)
* feat: add sub-agents support

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* add example

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-31 18:20:58 +02:00
Ettore Di Giacinto 8548e1e1aa feat: add auto-improver
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-19 17:35:38 +00:00
Ettore Di Giacinto a09552a3a4 fix: only check content
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-16 00:41:54 +00:00
Ettore Di Giacinto 1952b0d9ce fix: recover from models that give invalid output
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-16 00:33:55 +00:00
Ettore Di Giacinto 63abdec718 feat: support streaming mode (#44)
* feat: support streaming mode

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat: support streaming mode for tool calls

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat: add decision with streaming

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-15 23:29:27 +01:00
Ettore Di Giacinto 42271c7e1a re-inforce to not use tools if maxIterations is reached
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-13 17:02:02 +00:00
Ettore Di Giacinto 9b8f2eac82 fix: consume message IF in sink mode
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
v0.9.4
2026-03-13 10:06:01 +00:00
Ettore Di Giacinto 3bc79f756d fixups v0.9.3 2026-03-09 14:50:14 +00:00
Ettore Di Giacinto e073d115bd fix: normalize messages and join system messages automatically
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-03-06 20:24:29 +00:00
Richard Palethorpe 070e9f69c0 feat: Add grammar constraint for LocalAI (#43)
Signed-off-by: Richard Palethorpe <io@richiejp.com>
v0.9.2
2026-02-27 13:12:23 +01:00
LocalAI [bot] b766916377 fix: assign to intentionMessages instead of messages (ineffectual assignment) (#42)
The code was assigning to 'messages' but then using 'intentionMessages'
throughout the rest of the function. This caused a golang-ci lint error.

Closes #42
2026-02-26 00:48:59 +01:00
LocalAI [bot] 37b69f1aec feat: add automatic conversation compaction based on token threshold (#41)
* feat: add automatic conversation compaction based on token threshold

This commit adds automatic conversation compaction to prevent context overflow
during long-running tool execution sessions.

Key changes:
- Added LLMUsage struct to track token usage from LLM responses
- Modified LLM interface to return token usage alongside Fragment
- Added WithCompactionThreshold option to set token count threshold
- Added WithCompactionKeepMessages option to configure recent messages to keep
- Added compaction logic in ExecuteTools after LLM calls
- Added helper functions: compactFragment, checkAndCompact, estimateTokens
- Added PromptConversationCompaction for generating conversation summaries
- Updated OpenAI and LocalAI clients to return token usage
- Updated mock client for testing

When compactionThreshold is set (> 0), the conversation will be automatically
compacted when estimated token count exceeds the threshold. The compaction
generates a summary of the conversation history using an LLM call while
preserving recent messages.

Signed-off-by: Autonomous Coding Agent <agent@autonomous>

* fix: use actual usage tokens from LLM response for compaction

- Store LastUsage in Status struct from LLM responses
- checkAndCompact now uses actual TotalTokens from LLM response
- Removed estimateTokens function (no longer needed)
- Fallback estimate only used on first iteration when no usage data available

* fix: capture usage tokens after sink state LLM call for compaction

The sink state handling was not capturing usage tokens from the LLM response,
which meant the compaction check would use the rough estimate instead of
actual usage tokens. This change ensures LastUsage is stored after the
llm.Ask call in the hasSinkState block, allowing proper token-based compaction.

* fix: move compaction check to beginning of tool loop

- Removed compaction check after max iterations (not needed)
- Removed compaction check after sink state (not needed)
- Added compaction check at beginning of tool loop (after totalIterations++)
- Uses actual usage tokens from LLM response

* fix: update Ask to return usage tokens from LocalAIClient

* fix: set LastUsage in Ask function return fragment

This addresses reviewer feedback that Ask() should automatically update
the Fragment's LastUsage, not have callers do it. The OpenAIClient and
LocalAIClient Ask functions now set Status.LastUsage before returning.

* refactor: Ask() updates Fragment.Status.LastUsage directly

Instead of returning LLMUsage from Ask(), the LLM clients now update
the Fragment's Status.LastUsage directly. This simplifies the interface
and ensures usage is always tracked in the fragment.

Changes:
- LLM.Ask() now returns (Fragment, error) instead of (Fragment, LLMUsage, error)
- Clients (openai_client.go, localai_client.go) set LastUsage on the returned fragment
- Mock client also updated to set usage in Status
- All callers updated to use new 2-value return signature

This addresses reviewer feedback on PR #41.

* Apply suggestion from @mudler

* Apply suggestion from @mudler

* Apply suggestion from @mudler

* Apply suggestion from @mudler

* Apply suggestions from code review

* Apply suggestion from @mudler

* test: add mocked tests for compaction functionality

- Add DefaultPrompts() function to prompt package for tests
- Export CompactFragment and CheckAndCompact functions for testing
- Add comprehensive unit tests for compaction logic using mocks
- Remove duplicate Ginkgo compaction tests that have import issues

* test: add compaction tests to tools_test.go suite

Add Ginkgo tests for compaction functionality within the existing
tools_test.go suite. Tests cover:
- No compaction when threshold is disabled (0)
- No compaction when tokens below threshold
- Compaction when token threshold is exceeded
- Parent fragment preservation after compaction
- Status preservation after compaction
- Rough token estimate usage when LastUsage is not set

This addresses the reviewer's request to keep tests consistent with
other ginkgo tests in tools_test.go.

* chore: run go fmt and add compaction docs to README

* Make CompactFragment and CheckAndCompact private

Per reviewer request:
- Changed CompactFragment to compactFragment (private)
- Changed CheckAndCompact to checkAndCompact (private)
- Removed tools_compaction_test.go (tests should be in tools_test.go)

The compaction functionality is still available internally via
ExecuteTools with WithCompactionThreshold option.

* chore: remove exported functions from README, keep them private

* chore: verify all changes applied - build passes

* chore: verify build and vet pass

---------

Signed-off-by: Autonomous Coding Agent <agent@autonomous>
Co-authored-by: Ettore Di Giacinto <mudler@users.noreply.github.com>
2026-02-26 00:02:29 +01:00
Ettore Di Giacinto 070948df04 chore: add sleeps between re-attempt in case of failure
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-23 11:19:54 +01:00
Ettore Di Giacinto 3e74a9b3ac feat: add Extract for extracting typed structs
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-02-22 22:46:53 +00:00