feat(studio): add REST for proposed changes

List, read, confirm, dismiss and undo the changes a model proposed, and
read their audit. The routes sit under /api/chats with the other chat
routes and need the chats feature. A person reaches only their own
proposals; another person's id is a 404.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 09:33:44 +00:00
1 parent d32440096d
commit acba42ae7b
8 files changed
+1614 -30

No files matched your search

+6
View File
@@ -105,6 +105,12 @@ var RouteFeatureRegistry = []RouteFeature{
{"GET", "/api/chats/shared-pages/:folder/:name/file", FeatureChats},
{"PUT", "/api/chats/shared-pages/:folder/:name/file", FeatureChats},
{"POST", "/api/chats/shared-pages/:folder/:name/chat", FeatureChats},
{"GET", "/api/chats/changes", FeatureChats},
{"GET", "/api/chats/changes/audit", FeatureChats},
{"GET", "/api/chats/changes/:id", FeatureChats},
{"POST", "/api/chats/changes/:id/confirm", FeatureChats},
{"POST", "/api/chats/changes/:id/dismiss", FeatureChats},
{"POST", "/api/chats/changes/:id/undo", FeatureChats},
{"GET", "/api/chats/triggers", FeatureTriggers},
{"POST", "/api/chats/triggers", FeatureTriggers},
{"GET", "/api/chats/triggers/preview", FeatureTriggers},
File diff suppressed because one or more lines are too long.
+136
View File
@@ -0,0 +1,136 @@
package studio
import (
"net/http"
"strconv"
"github.com/labstack/echo/v4"
studiosvc "github.com/mudler/LocalAI/core/services/studio"
"github.com/mudler/LocalAI/core/services/studio/proposals"
"github.com/mudler/LocalAI/pkg/addons/contract"
)
var changeRules = []errRule{
fixed(http.StatusNotFound, typeNotFound, "change not found", proposals.ErrNotFound),
verbatim(http.StatusBadRequest, typeInvalid, studiosvc.ErrBadChange.Error()+": ", studiosvc.ErrBadChange),
verbatim(http.StatusConflict, typeConflict, "studio: ", studiosvc.ErrChangeExpired, studiosvc.ErrChangeAnswered,
studiosvc.ErrTargetChanged, studiosvc.ErrNotApplied, studiosvc.ErrCannotRevert, studiosvc.ErrNothingToRevert),
fixed(http.StatusConflict, typeFilesOff, "changes need files, which are off on this server", studiosvc.ErrChangesOff),
}
func failChange(c echo.Context, err error) error { return mapErr(c, err, changeRules, failFor) }
// ListChanges lists the change proposals.
// @Summary List the changes that a chat proposed.
// @Description A model proposes a change to your memory or an agent profile with the propose_change tool, from a Studio chat or a connector chat. Nothing is written until you confirm. A proposal expires after 24 hours. Newest first; status keeps only one status.
// @Tags chats
// @Param status query string false "pending, applied, dismissed, expired, reverted or stale"
// @Success 200 {array} contract.ChangeProposal "Your proposals"
// @Failure 400 {object} schema.ErrorResponse "The status is not known"
// @Router /api/chats/changes [get]
func (h *Handlers) ListChanges(c echo.Context) error {
var statuses []contract.ProposalStatus
if s := c.QueryParam("status"); s != "" {
st := contract.ProposalStatus(s)
switch st {
case contract.ProposalPending, contract.ProposalApplied, contract.ProposalDismissed, contract.ProposalExpired, contract.ProposalReverted, contract.ProposalStale:
statuses = append(statuses, st)
default:
return fail(c, http.StatusBadRequest, typeInvalid, "unknown status")
}
}
list, err := h.svc.Changes().List(c.Request().Context(), userID(c), statuses...)
if err != nil {
return failChange(c, err)
}
if list == nil {
list = []contract.ChangeProposal{}
}
return c.JSON(http.StatusOK, list)
}
// GetChange returns one change proposal.
// @Summary Get a proposed change.
// @Tags chats
// @Param id path string true "the proposal"
// @Success 200 {object} contract.ChangeProposal "The proposal, with the text it would write"
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
// @Router /api/chats/changes/{id} [get]
func (h *Handlers) GetChange(c echo.Context) error {
p, err := h.svc.Changes().Get(c.Request().Context(), userID(c), c.Param("id"))
if err != nil {
return failChange(c, err)
}
return c.JSON(http.StatusOK, p)
}
// ConfirmChange writes a proposed change.
// @Summary Confirm a proposed change.
// @Description Writes the change to your files as one change set that Undo can take back. It is refused with 409 when the file changed since the proposal, or when the proposal expired or was answered.
// @Tags chats
// @Param id path string true "the proposal"
// @Success 200 {object} contract.ChangeProposal "The applied proposal"
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
// @Failure 409 {object} schema.ErrorResponse "Stale, expired or answered"
// @Router /api/chats/changes/{id}/confirm [post]
func (h *Handlers) ConfirmChange(c echo.Context) error {
p, err := h.svc.Changes().Confirm(c.Request().Context(), userID(c), c.Param("id"))
if err != nil {
return failChange(c, err)
}
return c.JSON(http.StatusOK, p)
}
// DismissChange answers a proposed change with no.
// @Summary Dismiss a proposed change.
// @Tags chats
// @Param id path string true "the proposal"
// @Success 200 {object} contract.ChangeProposal "The dismissed proposal"
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
// @Failure 409 {object} schema.ErrorResponse "Not waiting"
// @Router /api/chats/changes/{id}/dismiss [post]
func (h *Handlers) DismissChange(c echo.Context) error {
p, err := h.svc.Changes().Dismiss(c.Request().Context(), userID(c), c.Param("id"))
if err != nil {
return failChange(c, err)
}
return c.JSON(http.StatusOK, p)
}
// UndoChange reverts an applied change.
// @Summary Undo an applied change.
// @Description Restores the file with the change set the confirmation wrote. It is refused with 409 when the file was edited since.
// @Tags chats
// @Param id path string true "the proposal"
// @Success 200 {object} contract.ChangeProposal "The reverted proposal"
// @Failure 404 {object} schema.ErrorResponse "Not yours or unknown"
// @Failure 409 {object} schema.ErrorResponse "Not applied, or edited since"
// @Router /api/chats/changes/{id}/undo [post]
func (h *Handlers) UndoChange(c echo.Context) error {
p, err := h.svc.Changes().Revert(c.Request().Context(), userID(c), c.Param("id"))
if err != nil {
return failChange(c, err)
}
return c.JSON(http.StatusOK, p)
}
// ChangeAudit lists the audit of the changes.
// @Summary List the audit of proposed changes.
// @Description Who proposed, confirmed, dismissed or reverted what, from which source, newest first. It holds a hash of the text, never the text.
// @Tags chats
// @Param limit query int false "most lines (default 50, at most 200)"
// @Success 200 {array} proposals.Event "Your audit lines"
// @Router /api/chats/changes/audit [get]
func (h *Handlers) ChangeAudit(c echo.Context) error {
limit := 50
if v, err := strconv.Atoi(c.QueryParam("limit")); err == nil && v > 0 {
limit = min(v, 200)
}
evs, err := h.svc.Changes().Events(c.Request().Context(), userID(c), limit)
if err != nil {
return failChange(c, err)
}
return c.JSON(http.StatusOK, evs)
}
+6
View File
@@ -179,6 +179,12 @@ func RegisterStudioRoutes(e *echo.Echo, svc *studiosvc.Service, models studioend
// The change feed of the user's files belongs to the chats feature: the
// files are the chats' workspace.
e.GET("/api/workspace/events", h.WorkspaceEvents)
e.GET(base+"/changes", h.ListChanges)
e.GET(base+"/changes/audit", h.ChangeAudit)
e.GET(base+"/changes/:id", h.GetChange)
e.POST(base+"/changes/:id/confirm", h.ConfirmChange)
e.POST(base+"/changes/:id/dismiss", h.DismissChange)
e.POST(base+"/changes/:id/undo", h.UndoChange)
e.GET(base+"/triggers", h.ListTriggers)
e.POST(base+"/triggers", h.CreateTrigger)
e.GET(base+"/triggers/preview", h.PreviewTrigger)
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
//go:build auth
package routes_test
import (
"encoding/json"
"net/http"
"github.com/mudler/LocalAI/core/services/studio"
"github.com/mudler/LocalAI/core/services/studio/studiotest"
"github.com/mudler/LocalAI/core/services/studio/workspace"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Chats proposed changes", func() {
It("lists, confirms, dismisses and undoes what a model proposed, for its owner only", func() {
args := func(target, content string) string {
raw, _ := json.Marshal(map[string]string{"kind": "memory", "scope": "user", "target": target, "content": content, "summary": "note " + target})
return string(raw)
}
h := &studiotest.ToolHarness{Script: func(_ workspace.Actor, text string) []studiotest.ToolStep {
return []studiotest.ToolStep{{Tool: "propose_change", Args: args(text, "- fact "+text+"\n")}, {Say: "ok"}}
}}
s := newStudioServerWith(newTestFiles(), func(d *studio.Deps) { d.Harness = h })
id := s.create("alice")
propose := func(target string) string {
s.readEvents("alice", id, func() {
defer GinkgoRecover()
s.do("alice", http.MethodPost, "/api/chats/"+id+"/messages", map[string]any{"text": target})
}, func(kind string) bool { return kind == "turn_end" })
var list []map[string]any
rec := s.do("alice", http.MethodGet, "/api/chats/changes?status=pending", nil)
Expect(rec.Code).To(Equal(http.StatusOK), rec.Body.String())
Expect(json.Unmarshal(rec.Body.Bytes(), &list)).To(Succeed())
Expect(list).NotTo(BeEmpty())
return list[0]["id"].(string)
}
a := propose("a.md")
url := "/api/chats/changes/" + a
Expect(s.do("bob", http.MethodGet, url, nil).Code).To(Equal(http.StatusNotFound))
Expect(s.do("bob", http.MethodPost, url+"/confirm", nil).Code).To(Equal(http.StatusNotFound))
got := decodeJSON(s.do("alice", http.MethodGet, url, nil))
Expect(got).To(And(HaveKeyWithValue("status", "pending"), HaveKeyWithValue("target", "Memory/a.md"), HaveKeyWithValue("v", float64(1))))
Expect(s.do("alice", http.MethodGet, "/api/chats/files/content?path=/Memory/a.md", nil).Code).To(Equal(http.StatusNotFound))
conf := s.do("alice", http.MethodPost, url+"/confirm", nil)
Expect(conf.Code).To(Equal(http.StatusOK), conf.Body.String())
Expect(decodeJSON(conf)).To(HaveKeyWithValue("status", "applied"))
file := s.do("alice", http.MethodGet, "/api/chats/files/content?path=/Memory/a.md", nil)
Expect(file.Code).To(Equal(http.StatusOK))
Expect(file.Body.String()).To(Equal("- fact a.md\n"))
Expect(s.do("alice", http.MethodPost, url+"/confirm", nil).Code).To(Equal(http.StatusConflict))
undo := s.do("alice", http.MethodPost, url+"/undo", nil)
Expect(undo.Code).To(Equal(http.StatusOK), undo.Body.String())
Expect(decodeJSON(undo)).To(HaveKeyWithValue("status", "reverted"))
Expect(s.do("alice", http.MethodGet, "/api/chats/files/content?path=/Memory/a.md", nil).Code).To(Equal(http.StatusNotFound))
Expect(s.do("alice", http.MethodPost, url+"/undo", nil).Code).To(Equal(http.StatusConflict))
b := propose("b.md")
dis := s.do("alice", http.MethodPost, "/api/chats/changes/"+b+"/dismiss", nil)
Expect(dis.Code).To(Equal(http.StatusOK), dis.Body.String())
Expect(decodeJSON(dis)).To(HaveKeyWithValue("status", "dismissed"))
Expect(s.do("alice", http.MethodGet, "/api/chats/changes?status=bogus", nil).Code).To(Equal(http.StatusBadRequest))
var audit []map[string]any
rec := s.do("alice", http.MethodGet, "/api/chats/changes/audit", nil)
Expect(rec.Code).To(Equal(http.StatusOK))
Expect(json.Unmarshal(rec.Body.Bytes(), &audit)).To(Succeed())
Expect(audit).To(HaveLen(5), "propose, confirm, revert, propose, dismiss: %v", audit)
Expect(rec.Body.String()).NotTo(ContainSubstring("fact"))
var none []map[string]any
Expect(json.Unmarshal(s.do("bob", http.MethodGet, "/api/chats/changes", nil).Body.Bytes(), &none)).To(Succeed())
Expect(none).To(BeEmpty())
})
})
+507 -8
View File
@@ -3708,7 +3708,7 @@ const docTemplate = `{
}
},
"409": {
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing)",
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
@@ -4690,7 +4690,7 @@ const docTemplate = `{
},
"/api/chats/agents/{id}/export": {
"get": {
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's path in your files now (empty when the folder is missing or not chosen); a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's home-relative path now (empty when the folder is missing, not chosen, or outside your home, even if readable); importing an empty path preserves the scope as unset with a warning; a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
"tags": [
"chats"
],
@@ -4746,6 +4746,212 @@ const docTemplate = `{
}
}
},
"/api/chats/changes": {
"get": {
"description": "A model proposes a change to your memory or an agent profile with the propose_change tool, from a Studio chat or a connector chat. Nothing is written until you confirm. A proposal expires after 24 hours. Newest first; status keeps only one status.",
"tags": [
"chats"
],
"summary": "List the changes that a chat proposed.",
"parameters": [
{
"type": "string",
"description": "pending, applied, dismissed, expired, reverted or stale",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "Your proposals",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/contract.ChangeProposal"
}
}
},
"400": {
"description": "The status is not known",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/audit": {
"get": {
"description": "Who proposed, confirmed, dismissed or reverted what, from which source, newest first. It holds a hash of the text, never the text.",
"tags": [
"chats"
],
"summary": "List the audit of proposed changes.",
"parameters": [
{
"type": "integer",
"description": "most lines (default 50, at most 200)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "Your audit lines",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/proposals.Event"
}
}
}
}
}
},
"/api/chats/changes/{id}": {
"get": {
"tags": [
"chats"
],
"summary": "Get a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The proposal, with the text it would write",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/confirm": {
"post": {
"description": "Writes the change to your files as one change set that Undo can take back. It is refused with 409 when the file changed since the proposal, or when the proposal expired or was answered.",
"tags": [
"chats"
],
"summary": "Confirm a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The applied proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Stale, expired or answered",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/dismiss": {
"post": {
"tags": [
"chats"
],
"summary": "Dismiss a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The dismissed proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Not waiting",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/undo": {
"post": {
"description": "Restores the file with the change set the confirmation wrote. It is refused with 409 when the file was edited since.",
"tags": [
"chats"
],
"summary": "Undo an applied change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The reverted proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Not applied, or edited since",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/connectors": {
"get": {
"description": "A binding connects a bot of a messaging platform (served by a connector add-on, such as Telegram) to one of your saved agents. A person who messages the bot talks to that agent, in a conversation of their own, as you: your quota, your model allowlist and only the agent's grants apply.",
@@ -9356,7 +9562,7 @@ const docTemplate = `{
}
},
"409": {
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing)",
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
@@ -14593,6 +14799,9 @@ const docTemplate = `{
"canStop": {
"type": "boolean"
},
"hostOperations": {
"$ref": "#/definitions/hostops.RequestSummary"
},
"managementUrl": {
"description": "ManagementURL opens the management view of the addon.",
"type": "string",
@@ -14792,6 +15001,32 @@ const docTemplate = `{
}
}
},
"apps.HomeCommandView": {
"type": "object",
"properties": {
"alias": {
"type": "string"
},
"icon": {
"type": "string"
},
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"page": {
"type": "string"
}
}
},
"apps.List": {
"type": "object",
"properties": {
@@ -14895,6 +15130,20 @@ const docTemplate = `{
"type": "string",
"example": "/api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js"
},
"homeCommands": {
"description": "HomeCommands contains only resolved navigation metadata for this app.",
"type": "array",
"items": {
"$ref": "#/definitions/apps.HomeCommandView"
}
},
"modelActions": {
"description": "ModelActions contains only declared destinations for this installed UI.",
"type": "array",
"items": {
"$ref": "#/definitions/contract.ModelAction"
}
},
"navigation": {
"type": "array",
"items": {
@@ -15140,6 +15389,124 @@ const docTemplate = `{
}
}
},
"contract.ChangeKind": {
"type": "string",
"enum": [
"memory",
"profile",
"skill",
"page",
"dashboard"
],
"x-enum-varnames": [
"ChangeMemory",
"ChangeProfile",
"ChangeSkill",
"ChangePage",
"ChangeDashboard"
]
},
"contract.ChangeProposal": {
"type": "object",
"properties": {
"agent": {
"type": "string"
},
"answered": {
"type": "string"
},
"base_hash": {
"type": "string"
},
"content": {
"type": "string"
},
"created": {
"type": "string"
},
"expires": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/contract.ChangeKind"
},
"owner": {
"type": "string"
},
"rev": {
"description": "Rev is the workspace change set that applying wrote; RevertRev the one\nthat reverting wrote. Zero until then.",
"type": "integer"
},
"revert_rev": {
"type": "integer"
},
"scope": {
"$ref": "#/definitions/contract.ChangeScope"
},
"source": {
"$ref": "#/definitions/contract.ChangeSource"
},
"status": {
"$ref": "#/definitions/contract.ProposalStatus"
},
"summary": {
"type": "string"
},
"target": {
"type": "string"
},
"v": {
"type": "integer"
}
}
},
"contract.ChangeScope": {
"type": "string",
"enum": [
"user",
"agent",
"channel"
],
"x-enum-varnames": [
"ScopeUser",
"ScopeAgent",
"ScopeChannel"
]
},
"contract.ChangeSource": {
"type": "object",
"properties": {
"chat": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"type": "string"
},
"thread": {
"type": "string"
}
}
},
"contract.ModelAction": {
"type": "object",
"properties": {
"capability": {
"type": "string"
},
"page": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"contract.OutputDescription": {
"type": "object",
"properties": {
@@ -15157,6 +15524,25 @@ const docTemplate = `{
}
}
},
"contract.ProposalStatus": {
"type": "string",
"enum": [
"pending",
"applied",
"dismissed",
"expired",
"reverted",
"stale"
],
"x-enum-varnames": [
"ProposalPending",
"ProposalApplied",
"ProposalDismissed",
"ProposalExpired",
"ProposalReverted",
"ProposalStale"
]
},
"contract.Receipt": {
"type": "object",
"properties": {
@@ -15255,11 +15641,11 @@ const docTemplate = `{
"type": "object",
"properties": {
"name": {
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nthat replaces a built-in one has the built-in's name (see Shadows).",
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nfor a reserved document slot has the slot's name (see Shadows).",
"type": "string"
},
"shadows": {
"description": "Shadows names the built-in skill this skill replaces. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
"description": "Shadows names the document-provider slot this skill supplies. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
"type": "string"
}
}
@@ -16566,6 +16952,50 @@ const docTemplate = `{
}
}
},
"hostops.RequestSummary": {
"type": "object",
"properties": {
"granted": {
"type": "array",
"items": {
"type": "string"
}
},
"grantedCount": {
"type": "integer"
},
"missingOptional": {
"type": "array",
"items": {
"type": "string"
}
},
"missingRequired": {
"type": "array",
"items": {
"type": "string"
}
},
"optional": {
"type": "array",
"items": {
"type": "string"
}
},
"optionalCount": {
"type": "integer"
},
"required": {
"type": "array",
"items": {
"type": "string"
}
},
"requiredCount": {
"type": "integer"
}
}
},
"library.Info": {
"type": "object",
"properties": {
@@ -17116,6 +17546,47 @@ const docTemplate = `{
}
}
},
"proposals.Event": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"agent": {
"type": "string"
},
"at": {
"type": "string"
},
"detail": {
"type": "string"
},
"hash": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/contract.ChangeKind"
},
"owner": {
"type": "string"
},
"proposal": {
"type": "string"
},
"rev": {
"type": "integer"
},
"scope": {
"$ref": "#/definitions/contract.ChangeScope"
},
"source": {
"$ref": "#/definitions/contract.ChangeSource"
},
"target": {
"type": "string"
}
}
},
"proto.MemoryUsageData": {
"type": "object",
"properties": {
@@ -17747,6 +18218,9 @@ const docTemplate = `{
"commands_reason": {
"description": "CommandsReason is why commands cannot run, when Commands is unavailable.",
"type": "string"
},
"execution_status": {
"$ref": "#/definitions/schema.ChatsExecutionStatus"
}
}
},
@@ -18082,7 +18556,7 @@ const docTemplate = `{
"type": "string"
},
"path": {
"description": "Path is the folder's path in your files now (\"/\" is all of them). It\nis absent when the folder is missing or not chosen.",
"description": "Path is home-relative (\"/\" is the whole home), or /shared/\u003cname\u003e/...\nfor a followed readable shared folder. It is absent when missing,\nnot chosen, or readable outside these public locations; a readable\nfolder without a public path still has state ok.",
"type": "string"
},
"state": {
@@ -18241,7 +18715,7 @@ const docTemplate = `{
"type": "string"
},
"folder": {
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the folder's path and mode when the file was made.",
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the home-relative path and mode when the file was\nmade. A missing, unchosen or non-home folder has an empty export path;\nimport preserves its mode as unset and warns instead of clearing scope.",
"allOf": [
{
"$ref": "#/definitions/schema.ChatsAgentFolderWrite"
@@ -19139,7 +19613,7 @@ const docTemplate = `{
}
},
"applies": {
"description": "Applies is false when commands run on this machine (applies_reason\nhost) or no Sandbox serves bash (none).",
"description": "Applies is true when the add-on that runs the commands reports that it\nenforces the network policy. It is false when commands run on this\nmachine (applies_reason host), or no Sandbox serves bash or the add-on\ndoes not report enforcement (none).",
"type": "boolean"
},
"applies_reason": {
@@ -19451,6 +19925,31 @@ const docTemplate = `{
}
}
},
"schema.ChatsExecutionStatus": {
"type": "object",
"properties": {
"explanation": {
"type": "string"
},
"generation": {
"type": "string"
},
"incarnation": {
"type": "string"
},
"isolation": {
"type": "string"
},
"readiness": {
"type": "string"
},
"unknown_reason": {
"description": "UnknownReason says why the status is unknown: owner_unreachable,\nnot_reported, stale, invalid_status, not_selected, no_answer or\nreported_unknown. Absent when the status is known.",
"type": "string",
"example": "owner_unreachable"
}
}
},
"schema.ChatsExport": {
"type": "object",
"properties": {
+507 -8
View File
@@ -3705,7 +3705,7 @@
}
},
"409": {
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing)",
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
@@ -4687,7 +4687,7 @@
},
"/api/chats/agents/{id}/export": {
"get": {
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's path in your files now (empty when the folder is missing or not chosen); a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's home-relative path now (empty when the folder is missing, not chosen, or outside your home, even if readable); importing an empty path preserves the scope as unset with a warning; a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
"tags": [
"chats"
],
@@ -4743,6 +4743,212 @@
}
}
},
"/api/chats/changes": {
"get": {
"description": "A model proposes a change to your memory or an agent profile with the propose_change tool, from a Studio chat or a connector chat. Nothing is written until you confirm. A proposal expires after 24 hours. Newest first; status keeps only one status.",
"tags": [
"chats"
],
"summary": "List the changes that a chat proposed.",
"parameters": [
{
"type": "string",
"description": "pending, applied, dismissed, expired, reverted or stale",
"name": "status",
"in": "query"
}
],
"responses": {
"200": {
"description": "Your proposals",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/contract.ChangeProposal"
}
}
},
"400": {
"description": "The status is not known",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/audit": {
"get": {
"description": "Who proposed, confirmed, dismissed or reverted what, from which source, newest first. It holds a hash of the text, never the text.",
"tags": [
"chats"
],
"summary": "List the audit of proposed changes.",
"parameters": [
{
"type": "integer",
"description": "most lines (default 50, at most 200)",
"name": "limit",
"in": "query"
}
],
"responses": {
"200": {
"description": "Your audit lines",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/proposals.Event"
}
}
}
}
}
},
"/api/chats/changes/{id}": {
"get": {
"tags": [
"chats"
],
"summary": "Get a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The proposal, with the text it would write",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/confirm": {
"post": {
"description": "Writes the change to your files as one change set that Undo can take back. It is refused with 409 when the file changed since the proposal, or when the proposal expired or was answered.",
"tags": [
"chats"
],
"summary": "Confirm a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The applied proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Stale, expired or answered",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/dismiss": {
"post": {
"tags": [
"chats"
],
"summary": "Dismiss a proposed change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The dismissed proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Not waiting",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/changes/{id}/undo": {
"post": {
"description": "Restores the file with the change set the confirmation wrote. It is refused with 409 when the file was edited since.",
"tags": [
"chats"
],
"summary": "Undo an applied change.",
"parameters": [
{
"type": "string",
"description": "the proposal",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The reverted proposal",
"schema": {
"$ref": "#/definitions/contract.ChangeProposal"
}
},
"404": {
"description": "Not yours or unknown",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
},
"409": {
"description": "Not applied, or edited since",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
}
}
}
},
"/api/chats/connectors": {
"get": {
"description": "A binding connects a bot of a messaging platform (served by a connector add-on, such as Telegram) to one of your saved agents. A person who messages the bot talks to that agent, in a conversation of their own, as you: your quota, your model allowlist and only the agent's grants apply.",
@@ -9353,7 +9559,7 @@
}
},
"409": {
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing)",
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
"schema": {
"$ref": "#/definitions/schema.ErrorResponse"
}
@@ -14590,6 +14796,9 @@
"canStop": {
"type": "boolean"
},
"hostOperations": {
"$ref": "#/definitions/hostops.RequestSummary"
},
"managementUrl": {
"description": "ManagementURL opens the management view of the addon.",
"type": "string",
@@ -14789,6 +14998,32 @@
}
}
},
"apps.HomeCommandView": {
"type": "object",
"properties": {
"alias": {
"type": "string"
},
"icon": {
"type": "string"
},
"id": {
"type": "string"
},
"label": {
"type": "string"
},
"labels": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"page": {
"type": "string"
}
}
},
"apps.List": {
"type": "object",
"properties": {
@@ -14892,6 +15127,20 @@
"type": "string",
"example": "/api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js"
},
"homeCommands": {
"description": "HomeCommands contains only resolved navigation metadata for this app.",
"type": "array",
"items": {
"$ref": "#/definitions/apps.HomeCommandView"
}
},
"modelActions": {
"description": "ModelActions contains only declared destinations for this installed UI.",
"type": "array",
"items": {
"$ref": "#/definitions/contract.ModelAction"
}
},
"navigation": {
"type": "array",
"items": {
@@ -15137,6 +15386,124 @@
}
}
},
"contract.ChangeKind": {
"type": "string",
"enum": [
"memory",
"profile",
"skill",
"page",
"dashboard"
],
"x-enum-varnames": [
"ChangeMemory",
"ChangeProfile",
"ChangeSkill",
"ChangePage",
"ChangeDashboard"
]
},
"contract.ChangeProposal": {
"type": "object",
"properties": {
"agent": {
"type": "string"
},
"answered": {
"type": "string"
},
"base_hash": {
"type": "string"
},
"content": {
"type": "string"
},
"created": {
"type": "string"
},
"expires": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/contract.ChangeKind"
},
"owner": {
"type": "string"
},
"rev": {
"description": "Rev is the workspace change set that applying wrote; RevertRev the one\nthat reverting wrote. Zero until then.",
"type": "integer"
},
"revert_rev": {
"type": "integer"
},
"scope": {
"$ref": "#/definitions/contract.ChangeScope"
},
"source": {
"$ref": "#/definitions/contract.ChangeSource"
},
"status": {
"$ref": "#/definitions/contract.ProposalStatus"
},
"summary": {
"type": "string"
},
"target": {
"type": "string"
},
"v": {
"type": "integer"
}
}
},
"contract.ChangeScope": {
"type": "string",
"enum": [
"user",
"agent",
"channel"
],
"x-enum-varnames": [
"ScopeUser",
"ScopeAgent",
"ScopeChannel"
]
},
"contract.ChangeSource": {
"type": "object",
"properties": {
"chat": {
"type": "string"
},
"id": {
"type": "string"
},
"kind": {
"type": "string"
},
"thread": {
"type": "string"
}
}
},
"contract.ModelAction": {
"type": "object",
"properties": {
"capability": {
"type": "string"
},
"page": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"contract.OutputDescription": {
"type": "object",
"properties": {
@@ -15154,6 +15521,25 @@
}
}
},
"contract.ProposalStatus": {
"type": "string",
"enum": [
"pending",
"applied",
"dismissed",
"expired",
"reverted",
"stale"
],
"x-enum-varnames": [
"ProposalPending",
"ProposalApplied",
"ProposalDismissed",
"ProposalExpired",
"ProposalReverted",
"ProposalStale"
]
},
"contract.Receipt": {
"type": "object",
"properties": {
@@ -15252,11 +15638,11 @@
"type": "object",
"properties": {
"name": {
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nthat replaces a built-in one has the built-in's name (see Shadows).",
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nfor a reserved document slot has the slot's name (see Shadows).",
"type": "string"
},
"shadows": {
"description": "Shadows names the built-in skill this skill replaces. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
"description": "Shadows names the document-provider slot this skill supplies. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
"type": "string"
}
}
@@ -16563,6 +16949,50 @@
}
}
},
"hostops.RequestSummary": {
"type": "object",
"properties": {
"granted": {
"type": "array",
"items": {
"type": "string"
}
},
"grantedCount": {
"type": "integer"
},
"missingOptional": {
"type": "array",
"items": {
"type": "string"
}
},
"missingRequired": {
"type": "array",
"items": {
"type": "string"
}
},
"optional": {
"type": "array",
"items": {
"type": "string"
}
},
"optionalCount": {
"type": "integer"
},
"required": {
"type": "array",
"items": {
"type": "string"
}
},
"requiredCount": {
"type": "integer"
}
}
},
"library.Info": {
"type": "object",
"properties": {
@@ -17113,6 +17543,47 @@
}
}
},
"proposals.Event": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"agent": {
"type": "string"
},
"at": {
"type": "string"
},
"detail": {
"type": "string"
},
"hash": {
"type": "string"
},
"kind": {
"$ref": "#/definitions/contract.ChangeKind"
},
"owner": {
"type": "string"
},
"proposal": {
"type": "string"
},
"rev": {
"type": "integer"
},
"scope": {
"$ref": "#/definitions/contract.ChangeScope"
},
"source": {
"$ref": "#/definitions/contract.ChangeSource"
},
"target": {
"type": "string"
}
}
},
"proto.MemoryUsageData": {
"type": "object",
"properties": {
@@ -17744,6 +18215,9 @@
"commands_reason": {
"description": "CommandsReason is why commands cannot run, when Commands is unavailable.",
"type": "string"
},
"execution_status": {
"$ref": "#/definitions/schema.ChatsExecutionStatus"
}
}
},
@@ -18079,7 +18553,7 @@
"type": "string"
},
"path": {
"description": "Path is the folder's path in your files now (\"/\" is all of them). It\nis absent when the folder is missing or not chosen.",
"description": "Path is home-relative (\"/\" is the whole home), or /shared/\u003cname\u003e/...\nfor a followed readable shared folder. It is absent when missing,\nnot chosen, or readable outside these public locations; a readable\nfolder without a public path still has state ok.",
"type": "string"
},
"state": {
@@ -18238,7 +18712,7 @@
"type": "string"
},
"folder": {
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the folder's path and mode when the file was made.",
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the home-relative path and mode when the file was\nmade. A missing, unchosen or non-home folder has an empty export path;\nimport preserves its mode as unset and warns instead of clearing scope.",
"allOf": [
{
"$ref": "#/definitions/schema.ChatsAgentFolderWrite"
@@ -19136,7 +19610,7 @@
}
},
"applies": {
"description": "Applies is false when commands run on this machine (applies_reason\nhost) or no Sandbox serves bash (none).",
"description": "Applies is true when the add-on that runs the commands reports that it\nenforces the network policy. It is false when commands run on this\nmachine (applies_reason host), or no Sandbox serves bash or the add-on\ndoes not report enforcement (none).",
"type": "boolean"
},
"applies_reason": {
@@ -19448,6 +19922,31 @@
}
}
},
"schema.ChatsExecutionStatus": {
"type": "object",
"properties": {
"explanation": {
"type": "string"
},
"generation": {
"type": "string"
},
"incarnation": {
"type": "string"
},
"isolation": {
"type": "string"
},
"readiness": {
"type": "string"
},
"unknown_reason": {
"description": "UnknownReason says why the status is unknown: owner_unreachable,\nnot_reported, stale, invalid_status, not_selected, no_answer or\nreported_unknown. Absent when the status is known.",
"type": "string",
"example": "owner_unreachable"
}
}
},
"schema.ChatsExport": {
"type": "object",
"properties": {
+372 -13
View File
@@ -166,6 +166,8 @@ definitions:
type: boolean
canStop:
type: boolean
hostOperations:
$ref: '#/definitions/hostops.RequestSummary'
managementUrl:
description: ManagementURL opens the management view of the addon.
example: /app/addons?manage=notes
@@ -351,6 +353,23 @@ definitions:
example: summarize
type: string
type: object
apps.HomeCommandView:
properties:
alias:
type: string
icon:
type: string
id:
type: string
label:
type: string
labels:
additionalProperties:
type: string
type: object
page:
type: string
type: object
apps.List:
properties:
apps:
@@ -441,6 +460,18 @@ definitions:
package digest, so a new package is a new URL.
example: /api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js
type: string
homeCommands:
description: HomeCommands contains only resolved navigation metadata for this
app.
items:
$ref: '#/definitions/apps.HomeCommandView'
type: array
modelActions:
description: ModelActions contains only declared destinations for this installed
UI.
items:
$ref: '#/definitions/contract.ModelAction'
type: array
navigation:
items:
type: string
@@ -627,6 +658,90 @@ definitions:
name:
type: string
type: object
contract.ChangeKind:
enum:
- memory
- profile
- skill
- page
- dashboard
type: string
x-enum-varnames:
- ChangeMemory
- ChangeProfile
- ChangeSkill
- ChangePage
- ChangeDashboard
contract.ChangeProposal:
properties:
agent:
type: string
answered:
type: string
base_hash:
type: string
content:
type: string
created:
type: string
expires:
type: string
id:
type: string
kind:
$ref: '#/definitions/contract.ChangeKind'
owner:
type: string
rev:
description: |-
Rev is the workspace change set that applying wrote; RevertRev the one
that reverting wrote. Zero until then.
type: integer
revert_rev:
type: integer
scope:
$ref: '#/definitions/contract.ChangeScope'
source:
$ref: '#/definitions/contract.ChangeSource'
status:
$ref: '#/definitions/contract.ProposalStatus'
summary:
type: string
target:
type: string
v:
type: integer
type: object
contract.ChangeScope:
enum:
- user
- agent
- channel
type: string
x-enum-varnames:
- ScopeUser
- ScopeAgent
- ScopeChannel
contract.ChangeSource:
properties:
chat:
type: string
id:
type: string
kind:
type: string
thread:
type: string
type: object
contract.ModelAction:
properties:
capability:
type: string
page:
type: string
path:
type: string
type: object
contract.OutputDescription:
properties:
handle:
@@ -638,6 +753,22 @@ definitions:
size:
type: integer
type: object
contract.ProposalStatus:
enum:
- pending
- applied
- dismissed
- expired
- reverted
- stale
type: string
x-enum-varnames:
- ProposalPending
- ProposalApplied
- ProposalDismissed
- ProposalExpired
- ProposalReverted
- ProposalStale
contract.Receipt:
properties:
operationId:
@@ -708,11 +839,11 @@ definitions:
Name is the skill's name and the name of its folder. A plain skill is
named after the add-on: the add-on ID, or the ID and a dash and more
(office-quickstart), so two add-ons never ship one plain name. A skill
that replaces a built-in one has the built-in's name (see Shadows).
for a reserved document slot has the slot's name (see Shadows).
type: string
shadows:
description: |-
Shadows names the built-in skill this skill replaces. It must equal Name
Shadows names the document-provider slot this skill supplies. It must equal Name
and be one of ReplaceableSkills. It repeats the name on purpose: the
replacement is an explicit act, which the install warning and the check
read from the manifest without opening the package. Empty: a plain
@@ -1729,6 +1860,35 @@ definitions:
example: 4f1c
type: string
type: object
hostops.RequestSummary:
properties:
granted:
items:
type: string
type: array
grantedCount:
type: integer
missingOptional:
items:
type: string
type: array
missingRequired:
items:
type: string
type: array
optional:
items:
type: string
type: array
optionalCount:
type: integer
required:
items:
type: string
type: array
requiredCount:
type: integer
type: object
library.Info:
properties:
authReference:
@@ -2126,6 +2286,33 @@ definitions:
updated_at:
type: string
type: object
proposals.Event:
properties:
action:
type: string
agent:
type: string
at:
type: string
detail:
type: string
hash:
type: string
kind:
$ref: '#/definitions/contract.ChangeKind'
owner:
type: string
proposal:
type: string
rev:
type: integer
scope:
$ref: '#/definitions/contract.ChangeScope'
source:
$ref: '#/definitions/contract.ChangeSource'
target:
type: string
type: object
proto.MemoryUsageData:
properties:
breakdown:
@@ -2587,6 +2774,8 @@ definitions:
commands_reason:
description: CommandsReason is why commands cannot run, when Commands is unavailable.
type: string
execution_status:
$ref: '#/definitions/schema.ChatsExecutionStatus'
type: object
schema.ChatsAddonSwitches:
properties:
@@ -2834,8 +3023,10 @@ definitions:
type: string
path:
description: |-
Path is the folder's path in your files now ("/" is all of them). It
is absent when the folder is missing or not chosen.
Path is home-relative ("/" is the whole home), or /shared/<name>/...
for a followed readable shared folder. It is absent when missing,
not chosen, or readable outside these public locations; a readable
folder without a public path still has state ok.
type: string
state:
description: |-
@@ -2962,7 +3153,9 @@ definitions:
description: |-
Folder chooses the one folder the agent's chats work in. A write
without it keeps the folder the agent has; {"path": ""} clears it. In
an export file it is the folder's path and mode when the file was made.
an export file it is the home-relative path and mode when the file was
made. A missing, unchosen or non-home folder has an empty export path;
import preserves its mode as unset and warns instead of clearing scope.
instructions:
type: string
mcp_servers:
@@ -3680,8 +3873,10 @@ definitions:
type: array
applies:
description: |-
Applies is false when commands run on this machine (applies_reason
host) or no Sandbox serves bash (none).
Applies is true when the add-on that runs the commands reports that it
enforces the network policy. It is false when commands run on this
machine (applies_reason host), or no Sandbox serves bash or the add-on
does not report enforcement (none).
type: boolean
applies_reason:
type: string
@@ -3899,6 +4094,26 @@ definitions:
- $ref: '#/definitions/schema.ChatsUsage'
description: 'Usage is set on usage events: what one model request used.'
type: object
schema.ChatsExecutionStatus:
properties:
explanation:
type: string
generation:
type: string
incarnation:
type: string
isolation:
type: string
readiness:
type: string
unknown_reason:
description: |-
UnknownReason says why the status is unknown: owner_unreachable,
not_reported, stale, invalid_status, not_selected, no_answer or
reported_unknown. Absent when the status is known.
example: owner_unreachable
type: string
type: object
schema.ChatsExport:
properties:
created:
@@ -12025,7 +12240,8 @@ paths:
schema:
$ref: '#/definitions/schema.ErrorResponse'
"409":
description: The saved agent's folder is missing or not chosen (agent_folder_missing)
description: The saved agent's folder is missing or not chosen (agent_folder_missing),
or its complete file access exceeds the folder limit (agent_folder_limit)
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Start a chat.
@@ -13163,7 +13379,8 @@ paths:
$ref: '#/definitions/schema.ErrorResponse'
"409":
description: A turn is running for a command, or the saved agent's folder
is missing (agent_folder_missing)
is missing (agent_folder_missing), or its complete file access exceeds
the folder limit (agent_folder_limit)
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Send a message to a chat.
@@ -14026,10 +14243,11 @@ paths:
get:
description: Returns the definition as a JSON file (version, kind localai-agent,
agent). It holds no ID, owner or time. An agent with a folder is version 2
and carries folder {path, mode}, the folder's path in your files now (empty
when the folder is missing or not chosen); a server that knows only version
1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import
reads it. This replaces the old /api/agents/{name}/export.
and carries folder {path, mode}, the folder's home-relative path now (empty
when the folder is missing, not chosen, or outside your home, even if readable);
importing an empty path preserves the scope as unset with a warning; a server
that knows only version 1 refuses it. An agent without a folder is version
1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.
parameters:
- description: the agent's id
in: path
@@ -14133,6 +14351,147 @@ paths:
summary: Save a starter agent as your own.
tags:
- chats
/api/chats/changes:
get:
description: A model proposes a change to your memory or an agent profile with
the propose_change tool, from a Studio chat or a connector chat. Nothing is
written until you confirm. A proposal expires after 24 hours. Newest first;
status keeps only one status.
parameters:
- description: pending, applied, dismissed, expired, reverted or stale
in: query
name: status
type: string
responses:
"200":
description: Your proposals
schema:
items:
$ref: '#/definitions/contract.ChangeProposal'
type: array
"400":
description: The status is not known
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: List the changes that a chat proposed.
tags:
- chats
/api/chats/changes/{id}:
get:
parameters:
- description: the proposal
in: path
name: id
required: true
type: string
responses:
"200":
description: The proposal, with the text it would write
schema:
$ref: '#/definitions/contract.ChangeProposal'
"404":
description: Not yours or unknown
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Get a proposed change.
tags:
- chats
/api/chats/changes/{id}/confirm:
post:
description: Writes the change to your files as one change set that Undo can
take back. It is refused with 409 when the file changed since the proposal,
or when the proposal expired or was answered.
parameters:
- description: the proposal
in: path
name: id
required: true
type: string
responses:
"200":
description: The applied proposal
schema:
$ref: '#/definitions/contract.ChangeProposal'
"404":
description: Not yours or unknown
schema:
$ref: '#/definitions/schema.ErrorResponse'
"409":
description: Stale, expired or answered
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Confirm a proposed change.
tags:
- chats
/api/chats/changes/{id}/dismiss:
post:
parameters:
- description: the proposal
in: path
name: id
required: true
type: string
responses:
"200":
description: The dismissed proposal
schema:
$ref: '#/definitions/contract.ChangeProposal'
"404":
description: Not yours or unknown
schema:
$ref: '#/definitions/schema.ErrorResponse'
"409":
description: Not waiting
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Dismiss a proposed change.
tags:
- chats
/api/chats/changes/{id}/undo:
post:
description: Restores the file with the change set the confirmation wrote. It
is refused with 409 when the file was edited since.
parameters:
- description: the proposal
in: path
name: id
required: true
type: string
responses:
"200":
description: The reverted proposal
schema:
$ref: '#/definitions/contract.ChangeProposal'
"404":
description: Not yours or unknown
schema:
$ref: '#/definitions/schema.ErrorResponse'
"409":
description: Not applied, or edited since
schema:
$ref: '#/definitions/schema.ErrorResponse'
summary: Undo an applied change.
tags:
- chats
/api/chats/changes/audit:
get:
description: Who proposed, confirmed, dismissed or reverted what, from which
source, newest first. It holds a hash of the text, never the text.
parameters:
- description: most lines (default 50, at most 200)
in: query
name: limit
type: integer
responses:
"200":
description: Your audit lines
schema:
items:
$ref: '#/definitions/proposals.Event'
type: array
summary: List the audit of proposed changes.
tags:
- chats
/api/chats/connectors:
get:
description: 'A binding connects a bot of a messaging platform (served by a