Files
LocalAI/pkg/addons/contract/changeproposal.go
T
Ettore Di Giacinto 23beb6dc80 feat(chats): let a person choose how memory notes are saved
A memory note is still a proposal that the person confirms by default.
An add-on can declare the user setting with the role memory.save-mode,
and the person then picks: ask first, save it and tell me, or never.

With auto, a note from the person's own Studio chat is applied at once
through the whole proposal path (grant, gate, checks, rate limit, audit)
and confirmed as the person, so it is one change set that revert_change
and the Undo on the chat restore. The chat shows "Saved to memory" with
Undo and no confirmation card. The record carries auto: true and the
audit says the write was automatic.

Safety rules live in the host. A connector chat always asks, a trigger or
job has no tool, a saved agent cannot widen the choice, and a failure to
read the setting asks. At most 5 notes are saved unasked in a turn; a note
over 4 KiB or one that looks like it holds a secret becomes a normal
proposal. With never the memory kind is hidden from propose_change and a
stray call is refused. Only memory reads the setting.

Assisted-by: Claude:claude-sonnet-5-5 go node playwright
2026-11-03 22:46:22 +00:00

139 lines
5.2 KiB
Go

// SPDX-License-Identifier: MIT
package contract
import (
"slices"
"time"
)
// A change proposal is how a model asks to change the person's own state: a
// memory note, a profile, a skill. The host stores it, shows it to the owner
// and writes it only after the owner confirms. Every surface that can run a
// model uses the same record: a Studio chat, a connector chat of any platform
// and an add-on page. An add-on never needs platform code for it. The model
// calls the host tool propose_change, and a connector carries the text of the
// tool result like any other answer.
//
// The record is versioned. A reader must ignore fields it does not know.
// ChangeProposalVersion is the version of the ChangeProposal wire record.
const ChangeProposalVersion = 1
// ChangeKind is what a proposal changes.
type ChangeKind string
// The kinds of change. The host decides which ones it offers.
const (
ChangeMemory ChangeKind = "memory"
ChangeProfile ChangeKind = "profile"
ChangeSkill ChangeKind = "skill"
ChangePage ChangeKind = "page"
ChangeDashboard ChangeKind = "dashboard"
)
// ChangeKinds lists every kind in a stable order.
func ChangeKinds() []ChangeKind {
return []ChangeKind{ChangeMemory, ChangeProfile, ChangeSkill, ChangePage, ChangeDashboard}
}
// ValidChangeKind reports whether s names a kind.
func ValidChangeKind(s string) bool { return slices.Contains(ChangeKinds(), ChangeKind(s)) }
// ChangeScope says whose state a proposal changes.
type ChangeScope string
// The scopes. user: the person, in every chat. agent: one saved agent.
// channel: one connector conversation.
const (
ScopeUser ChangeScope = "user"
ScopeAgent ChangeScope = "agent"
ScopeChannel ChangeScope = "channel"
)
// ValidChangeScope reports whether s names a scope.
func ValidChangeScope(s string) bool {
switch ChangeScope(s) {
case ScopeUser, ScopeAgent, ScopeChannel:
return true
}
return false
}
// ProposalStatus is where a proposal is in its life.
type ProposalStatus string
// The statuses. Only pending can change. Applied can become reverted.
const (
ProposalPending ProposalStatus = "pending"
ProposalApplied ProposalStatus = "applied"
ProposalDismissed ProposalStatus = "dismissed"
ProposalExpired ProposalStatus = "expired"
ProposalReverted ProposalStatus = "reverted"
// ProposalStale: the target changed after the proposal, so it was refused.
ProposalStale ProposalStatus = "stale"
)
// Settled reports whether the proposal has an answer.
func (s ProposalStatus) Settled() bool { return s != ProposalPending }
// ChangeSource says where a proposal came from, without naming a platform.
// Kind is the origin of the chat as the host names it ("" for a chat a person
// watches, "connector" for a messaging app). ID names the binding of a
// connector chat. Chat is the host chat. Thread is for a connector that has
// threads; the host fills it when it knows it.
type ChangeSource struct {
Kind string `json:"kind,omitempty"`
ID string `json:"id,omitempty"`
Chat string `json:"chat,omitempty"`
Thread string `json:"thread,omitempty"`
}
// ChangeProposal is one proposed change. Content is the full new text of the
// target. BaseHash is the SHA-256 (hex) of the target when the model proposed,
// or empty when the target did not exist; a proposal is refused if the target
// has another hash when the owner confirms.
type ChangeProposal struct {
V int `json:"v"`
ID string `json:"id"`
Owner string `json:"owner,omitempty"`
Agent string `json:"agent,omitempty"`
Kind ChangeKind `json:"kind"`
Scope ChangeScope `json:"scope"`
Target string `json:"target"`
Summary string `json:"summary,omitempty"`
Content string `json:"content,omitempty"`
BaseHash string `json:"base_hash,omitempty"`
Source ChangeSource `json:"source,omitzero"`
Status ProposalStatus `json:"status"`
// Auto is true when the host applied the change at once because the person
// chose so (the memory save setting), not after a confirmation. The change
// set is the same and can be undone the same way.
Auto bool `json:"auto,omitempty"`
// Rev is the workspace change set that applying wrote; RevertRev the one
// that reverting wrote. Zero until then.
Rev int64 `json:"rev,omitempty"`
RevertRev int64 `json:"revert_rev,omitempty"`
Created time.Time `json:"created"`
Expires time.Time `json:"expires"`
Answered time.Time `json:"answered,omitzero"`
}
// The limits of the host. They are decisions of the owner of the product, not
// settings of an add-on.
const (
// ChangeExpiry is how long a proposal waits for an answer.
ChangeExpiry = 24 * time.Hour
// MaxPendingChanges is the most proposals of one person waiting at once.
MaxPendingChanges = 10
// MaxChangesPerHour is the most proposals one agent (or one plain chat
// user) makes in an hour.
MaxChangesPerHour = 5
// MaxChangeBytes is the largest content of a proposal.
MaxChangeBytes = 16 << 10
// MaxPageChangeBytes is the largest content of a page proposal: all the
// files of the page together.
MaxPageChangeBytes = 256 << 10
// MaxProfileBytes is the largest agent profile.
MaxProfileBytes = 4 << 10
)