Files
LocalAI/core/services/workerctl/chat.go
T
Ettore Di Giacinto adbb71cfdf feat(memory): add Always confirm, an agent save rule and a messaging row
A person can now choose four save levels on the Memory tab: Save it and
tell me, Ask me first (the default), Always confirm and Never. Always
confirm is like ask, and nothing can loosen it: no agent or messaging
setting overrides it, and the model cannot undo or forget a note by
itself (revert_change is refused, and the person uses the Undo).

One function, effectiveSaveLevel, combines the rules. The strictest wins,
in the order auto, ask, always, never. The inputs are the level of the
person, the rule of the saved agent, the messaging row and the chat.
A table test covers every combination.

A saved agent has an optional memory_save field: always or never. It can
only be stricter than the person, so auto is not offered and the API
refuses it. A write without the field keeps the stored rule, and use_mine
clears it. It is stored by both stores and is part of an agent export and
import. The agent form shows three cards under When it wants to save a
note.

The new row In messaging apps (messaging_save on GET and PUT
/api/chats/memory) takes ask (the default), paired_private or never. With
paired_private a note is saved at once only when the sender is paired and
the chat is one-to-one, and it goes to the notes of that conversation,
with an Undo. The limits stay: five notes a reply, the size limit, and a
note that looks like a secret still waits. The row cannot loosen the level
of the person when that is always or never. A setting that cannot be read
reads as ask.

The connector turn now carries whether the chat is one-to-one and whether
the sender is paired, in the turn grant and in the worker JSON. Both read
as false when unknown. The Telegram, Slack and Discord transports already
set Direct; new specs pin it for private, group and supergroup chats,
direct messages, channels, group conversations and threads.

A note from a messaging chat goes to the conversation first. It reaches the
notes of the agent or of the person only from a paired sender, only when
the memory choice of the agent allows that scope, and only after that
sender confirms it. Under Always confirm only the paired sender who asked
can confirm. An agent whose memory is None still saves nothing.

The script of the confirm-in-chat specs now proposes a note for the
conversation, because a sender who is only listed cannot write to the
notes of the person.

Assisted-by: Claude:claude-sonnet-5-5
2026-11-04 19:45:08 +00:00

47 lines
2.1 KiB
Go

package workerctl
// ChatStartRequest is the payload of a chat start: a frontend asks a chat
// worker to run the next turn of an unattended chat (a trigger run, a job or a
// connector message). The worker reads the chat, its owner and its model from
// the shared database, so the request names only the chat and the message.
type ChatStartRequest struct {
// Principal is the owner of the chat as the chats service names it.
Principal string `json:"principal"`
ChatID string `json:"chat_id"`
Text string `json:"text"`
// Grant is set for a connector message: what the sender of the turn may
// do. The worker keeps it for the turn, so a tool it builds is offered
// only for the capabilities in it. A start without a grant for a connector
// chat gets none of the gated tools.
Grant *ChatGrant `json:"grant,omitempty"`
}
// ChatGrant is the outcome of the permission check for the senders of one
// connector turn (see studio.Authorize). It holds IDs, never message text.
type ChatGrant struct {
Binding string `json:"binding"`
Kind string `json:"kind,omitempty"`
Senders []string `json:"senders,omitempty"`
Roles []string `json:"roles,omitempty"`
// Caps are the capabilities every sender of the turn holds.
Caps []string `json:"caps,omitempty"`
// Denied says why a capability in the ceiling is not held, by name.
Denied map[string]string `json:"denied,omitempty"`
// Direct says the chat is one-to-one and Paired that the sender is paired
// with the bot. A grant from an older frontend has neither, which reads as
// a group chat and an unpaired sender.
Direct bool `json:"direct,omitempty"`
Paired bool `json:"paired,omitempty"`
}
// ChatStartReply is the worker's answer to a ChatStartRequest. With Error
// empty, Started says whether the message began a turn on the worker;
// otherwise a turn was running, and the message waits under PendingID. A
// non-empty Error is the worker's refusal: the worker is present, and nothing
// started.
type ChatStartReply struct {
Started bool `json:"started,omitempty"`
PendingID string `json:"pending_id,omitempty"`
Error string `json:"error,omitempty"`
}