Files
LocalAI/docs/content/features/chats-agents.md
T
mudler-agentandEttore Di Giacinto 7904b3d0ab feat(addons): declare typed host capabilities (#13034)
* feat(addons): model execution status observations

Keep observation lifetimes explicit before runtime integration. The pure
reducer rejects retries and stale replies using exact binding identity.

Cover all eleven trace families, the complete transition matrix and
bounded histories with safety and liveness checks.

Assisted-by: nib:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): visualize execution status traces

Generate a self-contained viewer from the real reducer and shared traces.
Check independent expectations, coverage and bounded invariants before
exporting data. Detect stale artifacts and nondeterministic generation
without maintaining a second reducer in JavaScript.

Assisted-by: nib:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): honor the viewer artifact root

Keep default viewer output under SDD_LOCAL_ROOT when configured.\nLet go generate use the same default without forcing a source artifact.\nRetain the portable fallback and explicit output and check semantics.

Assisted-by: nib:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): add typed extension contracts

Validate optional home commands, execution bindings and requested host
operations without changing manifest versions or granting authority.
Decode bounded execution status payloads conservatively and document
strict staging compatibility for older hosts.

Assisted-by: nib:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): reject noncanonical extension keys

Reject case aliases before typed decoding can override canonical fields
or bypass presence checks. Keep locale maps outside exact-key checks.

Add regression coverage for status, declaration objects, and nested
entries. All status errors return unknown readiness and isolation.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): reject root enums for status tools

Status tools must accept the empty object. The supported schema enums
contain only strings, so a root enum excludes every object.

Reject root enums only for execution status bindings. Cover incompatible
root constraints and optional properties without changing shared validation.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): project typed home commands

Resolve declared home destinations and display metadata in the installed
Apps view. Pin packaged command icons to immutable asset URLs.

Cover strict declaration transport, immutable receipts, exact health echo,
and process restart. Keep legacy packages and disabled installed intent.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): preserve typed lifecycle declarations

Copy execution and host-operation declarations from installed receipts.
The existing capability completeness test exposes the missing fields.

Exercise real staged Manager upgrades, activation rollback and recovery.
Check installed Apps metadata through pinned mTLS frontends for each actor.
Verify mutation isolation and name receipt replay coverage accurately.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): stabilize fixture archive digests

Map iteration changes tar entry order between identical publishes.
Sort fixture filenames so unchanged packages retain their digest.

Check repeated publish bytes, digests and lexical archive entry order
with and without a manifest transform. Register the regression case.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): resolve explicit host policy grants

Load strict administrator policy files once before application startup.
Honor explicit empty overrides without reading ignored policy files.
Copy programmatic and manager grants to isolate startup authority.

Keep the legacy CLI fallback until the official deployment policy ships.
The generic resolver already defaults to empty grants.

Verify config, CLI, hostops and execution-status checks with GOMAXPROCS=2,
-p=2 and -ldflags="-s -w" to fit the available build disk.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): select explicit deployment grants

Move official host grants out of the generic executable into reviewed
host deployment policy. Stock Docker explicitly selects the shipped file;
unconfigured binaries and nil or empty programmatic grants grant nothing.

Document the standalone upgrade break and deliberate operator selection.
Do not infer consent from installed addons or merge explicit overrides.
Guard policy packaging and generic configuration boundaries with tests.

Verify executionstatus, viewer -check, config, CLI, packaging and root
architecture tests with auth, GOMAXPROCS=2 and bounded build parallelism.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): gate activation on required host ops

Refuse unmet required requests before package publication or runtime
withdrawal. Keep installed intent unavailable when startup grants change.
Optional requests permit degraded operation and never create authority.

Carry bounded admin diagnostics through Apps and the owner carrier.
Keep startup policy immutable and broker authorization authoritative.

Verify executionstatus, viewer -check, lifecycle, packages, updates,
hostops, Apps and gateway offline with auth and bounded parallelism.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(addons): guard host operation registry parity

Compare startup admission names against real builtin registrations so
registry drift cannot silently change request satisfaction.

Cover dynamic progress and stop operations and connector inbound.
Reject missing, extra, duplicate and case-changed names with negative
controls. Record both regression cases in the suite inventory.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): fence status observation effects

Keep observation ownership in admitted runtime operations. Deny all
correlated callbacks before dispatch, including inbound-class callbacks.
Reject status outputs before callers can open or adopt artifact handles.

Retain bounded denial correlations until the runtime endpoint closes so
late callbacks cannot recover ordinary authority. Preserve independent
ordinary operations and document the dedicated read API for T9.

Verify the status model, viewer, and race-enabled gateway, hostops and
runtime suites with the pinned offline environment.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): drain callbacks before retirement

Server.Close does not wait for active callback requests. Clearing retired
observation correlations first can restore inbound authority.

Stop endpoint admission before closing connections. Drain admitted requests
before releasing correlations, and refuse dispatch after Stop. Keep locks
out of handlers and cleanup waits so cancellation cannot deadlock teardown.

Cover pending inbound callbacks, decoded authorization paused across Stop,
and handlers that ignore cancellation. Preserve ordinary authorized work
on other endpoints. Document delayed reclamation under the existing bound.

Verify the status model and viewer, then race-test gateway, hostops,
runtime, the model and root architecture checks with auth enabled.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): bind owner status to exact clients

Add authenticated, versioned status reads over the owner carrier.
Revalidate actor permissions and provider selection before dispatch and
before returning an observation. Keep status separate from authority.

Admit the captured client against its immutable identity under the
withdrawal lock. Never dispatch an old binding through a replacement.
Generate runtime operation IDs on the owner and retain the T8 effect fence.

Cover forged identities, strict DTOs, stale replies, restart ordering,
wire discriminators, and unchanged model exclusion of uiOnly tools.
Document the T10 callback and request-shell integration requirements.

Verify the model and viewer, then race-test gateway, runtime, tools,
and the model with auth enabled.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(addons): compose owner status with runtime

Exercise installed addon processes through the authenticated mTLS carrier.
Keep runtime clients real while channel barriers control lookup ordering.

Reject captured clients after same-generation replacement before dispatch.
Reject completed status reads when replacement precedes owner consumption.
Prove fresh bindings succeed and record named tools and host operation IDs.

Verify the model and viewer, then race-test gateway, runtime, tools,
the model, and the protocol fixture package with auth enabled.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(studio): scope execution status observations

Run the execution status reducer within one explicit request lifetime.
Coalesce consumers without retries and withdraw stale or canceled reads.

Keep owner admission and production lifecycle integration separate for
review before wiring the shell into requests and turns.

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

* fix(studio): isolate consumer cancellation

Consumer cancellation during validation must not withdraw a valid
observation from other consumers. Return unknown locally before shared
invalidation, while preserving parent closure and freshness failures.

Cover cancellation and deterministic deadline expiry with one-read
reducer traces and live-consumer checks.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): track selection mutation epochs

Detect selection ABA at config and owner mutation boundaries. Clone
provider pins for Studio readers and share synchronized selection state
across config value copies.

Track lifecycle publication, candidate rollback and recovery transitions.
Keep reads and unchanged publications stable. These lifetime epochs do
not replace current-incarnation checks or grant execution authority.

Verify race tests for config and the add-on Manager, plus application
compilation with auth tags. Request binding remains separate work.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): expose owner binding transport

Add a host-only binding API with an optional owner resolver. Forward
conversation claims over authenticated mTLS with strict versioned DTOs.
Compare full bindings during validation and deny missing resolvers.

Keep ordinary tool execution unchanged through an optional backend.
Test trusted resolver fixtures, stale bindings, denial and compatibility.
Application authority and store revisions remain a separate milestone.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(studio): fence selection snapshots with epochs

Read actor-scoped records and opaque mutation tokens under store locks.
Invalidate tokens after successful eligibility changes, including ABA.
Keep reads, rejected writes and turn history from invalidating tokens.
Clone mutable records so callers cannot bypass mutation tracking.

Support authoritative revisions only for memory-only JSON stores.
File stores lack exclusive ownership; GORM lacks persisted revisions.
Fail closed for both rather than trust frontend-local state.
Ordinary chats remain unchanged; application integration stays separate.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(studio): enforce persistent JSON ownership

Acquire lifetime sidecar locks before loading or repairing JSON data.
Enable selection revisions only under cooperating exclusive ownership.

Close drains operations before releasing ownership and rejects later use.
Clean up unpublished stores when application initialization fails.
Retain published locks through Shutdown because HTTP users may remain live.

Document coordinated upgrades and the advisory local-filesystem boundary.
Keep GORM revisions unsupported pending a separate approved design.

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Codex:gpt-5

* test(application): isolate warning log capture

Run warning assertions in isolated test subprocesses. The suite leaves
background log producers running, which race with global logger changes.

Keep the parent logger unchanged and check child exits and completion
markers. Preserve pin persistence and ownership warning assertions.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(studio): resolve local owner execution bindings

Resolve execution providers from owner-held chat and agent snapshots.
Fence bindings with selection epochs, mutation tokens and live runtime
identity. Recompute every component during status authorization.

Keep named-user and SQL observations unavailable until authoritative
access and shared selection revisions can detect ABA changes.

Drain restart test producers before releasing JSON store ownership.
Register six product resolver cases in the gateway suite inventory.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(studio): bound product scopes to chat turns

Keep request-local product state alive through tool calls, checkpoints
and persistence. Give chat priming its own scope instead of seeding the
next turn with a closed observation.

Exercise one reducer read per scope across prompt and tool consumers.
Check closure of pending reads on normal completion and interruption.
Provider wiring and safety projection remain separate work.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(studio): consume typed execution providers

Replace the Sandbox-specific status heuristic with owner-bound typed reads.
Share one reducer observation across turn consumers and close it at the
outer scope boundary. Keep priming and standalone requests independent.

Sandbox exposes a separate callback-free typed read while preserving its
private Settings payload. Unknown backends never imply isolation.
Provider isolation does not prove host network-policy enforcement.

Local-owner JSON bindings are supported. Named-user and SQL bindings
remain explicitly unavailable and fail closed.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(application): isolate auth startup lifetime

Shutdown does not join every background producer. Auth startup tests
can therefore delete their temporary directory while a writer remains.

Run each case in a bounded child process. The parent waits for child
exit before deleting the fixture, preserving every startup assertion.
Production shutdown and JSON ownership retention remain unchanged.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(ui): resolve installed Home app commands

Project Home navigation from installed addon declarations, not product IDs.
Reserve native and skill names and suppress every colliding addon alias.

Re-read the authenticated registry before navigation and reject stale
actors, generations, immutable entries and destinations. Keep mounted
consumers polling through disabled and failed states. Abort pending reads
on actor changes and ignore replies from obsolete request epochs.

Studio declares its own alias. Preserve legacy redirects and pin migration.
Add unit coverage, localized Home copy and command lifecycle documentation.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): accept resolved Home command metadata

Apps resolves inherited titles beyond the explicit manifest label cap.
Bound resolved labels to 147 Unicode characters, including secondary pages.
Keep the backend's 60-character limit for explicit manifest labels.

Match packaged icon paths to the contract's ASCII alphabet and image types.
Accept dotted names while rejecting traversal, encodings and foreign assets.

Verify 68 focused cases, 657 total cases, lint, i18n checks and the UI build.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): bind safety notices to actual providers

Warn for addon host execution and unknown isolation without changing tool
permissions or built-in host consent. Reset dismissal when the admitted
binding, observation, actor or chat context changes.

Expose presentation-only status identity from the admitted request.
Keep configured network policy visible when enforcement is unverified.
Render bounded provider explanations as escaped React text.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): isolate command status across sessions

Invalidate command observations when actors or authentication sessions
change, before effects can expose a previous actor’s reply.

Keep actual host execution warnings generic instead of recommending
Sandbox installation or repair. Cover both regressions with source-hook
lifecycle tests and provider notice assertions.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): declare audited host requests

Declare actual official hostclient operations without inferring requests
from deployment grants. Keep optional settings and egress checks separate
from required workspace and job operations.

Verify all twelve manifests through strict package fixtures and assert
that requests never self-grant. Guard generic provider and Home dispatch
without banning historical routes, native Studio, or deployment policy.

Document the audited operations and unresolved named-user and SQL gaps.
Refresh Sandbox catalog metadata for its existing typed status tool.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(addons): wire exact extension inventories and real provider selection

Run contract, official packaging and root architecture gates through actual report inventories. Exercise two installed execution providers through the real owner resolver and distinguish callback pass-through coverage. Share strict report validation with focused-scope handling and missing/skipped artifact self-tests; document direct evidence and remaining harness limits.

Assisted-by: Codex:gpt-5

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

* fix(ui): expire registry reads on auth refresh

Bind registry authority to the actual authentication session epoch.
Same-user refresh now rejects old polls and pending Home navigation
before effect cleanup, even when permissions and metadata stay unchanged.

Exercise both races through the production registry provider harness.
Verify 59 focused tests, 669 total tests, lint, i18n, and the UI build.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(studio): resolve current execution selection

Use existing principal-scoped chat and agent reads for advisory status.
Revalidate current named credentials instead of requiring historical SQL
revisions. Local-owner mode also works with the existing GORM store.

Remove resolver nonces and historical revision prerequisites. Keep exact
runtime binding and existing status admission hooks. Document current-read
consistency without promising revoke-and-restore detection.

Verify the SQLite regression fails before the fix. Two separate full race
runs pass: application 262 specs (3 skips), auth 257, gateway 115, tools 91.
Cover named chat, API key and session credentials and current-state denial.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(addons): drop selection revision tracking

Use current selection comparisons for advisory status instead of mutation
history. Keep actor, credential and conversation identity explicit in the
model. Hash current inputs at the gateway without exposing raw store data.

Remove unused epochs, revision interfaces and JSON ownership lifetimes.
Preserve current authorization, runtime identity, mutexes and atomic writes.
Document current-read semantics without historical ABA guarantees.

Verify all affected packages with the race detector and auth tag. Check
the trace viewer, prerequisite self-tests and 18 chat-command unit tests.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): scope Apps reads and provider status

Bind Apps replies and registry ingestion to their originating auth session.
Reject late replies before effect cleanup on refresh or account switch.

Use selected-provider command DTOs in the composer instead of private
Sandbox discovery. Keep unknown isolation unknown and remove specific
add-on installation recommendations from the affected native surfaces.

Test production Apps, registry, switches and composer paths with controlled
cleanup and two arbitrary providers. Update the matching feature docs.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): guard composer and session publication

Remove the deleted Sandbox variable from the full composer render.
Report unknown command isolation explicitly in the about card.

Bind Apps records and admin action results to their originating actor
and actual authentication epoch before cleanup can run. Reject stale
request admission and verification without changing server authorization.

Add full-component render and pending session-transition regressions.
Enable no-undef for the production composer in normal UI lint.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): expire cancelled upgrade reconciliation

Bind cancellation reconciliation to the initiating session authority.
Discard status replies after expiry without publishing old outcomes.
Match the exact operation identity before clearing a held action.

Cover pending cancellation reads, expiry before reads, and preservation
of a newer attempt with the same action key.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): bind background upgrades to auth lifetime

Keep background upgrade authority in AuthProvider, independent of views.
Expire pending observations before auth consumers render or logout waits.
Preserve same-session navigation and exact-operation cleanup.

Cover cancellation, completion, and verification after the origin unmounts
with actual provider transitions and the real upgrade store.
Thread captured authority through Activity retries and document expiry.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(addons): remove implicit sidebar pins

Start every account without implicit Studio or Talk pins. Preserve saved
user selections and durable explicit pins across reopen and frontends.

Remove startup seeding, seed metadata APIs, and first-install exceptions.
Apply equal retention to all absent IDs without dropping database tables.
Keep store initialization, cached errors, and the leader sweep unchanged.

Verify genuine RED regressions before removal. Race checks pass for all
42 pin specs, 10 focused application specs, and 10 pin endpoint specs.
Update pin documentation; renamed specs have no exact inventory entries.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* feat(addons): declare typed model destinations

Let installed addons own model launch destinations without teaching core
about addon identities or private routes. Keep native chat and completion
outside the declaration vocabulary.

Validate strict keys, unique capabilities, declared pages and SDK route
bounds. Carry cloned typed metadata through Apps using the shared
contract type, and add owning Studio, Talk, Face and Voice declarations.

Verify strict contract RED before implementation, then 1276 passing
specs across contract, Apps, packages, runtime, protocol, config and manager.
Verify 178 packaging and architecture specs and exact carrier inventories.
Frontend dispatch and route cleanup remain a separate milestone.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(ui): resolve declared model launch actions

Resolve installed model actions through the shared addon registry. Keep
native chat and completion launches, and never promote absent providers.

Refresh actor-filtered Apps before navigation and reject stale authority,
provider metadata, generation, immutable entry and destination claims.
Use named menu choices for collisions without loading addon modules.

Remove unreleased addon-specific redirects and migrate browser fixtures
to canonical declared pages. Preserve native chat handoff and addon-local
navigation adapters. Update user documentation and historical plans.

Verify three production UI consumers fail against their previous code,
then pass all 13 mocked-HTTP browser regressions. Verify 696 host unit
cases, full quiet lint, targeted no-undef, locales, inline styles and build.
Full backend/browser integration remains outside these mocked-HTTP checks.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): validate Ginkgo hooks before focus

Ginkgo emits null container hierarchies for suite hooks. Require hooks\nto pass before applying focus only to It cases. Normalize null\nhierarchies without weakening exact required-case comparisons.\n\nRun reporter regressions in prerequisites. Register the declared uiOnly\ntool case and remove an update fixture case excluded by the existing\nmanager focus. Both inventories match the complete CI case output.\n\nVerify 12 reporter regressions, 39 prerequisite cases, three existing\nreports, and fresh runtime, tools and focused update manager suites.

Assisted-by: Codex:gpt-5 [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): restore Go CI contract checks

Align route fixtures with explicit pins and declared network controls.
Keep network enforcement unknown instead of inferring it from isolation.

Propagate viewer output errors and select output paths with CLI arguments.
Check test writes and use Ginkgo assertions without copying atomic state.
Register the three migrated binding cases in the exact suite inventory.

Document two narrow security false positives for administrator file access
and symbolic credentials in reducer traces. Focused race tests pass.
The available golangci-lint reports zero issues in affected packages.

Assisted-by: OpenAI ChatGPT [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(ui): align browser coverage with addon contracts

The generic provider refactor removes implicit install advice and private
Sandbox discovery. Update browser fixtures to use the installed command
metadata and typed execution observations without restoring those paths.
Keep command warnings, consent, disabled switches and privacy assertions.

Home now holds a live command consumer. Exercise polling shutdown on a
route without that consumer, after its lazy transition finishes. Preserve
the bounded cadence, error backoff and disable-stop assertions.

Restore the remembered-voices link through the addon's compatibility
router. Cover native migrations and NotFound for removed generator aliases.

Validation: 293 affected browser cases and three native service cases pass.
The gallery command passes all 74 inventoried cases. Full UI units, quiet
lint, locale parity, production build and diff checks pass.

Assisted-by: OpenAI ChatGPT [functions]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* fix(addons): bind status to owner provider policy

Resolve typed status with the same current owner pins as execution.
Fail closed on owner policy outages and fingerprint authoritative pins.

Exercise CAS updates, conflicts, stale bindings, and policy outages.
Retain host settings when the named-account fixture replaces its gateway.
Refresh exact gateway and Apps inventories from passing reports.

Validation: the new regression fails before the fix. All 16 focused
binding and policy cases pass. Gateway passes 361 exact cases and Apps
passes 265 exact cases across their configured package reports.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(addons): share qualified tool identities

Use one formatter and contract-component parser for add-on identities.
Keep callable names bounded to 64 characters while recognizing long
unavailable stand-ins without granting dispatch authority.

Validate selected execution names and their provider ID at the owner
binding boundary. The real adapter already emits canonical identities.
Update exact importer and case inventories from passing reports.

Migrate the binding backend test to Ginkgo and check shutdown errors
reported by lint in the affected packages.

Validation: behavioral RED proves malformed stand-in acceptance and
plain selected-name acceptance. Affected full race suites pass: tools
94 and Studio 638, with zero skips. Application passes 275 of 278;
three existing PostgreSQL cases skip without their configured URL. All 12 owner-binding cases
pass and match the exact inventory. Scoped golangci-lint reports no issues.

Assisted-by: Codex:gpt-5 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* refactor(addons): share execution declarations

Share pure declaration predicates across manifest validation, gateway
observations, and owner bindings. Keep authorization and exact-runtime
admission checks local. Reuse the strict manifest schema validator so
raw status definitions fail closed without changing manifest acceptance.

Share the workspace-operation prefix scan without changing grant sources.
Add declaration parity and workspace-prefix tables and update exact
inventories from passing reports.

Validation: two new gateway cases fail before the fix; all 14 pass after.
Race tests pass: contract 245, gateway 136, tools 94, and focused
application binding/workspace cases 22. Scoped golangci-lint: 0 issues.

Assisted-by: Codex:gpt-5 golangci-lint
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

* test(addons): retain document skill cases

Match the contract inventory to the passing report after rebasing.\nKeep the new document-provider case and updated reserved-name cases.

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

Assisted-by: OpenAI:gpt-6.1-sol

* test(addons): follow owner-managed upgrades

Accepted upgrades belong to the owner and cannot be cancelled by UI.
Exercise observer expiry during reads, completion and verification.
Supply the real upgrade store to source-executed auth harnesses so
logout reaches its synchronous authority invalidation.

Match whole-package inventories to fresh Manager and gateway reports.

Assisted-by: Codex:gpt-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>

---------

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-02 23:50:40 +00:00

40 KiB

+++ disableToc = false title = "Chats: saved agents, triggers and inbox" weight = 22 url = "/features/chats-agents/" +++

A saved agent is a definition you can start a chat with. It has instructions and four lists: the MCP servers, add-ons, collections and skills the chat may use. Triggers and the inbox build on agents and are described on this page too.

Saved agents

An agent has:

Field Meaning
name 1 to 80 characters. Unique for you, ignoring case.
description A short note for you.
instructions Text added to the system prompt of every turn. At most 32 KiB.
model Optional. The chat uses this model. Chat-capable model aliases can also be selected; the agent saves the alias name, not its target name. Without it, the chat is created with the model you choose.
mcp_servers Names of servers in the model's MCP configuration, and your MCP connectors as connector:<id>. The form lists both.
addons IDs of installed add-ons.
collections Names of your collections.
skills Names of skills. The index of skills and the load_skill tool of the chat offer only these.
network The network of the [Sandbox]({{%relref "features/addons#network-access" %}}) commands of the agent's trigger and connector runs: off (the default), registries (the package registries) or all (every public host), capped by the administrator's chats_egress_ceiling. A run never shows a card: a host outside it is blocked, and the block is in the run's output. A chat you start with the agent starts with this preset too; you change it in the chat's Tools menu. The agent form sets it in the add-ons fold.
folder Optional. One folder of your files that chats with the agent work in, and the mode: read or write. Without it the agent works on all your files. See One folder for an agent.

Each list is an exact grant. An empty list grants nothing: a chat with an agent that names no add-ons has no add-on tools, even when you could use some. A plain chat, with no agent, is not changed.

Document selections keep the names docx, pptx, pdf, and xlsx. Selecting a skill does not grant its provider or command execution. Grant Simple Office explicitly for its tool-based procedures; no command runner is required. For Sandbox procedures, grant Sandbox and allow its selected, usable isolated bash tool. A host command runner alone does not qualify. Without an eligible provider, the selected skill is unavailable; no built-in document procedure serves instead. An unavailable provider pin never silently selects another provider. See [Document skills]({{%relref "features/chats#document-skills" %}}) for provider choices and OFF preferences.

Add-on changes apply at once

An add-on you turn on for a chat, or name in an agent, is allowed to change your data in that chat. A tool call that adds rows, changes a schema, creates a database or starts a job is applied at once, as you, with the same permission and stale-data checks as the Add-ons page. There is no confirmation card. The model is told what was applied, and the tool entry in the chat says it. A change the add-on kept a change set for can be undone from the chat with an Undo button (Tables rows, schema and databases, and Fine-tuning and Quantization job starts); the result says whether a change can be undone and, if not, why. File changes of the Sandbox and Office add-ons are one change set in the history of your files and can be undone there. A trigger or a scheduled run behaves the same, with no proposal waiting for a browser. Other ways to call add-on tools, such as the OpenAI-compatible API and MCP, still store a proposal that only your own browser session can confirm.

An agent never gives more than you have

A chat gets the intersection of the agent's lists and what you may use at that moment. If an add-on is disabled, a collection is deleted or you lose the MCP feature, the chat still starts. The thing is absent, and the model gets one line at the start of the turn, for example unavailable this turn: add-on office, collection docs. A sub-agent of the chat can only get tools the chat has, and the groups of built-in tools that the chat switched off in its Tools menu (for example Your files) stay off for the sub-agent too.

A chat can also start a saved agent as a sub-agent, when you switch Saved agents on in its Tools menu. See [Saved agents as sub-agents]({{%relref "features/chats#saved-agents-as-sub-agents" %}}).

If you delete an agent, the chats that ran with it keep their history. For an agent without a folder, their next turn runs without the agent's tools, and the model is told that the agent was deleted. For an agent with a folder, the next turn is refused instead; it never falls back to all your files.

Pages

The Agents page (Agents in the sidebar, /app/agents) lists your agents with the chats that ran with each one, and links to the Triggers and the Tasks. A page for one agent (/app/agents/{id}) shows its definition, the triggers that start it and its run history from the server (the chats you had with it and the ones its triggers and connectors started). Create and edit an agent in a form of folds with a preview of the definition and of what changed. Export and Import use the server's agent file (see below); the import opens the new agent and says what the server changed. It also reads the JSON export of an older agent, and names what it could not take over. With no agents yet, the page offers the server's starter agents. Chat with an agent from its row: the chat page opens empty with the agent chosen.

API

curl -X POST .../api/chats/agents -d '{"name": "Reviewer", "instructions": "Answer in French.", "addons": ["office"], "skills": ["review"]}'
curl .../api/chats/agents
curl -X POST .../api/chats -d '{"agent_id": "<id>", "model": "my-model"}'

GET, PUT and DELETE /api/chats/agents/{id} read, replace and delete one. A PUT replaces every field except folder (see below). Chats that already run with the agent use the new definition from their next turn. Another user's agent is always 404.

Agents need the agents feature, which is off by default for a user (see Chats permissions). They have no administration tools, as they are your own settings.

One folder for an agent

folder gives the agent one folder of your files and a mode, read (the agent's chats can read it) or write (they can read and change it):

curl -X POST .../api/chats/agents -d '{"name": "Acme", "folder": {"path": "/Projects/Acme", "mode": "read"}}'
# {"id":"...","name":"Acme",...,
#  "folder":{"path":"/Projects/Acme","name":"Acme","mode":"read","state":"ok"}}
  • path is a folder of your files, as the Files page shows it. / is all of your files, which with read makes an agent that can read everything and change nothing. The folder cannot be LocalAI, Chats or a folder inside them, and it cannot be a shared folder. A path that is not a folder of yours is 400.
  • The server keeps the folder itself, not its path. When you rename or move the folder, the agent follows it while you can read it. path shows its home-relative path, or /shared/<name>/... after a move into a shared folder. Other readable locations have no public path; the folder remains ok and the agent still follows it by identity.
  • state is ok, missing when the folder was deleted or you can no longer read it, or unset when no folder is chosen yet (an imported agent whose folder was not found here).
  • An agent whose folder is missing or unset does not start, and it never falls back to all your files: POST /api/chats answers 409 with the type agent_folder_missing, and so does a message to a chat that is open already. A chat whose agent had a folder and was then deleted is refused the same way. Choose a folder in the agent's settings, or restore the folder from the Trash (it comes back as the same folder, so the agent needs no edit), and send the message again.
  • A chat with the agent reaches its folder (read only for read), its own chat folder under Chats (attachments and the work folder, read and write), every readable shared folder and the skills (read only), and nothing else, provided that the complete set fits the folder limit below.
  • Scoped access has a limit of 32 folder roots per scope: the agent folder, one reserved chat folder, every skill folder that exists, and every readable shared folder. Three skill folders leave room for 27 shared folders. LocalAI never drops shared folders to fit. If the complete set exceeds the limit, POST /api/chats and the next message to an existing chat answer 409 with type agent_folder_limit. This is a capacity refusal, not a missing folder. Ask an administrator to reduce the shared folders available to you, then retry. The Tools panel exposes no file access while this check fails, even before the chat folder exists. Each turn checks access again, so a smaller shared set allows a retry without editing the agent. A failure to enumerate shared folders also refuses access instead of silently omitting them. Chats without an agent folder keep their existing access.
  • The model sees the agent's folder at /workspace/home, so /a.txt and /workspace/home/a.txt are a.txt in that folder. The chat folder is at /workspace/chat, with the work folder at /workspace/chat/work/. The shared folders are at /workspace/shared-<name>. The file tools, the image and document tools, the file search and the system prompt use this layout, and a sub-agent of the chat gets the same folders. A file you attach to a message is named to the model at /workspace/chat/<file>.
  • A sub-agent never reaches more than the chat that started it, in folder or in mode. A plain sub-agent and a saved agent without a folder work in the chat's folder, at the chat's mode. A saved agent with a folder can start from the chat only when its folder is the chat's folder or inside it; it then works in its own folder at the lower of the two modes. Any other agent is refused with "this agent works in another folder than this chat", and list_agents does not list it.
  • Commands of the chat run only in the Sandbox add-on. Its sandbox has the agent's folder at /workspace/home (read only for read), the chat folder at /workspace/chat (read and write), and nothing else of your files. What a command writes anywhere else is not saved, and the command's output says so. The built-in bash that runs on this machine is never offered to such a chat, also when chats_host_exec is on: a command on this machine can read every file LocalAI can read, so nothing can keep it in the folder. The Tools menu of the chat says why. On a server without the Sandbox, the agent still reads and changes its folder with the file tools.
  • Add-ons that a chat with the agent calls work in the same folders. For an add-on that works on your files, such as Office or the Sandbox, / is the agent's folder, and the Sandbox sees the chat folder next to it at /workspace/chat. This holds for every tool, also one that only reads. See [Workspace operations]({{%relref "features/addons#workspace-operations" %}}).
  • A PUT without folder keeps the folder the agent has, so a client that does not know the field cannot give the agent more files by editing it. "folder": {"path": ""} clears the folder.
  • The folder limits what the chat reaches through your files only. The MCP servers, web tools and collections that the agent grants are separate grants and are not limited to the folder. The chat is not told to build [Pages]({{%relref "features/pages" %}}) either: pages live in pages/ of your home, outside the folder.
  • The folder limits the agent, not you. The Files page, its History, Undo and Trash still show all your files, and a chat's file list and attachments keep their paths in your home there.
  • Triggers and connectors that use the agent keep to its folder too. See Triggers of an agent with a folder and the connectors page.

In the web interface, the Files section of the agent form sets the folder: All my files, or One folder with Choose folder, which opens a folder picker of your files (LocalAI, Chats and shared folders cannot be chosen), and the access, Can read or Can read and change. The agents list and the agent's page show the folder and its access, with a lock for read only, or Folder missing with a link to the form. The Files page marks a folder that an agent uses. A chat with the agent shows the folder next to the agent in the message box and in the Tools menu. A chat that cannot start because the folder is missing says so and offers Edit agent.

The folder is read from the stores at every request and at the start of every turn, and nothing is kept in memory between turns (stateless per request), so every frontend and chat worker of a cluster answers the same. A change of the folder or the mode applies from the next message of a chat that is open; a turn that is running finishes with the access it started with.

Downgrade. A server older than this feature does not know the folder of an agent. If you go back to such a version, its agents work on all your files again, without a warning, and an agent file of version 2 cannot be imported there. Before you downgrade, export and delete the agents that have a folder, or switch off their triggers and connectors.

Starter agents

GET /api/chats/agents/templates lists ready-made definitions (a research assistant, a code reviewer, a daily digest, a writing editor). They grant nothing. POST /api/chats/agents/templates/{id}/create saves one as your own ({"name": "..."} is optional); then grant it tools with PUT.

Export and import

curl .../api/chats/agents/<id>/export > reviewer.json
curl -X POST .../api/chats/agents/import -d @reviewer.json
curl -X POST .../api/chats/agents/import -F file=@reviewer.json

The file holds the definition only (kind localai-agent): no ID, owner or time. An agent without a folder is version 1. An agent with a folder is version 2 and carries folder with the folder's path at export time and the mode, for example {"path": "/Projects/Acme", "mode": "read"} (the path is empty when the folder is missing, not chosen, or now outside your home). A followed folder in a shared location stays usable by the original agent, but its export carries an empty path and the mode. Import keeps it scoped with state unset and warns you to choose a folder; it never interprets the shared path as a folder in your home. A server that reads only version 1 refuses a version 2 file, so an agent limited to one folder is never imported somewhere as an agent with all the files. An import always makes a new agent. The folder is looked up by its path in your files; when you have no such folder, the agent is saved with the mode and no folder (state: unset), it does not start until you choose one, and a warning says so. A name you already use gets " (2)", " (3)" and so on, and a model that this server does not have, or that you may not use, is dropped. The response lists such changes under warnings. The grants (MCP servers, add-ons, collections, skills) are kept as names; a grant that names something you do not have gives the chat nothing, as for any agent. Secrets are never part of an agent, so the file has none.

Run history

GET /api/chats/agents/{id}/runs lists the chats that ran with the agent, newest first: the ones you had with it, the ones its triggers started (origin: trigger) and the ones its connectors started (origin: connector), each with running and last_turn. total, failed and running count every run among your newest 5,000 chats. Open a run with GET /api/chats/{chat_id}.

Connectors

A connector binds a Telegram bot or a Slack app to an agent, so a person can talk to it from Telegram or Slack. An MCP connector is a tool server that an agent can name in its mcp_servers list.

Triggers

A trigger starts a chat as you, by itself. Each run is an ordinary chat of yours: it uses your files, your quota and your model allowlist, you can open it and undo what it changed, and it is marked with its trigger. To repeat a prompt inside one chat, every few minutes, use a [loop]({{%relref "features/chats#loops-repeat-a-prompt-in-a-chat" %}}) instead (see Loop or trigger? there). There are two kinds.

Kind Runs Fields
schedule At a time schedule, time_zone (an IANA name; UTC when empty)
file When a file arrives in a folder of your files folder (a path inside your files, such as /Contracts), quiet_seconds (default 30), min_gap_seconds (default 60)

A schedule is one of daily 08:00, weekdays 09:30, weekly mon,thu 09:30, monthly 15 08:00, once 2026-12-24 18:00 or five cron fields. The cron fields are numbers, lists, ranges and steps (*/15, 1-5, 1,15); names such as mon or jan are not read, and weekly takes day names. A time that does not exist in the time zone (a daylight-saving gap) moves to the first valid minute after it, and a repeated hour runs once. GET /api/chats/triggers/preview?when=...&tz=... shows the next times and says what the text means.

A file trigger waits until the file has not changed for the quiet period, then starts one run for that version of the file. The first message of the run says A new file arrived: /Contracts/a.txt. A run's own writes never start the same trigger. A file that changes again after a run of the trigger began waits for the trigger's minimum gap (min_gap_seconds, 60 by default, at most a day) before it starts another run, and further changes while it waits start nothing extra. Files that were already waiting when the run began still run. With the daily limit, this keeps a writer that reacts to the run's output, such as a page that saves after each change it sees, from starting runs in a loop: at most one run a minute and 24 a day by default. An AGENTS.md file in a folder, or above it, is added to the first message as rules for the run. Treat it as text from whoever can write there.

What a run gets

  • Its message. The first message is the prompt of the trigger, the folder rules and, for a file trigger, the file. LocalAI adds one closing line to every trigger run: "When you are done, answer with one short plain sentence about what changed. Do not print JSON or code unless the task asks the user for it." The answer is what you read in the chat and the inbox, so write the prompt without a request for JSON output. A connector chat is a talk with a person and does not get the line.
  • Who it is. The run is a chat of the trigger's owner, with the owner's grants. If the owner is over quota, the run fails with that reason and the owner gets a notification.
  • Which tools. With an agent (agent_id), the tools of that agent. Without one, the run uses the model and the add-on and skill switches of the chat that made the trigger (source_chat_id), or the defaults of a new chat. An unattended run with no agent gets no add-on tool that acts at once, such as the Sandbox: name the add-on in an agent to allow it. A run never gets propose_trigger and cannot change its own trigger.
  • Network. The Sandbox commands of a run reach the network of the agent's network field, under the administrator's ceiling, and nothing without an agent. Nobody watches a run, so no card asks: a blocked host stays blocked. A sub-agent follows the network of the chat that started it, its rules included, and never shows a card either.
  • Limits. A run has 15 minutes. At most 2 runs go at once, one per trigger, and a trigger has a daily limit (daily_cap, default 24). A run that is skipped or fails shows its reason on the trigger.
  • Cost. A run is a chat turn on the trigger's model, so it uses tokens. A run records the prompt and completion tokens of its model requests (tokens on the run), and a trigger reports the average of its last runs, the most runs one day can have, and whether the model is a remote service (cost on the trigger). LocalAI keeps no price list, so the cost is in tokens only.
  • History. A trigger keeps its last 200 finished runs, and every run of the last two days. A finished run older than 30 days is removed, whatever the count. A run that is queued or running is never removed. Set --trigger-run-keep and --trigger-run-retention-days (-1 keeps all) to change this. A file version that ran can run again once its old run is removed.
  • When it cannot run. A deleted agent fails the run with "the agent was deleted" and switches the trigger off. A disabled owner skips the run. An owner who no longer exists, or a folder that is gone, skips the run and switches the trigger off. An agent whose folder is missing or not chosen skips the run with "the agent's folder is missing" and switches the trigger off. Each of these tells the owner.

Triggers of an agent with a folder

A trigger whose agent has a folder keeps to that folder, like every chat of the agent:

  • A file trigger must watch the agent's folder or a folder inside it. Saving one that watches another folder (at create, at update, and when you confirm a proposal) is refused with 400: "the trigger's folder must be inside the agent's folder". A file outside the folder never starts a run.
  • Each run checks again. A trigger made before the agent got its folder, or whose agent moved to another folder since, skips the run with "the trigger's folder is not inside the agent's folder" and is switched off.
  • A run whose agent's folder is deleted, unreadable or not chosen never runs on your other files: it is skipped with "the agent's folder is missing", no chat is made, the trigger is switched off and you get a notification. Saving a trigger for such an agent is refused until you choose a folder. Restore the folder (or choose another one) and switch the trigger on again.
  • If the complete file access exceeds the 32-root limit, the run fails without starting a chat. The trigger stays enabled: reduce the available shared folders and retry. This differs from a missing agent folder, which switches the trigger off.
  • The rules of a run come from AGENTS.md files in the agent's folder only: for a schedule trigger the agent's folder itself, for a file trigger the folders from the agent's folder down to the watched folder. An AGENTS.md above the agent's folder is not read. The first message names folders and the file as the agent's chats see them: # Folder rules: /workspace/home and A new file arrived: /workspace/home/In/a.txt.
  • The run's chat reaches what a chat of the agent reaches, nothing more.
  • In a chat with the agent, the trigger tools name folders as the chat does (/In and /workspace/home/In are In in the agent's folder) and refuse a folder outside it. list_triggers, update_trigger and delete_trigger see only the triggers of that agent; change the others on the Triggers page.

The folder is looked up at every save and every run, so all frontends of a cluster apply the same rule (stateless per request).

A trigger runs nothing for the time before it was saved or switched on. After downtime, a missed schedule runs once, not once for each missed time.

Proposals

The four trigger tools (propose_trigger, list_triggers, update_trigger and delete_trigger) form the Triggers group in the chat's Tools menu. They are on in a chat you start, and one switch turns all four off for that chat (PUT /api/chats/{id}/builtins {"off": ["triggers"]}; see the Tools menu). Switch it off in the Home composer before you start the chat to have none from the first message.

In a chat, the model can propose a trigger with propose_trigger and list yours with list_triggers. A proposal is not active and runs nothing. It waits for you in the chat and on the Triggers page (/app/triggers, GET /api/chats/triggers/proposals, with ?chat={id} for the ones made in one chat). Change it first if you want (PUT .../proposals/{id} with the same body as a trigger; it stays a proposal), then confirm it (POST .../proposals/{id}/confirm, with your time_zone when it names none) or dismiss it (DELETE .../proposals/{id}). The chat carries the proposal: the tool line of propose_trigger has a card of kind trigger_proposal whose id is the proposal's id, in the live tool_end event and in the transcript, so it shows again after a reload, and a trigger_proposed event announces it to a client that is watching. If a proposal was made in a chat that has an agent, the trigger keeps that agent. At most 10 proposals wait at a time, and you can have 50 triggers.

The card sits below the text of the message it belongs to. When you answer it, it does not go away: it turns into a receipt, and the answer is kept on the server with the proposal (GET .../proposals/{id} returns state: pending, accepted, declined or undone), so the receipt shows again after a reload, in another browser and on every frontend of a cluster.

  • Accepted shows what was scheduled in plain words ("Scheduled: Every Monday at 09:00 (Europe/Rome)"), the name of the trigger, its next run, View trigger (the trigger has the proposal's id) and Undo.
  • Undo (POST .../proposals/{id}/undo) switches the trigger off and removes it from the Triggers page. The proposal stays as an undone record, and Schedule it again (POST .../proposals/{id}/reopen) makes it wait for your answer again. Undo is refused with 409 once the trigger ran (a run started by hand counts) or you edited or switched it on the Triggers page; the receipt then tells you to remove it there. The response carries can_undo and undo_blocked (ran or edited).
  • Declined leaves a quiet one-line receipt with Restore.
  • A trigger you delete on the Triggers page ends the receipt: the card says the trigger was removed (404).

At the start of its next turn the model is told what became of the triggers it proposed in the chat (accepted, declined or undone), so it does not say a trigger runs when it was undone, and does not propose a declined one again unless you ask. A declined or undone proposal does not count towards the limit of 10 waiting proposals or 50 triggers.

A schedule proposed in a chat runs at most once an hour. A shorter one (for example */15 * * * *) is refused with the reason, and the model is told to propose an hourly or slower one. An edit of the proposal keeps the same floor. On the Triggers page you can still choose a shorter schedule yourself.

A proposal also carries a plan, so you see what it does before you create it: what a run reads (reads), the file in your home it rewrites (writes), the Home widget that shows that file (widget, the widget's id in LocalAI/home.json) and about how many tokens one run uses (estimated_tokens). The card lists them with when it runs and its first run, that it runs as you with the chat's model, the cost (and whether the tokens leave the server) and how to stop it. When you create a trigger whose plan names a widget, LocalAI writes the trigger's id into that widget as trigger, so the widget can show when it runs. A form that edits a trigger and does not send the plan keeps it.

Changes from a chat

The model can also change the triggers you already have. list_triggers gives each trigger a short ref (for example t1a2b3c4d): it stays the same for the trigger and it is not the trigger's id. The list shows no prompts. To read the prompt and the other settings of one trigger, the model passes its ref with include_prompt: true. A ref of another user's trigger, or one that does not exist, is an error for the model. The model can also name a trigger by its exact name when only one trigger has that name.

  • update_trigger {ref, name, prompt, when, time_zone, daily_cap, enabled, reads, writes, widget, estimated_tokens} changes the fields it names and keeps the others. The same rules as for a proposal apply: a schedule that changes runs at most once an hour, the folder and the writes file must be in your files. A change of anything other than enabled does nothing by itself: a card shows each changed field before and after, with Apply and Dismiss. A change that only pauses (enabled: false) or resumes (enabled: true) the trigger applies at once, because you can take it back, and its card is a receipt with Undo.
  • delete_trigger {ref} deletes nothing by itself: a card shows the trigger's name, when it runs, whether it is on and its last run, with Delete and Keep.

The tool descriptions tell the model which change waits and which applies at once, so it does not say a change is made before you apply it. At the start of its next turn the model is told what you did with the changes it asked for in the chat.

After you apply a change the card is a receipt with Undo for one day. Undo puts the trigger back as it was before the change. For a delete it restores the trigger with the same id and settings; its run history is not restored, and the chats of its runs were never removed. Undo is refused (409, with the reason) after the day, when the trigger was changed on the Triggers page since, or, for a delete, when you already have 50 triggers. A change that waits cannot apply once the trigger was changed or removed after the model asked for it: the card says so and keeps only Dismiss.

The card's change is kept on the server, so the card shows the same state after a reload, in another browser and on every frontend of a cluster. The tool line has a card of kind trigger_change whose id is the change's id, and a trigger_proposed event announces it to a client that is watching.

Route What it does
GET /api/chats/triggers/changes/{id} The change: op (update, delete, pause or resume), state (pending, applied, dismissed or undone), trigger_id, before, after (absent for a delete), fields, stale, can_undo, undo_blocked (expired, edited, removed or full) and last_run. 404 a week after it was answered, when it is no longer kept.
POST .../changes/{id}/apply Makes the edit or deletes the trigger. 409 when the trigger changed since or the change was answered.
DELETE .../changes/{id} Dismisses the change (Keep for a delete). The trigger stays as it is.
POST .../changes/{id}/undo Takes an applied change back, for one day.

A change does not count towards the 50 triggers or the 10 waiting proposals. At most 10 changes wait at a time. Like the other trigger tools, these tools are offered only in a chat a person started: a trigger run, a connector run and a sub-agent never get them.

Clusters

In a cluster, one frontend runs the triggers at a time: the one that holds an advisory lock in the database. A due time or a file version is one unique row, so it runs once even when two frontends try. If that frontend stops, another takes over, ends the runs whose chat is over as failed ("interrupted by a restart") and tells the owner, and goes on. Triggers need the database; the JSON store is for a single process.

With chat workers, the frontend that leads still starts and watches each run, and a chat worker runs its turns.

The same holds for one server. A run that a stop or a restart cuts short is recorded as interrupted, its chat shows the turn as interrupted (not done, even if some text was written), and its owner gets a "failed" notification. The triggers are checked every 5 seconds, so a schedule starts within 5 seconds of its time and a file trigger within 5 seconds of the end of its quiet time.

API

curl -X POST .../api/chats/triggers -d '{"name": "Morning digest", "prompt": "Summarise yesterday.", "kind": "schedule", "schedule": "weekdays 09:00", "time_zone": "Europe/Rome", "enabled": true, "model": "my-model"}'
curl .../api/chats/triggers
curl -X POST .../api/chats/triggers/<id>/run      # start a run now
curl .../api/chats/triggers/<id>/runs

A trigger in a response also has plan (reads, writes, widget), cost (estimated_tokens, average_tokens and runs, max_runs_per_day, remote), last_done (the end of the newest run that ended well), min_gap_seconds for a file trigger, and each run its tokens. A create or replace takes plan, estimated_tokens and min_gap_seconds; an empty one keeps the stored value.

GET, PUT and DELETE /api/chats/triggers/{id} read, replace and delete one, and POST .../enable and .../disable switch it. Deleting a trigger stops its running run and drops its queued runs; the chats it made stay. Another user's trigger is always 404. Triggers are your own settings, so they have no administration tools.

Inbox

The inbox lists what ended while you were elsewhere: a trigger run that finished or failed, or something that needs you. Each item has a kind (run_ended, run_failed or needs_attention), a status (ok or failed), a source (trigger, connector or system; empty on items stored before sources existed), a short title and text, and a link to the chat it is about. A chat that a run started is unread until you open it: opening the chat marks its notifications read.

In the chat page, the Notifications button in the header shows the unread count (99+ above 99) and opens the panel (a sheet over the bottom of the screen on a phone):

  • All, Unread and Failed filter the list. Failed shows every failed run and every trigger or connector that was switched off.
  • Items are grouped under Today, Yesterday and Earlier. Each one shows what sent it, when (the exact time on hover) and, for a failure, the reason.
  • A click on an item marks it read and opens its chat. A connector that was switched off opens the connectors page.
  • Hover an item (or focus it) to mark it read or delete it. With the keyboard, the arrow keys, Home and End move between items, Enter opens one, Delete removes it and Esc closes the panel.
  • Mark all read is in the panel header; Clear read and Clear all are in its menu.
  • A notification that arrives while the panel is open slides in at the top and is highlighted for a few seconds.

You keep at most 500 notifications, none older than 90 days. The age limit applies when you read: an older notification is not listed or counted, and it is deleted the next time one is added.

curl ".../api/chats/notifications?unread=1"          # the unread ones, and the unread count
curl .../api/chats/notifications/count
curl -X POST .../api/chats/notifications/read -d '{"all": true}'
curl -X DELETE .../api/chats/notifications -d '{"ids": [12, 13]}'
curl -X DELETE .../api/chats/notifications -d '{"all": true}'                    # empty the inbox
curl -X DELETE .../api/chats/notifications -d '{"all": true, "read_only": true}' # remove only the read ones
curl -N .../api/chats/notifications/events            # new ones as server-sent events

Clear all asks first, in the panel, and says how many notifications it removes. Clear read keeps the unread ones. A removed notification cannot be restored. The list answers with total, everything you keep, because it returns at most limit items (50 by default, 500 at most); the panel loads more on request. A delete answers with the number removed in deleted and the unread count left. You can only ever remove your own notifications.

Events are live only. A client that was away lists the inbox. In a cluster, a notification added on one frontend reaches the clients connected to any other. When notifications are read or deleted, the stream sends a notifications_changed event, so your other open tabs read the list again and their unread badge follows. The notifications are rows in the shared database, so clearing the inbox on one frontend clears it for all of them.

Browser notifications

The inbox page can ask the browser for permission and subscribe it to push. A notification is then pushed to that browser, with its title and link and nothing else, also when no LocalAI tab is open. Clicking it opens the chat.

curl .../api/chats/push/key                       # the server's VAPID public key
curl -X POST .../api/chats/push/subscribe -d '{"endpoint": "https://...", "keys": {"p256dh": "...", "auth": "..."}}'
curl -X DELETE .../api/chats/push/subscribe -d '{"endpoint": "https://..."}'
  • The server makes its VAPID key pair the first time it is needed and keeps it with the subscriptions: in the database, or in push.json (mode 0600) under the data path. With neither, push is off and these routes answer 409.
  • The endpoint must be an https address on the public internet. The server refuses private, loopback and link-local addresses, both when a browser subscribes and when it sends, and it does not follow redirects.
  • You can have 10 subscribed browsers; the oldest go first. A subscription that the push service reports gone is removed.
  • In a cluster, the frontend that stores a notification sends the push, so it is sent once.

Storage and clusters

Saved agents, triggers, runs, notifications and push subscriptions are stored beside your chats: in the database when there is one, in JSON files under the data path otherwise, and in memory when there is no data path (the page then says persistent features are off, and push is off). The JSON stores are for a single process.

A cluster needs PostgreSQL, as the chats do. This is how each part of the state behaves with several frontends:

State Mode How
Agents, triggers, runs, notifications, push subscriptions Shared Rows in the database. Any frontend reads and writes them.
What a chat of an agent with a folder may reach Stateless The folder and its mode are columns of the agent's row. The frontend or chat worker that runs a turn, a trigger run, a connector reply or an add-on call reads them, and the folder, from the stores each time. Nothing is cached, so an edit applies on every frontend from the next turn.
Which frontend runs the triggers Single runner The frontend that holds the advisory lock (KeyChatsTriggers) runs the Runner. When it stops, another takes the lock.
A due time or a file version Shared One unique row for each trigger and due key. Two frontends cannot start the same run.
Trigger proposals and changes asked for in a chat Stateless Rows in the trigger table. Each request reads and writes them in the database, so any frontend can show, apply or undo them. The answer to a change (apply, dismiss, undo) is a conditional update of its row: when two frontends answer the same change at once, one wins and the other answers 409, so a change is applied or undone once.
Quiet periods of waiting files, the memory of a run's own writes Per leader Kept by the Runner of the current leader. A new leader reads the change feed again from its saved position, which stays below the oldest waiting file, so a file that was waiting is found again.
Live notifications Shared, live only Sent over the message bus to every frontend, with the "read or deleted" signal. A client that missed one lists the inbox, which is the record.
The supervisor of a running run Per instance The frontend that started the run watches it. The leader also ends any run that nobody watches whose chat is over, so a run is not lost when its frontend dies.
Where the turn of a run runs Stateless On the frontend that started it, or on a chat worker when the frontends send unattended turns there. The turn claim, its heartbeat and the waiting messages are in the database, so either one can run it and only one does.

JSON storage

Without an auth database, persistent chats and agent definitions use per-instance JSON storage. These stores use in-process mutexes and atomic writes. Run one process per JSON directory on a local filesystem; clusters use the SQL stores. There is no cross-process directory lock or historical selection ledger. Status observations use current principal-scoped reads in either backend.