From 865d52267dd23481063f79451eec69e4f71317bf Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:20:35 -0400 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E5=A2=9E=E5=8A=A0=E5=AE=9E?= =?UTF-8?q?=E9=AA=8C=E5=8A=9F=E8=83=BD=E5=BC=80=E5=85=B3=EF=BC=8C=E5=8F=AF?= =?UTF-8?q?=E5=BC=80=E5=90=AF=20noa=20=E4=B8=8A=E4=B8=8B=E6=96=87=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 系统设置新增「实验功能」区块,内含 noa 模型驱动上下文压缩开关(默认关)。 开启后平台接入的四类 agent(planner / worker / 主 agent / 对话)由 noa 接管 上下文压缩,取代内置 compaction;关闭即恢复内置压缩。 - agent: 新增 enableNoa 共享接入器(noaadapter.Enable 唯一入口,all-or-nothing), 四类 agent 各加 noaEnabledFn 解析器 + SetNoaEnabled,每 run 读一次,切换只影响 之后启动的 run,无需重建 agent。归档落在各 agent 本 run 工作目录下(任务级持久), session id 与 transcript 同规则。启用失败经 onWarn 上报并回退内置压缩,不中断任务。 - server: settings key noa_compaction + Manager 读写;8 处 agent 装配点注入平台级 解析器(server 5 + task_llm 3);GET/PUT /api/settings 增加字段。 - web: 系统设置页新增实验功能卡片与开关(乐观更新、失败回滚),类型与 mock 补字段。 官方默认行为不变:开关默认关闭时 noaadapter.Enable 不被调用,内置 compaction 原样工作。 --- agent/chat.go | 8 ++++ agent/mainagent.go | 13 ++++++ agent/noa.go | 47 +++++++++++++++++++++ agent/planner.go | 9 ++++ agent/worker.go | 12 ++++++ server/manager.go | 17 ++++++++ server/server.go | 17 ++++++++ server/task_llm.go | 3 ++ web/src/app/(main)/system/settings/page.tsx | 47 ++++++++++++++++++++- web/src/lib/mock/data.ts | 1 + web/src/lib/types.ts | 4 ++ 11 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 agent/noa.go diff --git a/agent/chat.go b/agent/chat.go index 221cc0e..75bc9d4 100644 --- a/agent/chat.go +++ b/agent/chat.go @@ -33,6 +33,7 @@ type ChatAgent struct { webSearch WebSearchOpts guard *guard.Guard // optional; nil disables intercept hooks for chat nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming) + noaEnabledFn func() bool // resolver: use experimental noa compaction? (nil = off) maxTokensFn func() int // resolver: per-reply output cap (nil/0 = send no cap) } @@ -46,6 +47,11 @@ func (c *ChatAgent) SetNonStreaming(fn func() bool) { c.nonStreamingFn = fn } func (c *ChatAgent) nonStreaming() bool { return c.nonStreamingFn != nil && c.nonStreamingFn() } +// SetNoaEnabled wires a resolver deciding whether chat runs use the experimental +// noa context-compression mechanism. nil/unset = off (built-in compaction). Read +// per run so the settings toggle takes effect without rebuilding the agent. +func (c *ChatAgent) SetNoaEnabled(fn func() bool) { c.noaEnabledFn = fn } + // SetMaxTokens wires a resolver for the per-reply output cap. nil/unset or 0 = // send no cap and let the endpoint decide. Read per run, like nonStreaming. func (c *ChatAgent) SetMaxTokens(fn func() int) { c.maxTokensFn = fn } @@ -161,6 +167,8 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin opts.Transcript = c.tx opts.SessionID = sessionID } + // 实验功能:开启后由 noa 接管上下文压缩(归档落在本会话 sessionWorkDir 下,持久)。 + enableNoa(&opts, c.noaEnabledFn, sessionWorkDir, "chat-"+sessionID, noaWarn("chat-"+sessionID)) ctx = attachSideCapture(ctx, &opts) s := agentcore.NewSession(opts) defer s.Close() diff --git a/agent/mainagent.go b/agent/mainagent.go index 42c5658..e8a1726 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -30,9 +30,15 @@ type MainAgent struct { workDir string // shared work dir (surfaced in prompt as artifact-output target) steerWork func(intentID int64, msg string) error // engine callback: steer a running work (nil = off) nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming) + noaEnabledFn func() bool // resolver: use experimental noa compaction? (nil = off) maxTokensFn func() int // resolver: per-reply output cap (nil/0 = send no cap) } +// SetNoaEnabled wires a resolver deciding whether runs use the experimental noa +// context-compression mechanism. nil/unset = off (built-in compaction). Read per +// run so the settings toggle takes effect without rebuilding the agent. +func (m *MainAgent) SetNoaEnabled(fn func() bool) { m.noaEnabledFn = fn } + // SetNonStreaming wires a resolver deciding whether runs use the non-streaming // model path (true = non-streaming). nil/unset = streaming (default). func (m *MainAgent) SetNonStreaming(fn func() bool) { m.nonStreamingFn = fn } @@ -166,6 +172,13 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db. opts.SessionID = fmt.Sprintf("exp%d-main-s%d", ts.ID(), mainSeg) } } + // 实验功能:开启后由 noa 接管上下文压缩(归档落在本任务 mainDir 下,任务级持久)。 + // session id 与 transcript 同规则(分段感知),使归档与恢复对齐。 + noaSession := fmt.Sprintf("exp%d-main", ts.ID()) + if mainSeg > 0 { + noaSession = fmt.Sprintf("exp%d-main-s%d", ts.ID(), mainSeg) + } + enableNoa(&opts, m.noaEnabledFn, mainDir, noaSession, noaWarn(noaSession)) ctx = attachSideCapture(ctx, &opts) s := agentcore.NewSession(opts) defer s.Close() diff --git a/agent/noa.go b/agent/noa.go new file mode 100644 index 0000000..9f6d197 --- /dev/null +++ b/agent/noa.go @@ -0,0 +1,47 @@ +package agent + +import ( + "log" + "path/filepath" + + "github.com/Autumn-27/norma/agentcore" + "github.com/Autumn-27/norma/noaadapter" +) + +// noaWarn returns a diagnostics sink tagging non-fatal noa messages with the +// session, routed through the package logger (agents have no per-instance one). +func noaWarn(session string) func(string) { + return func(msg string) { log.Printf("[noa] %s: %s", session, msg) } +} + +// noa 是 norma v0.4.0 引入的「模型驱动上下文压缩」机制,作为平台实验功能由用户在 +// 系统设置中开关。它与内置 compaction 互斥:noaadapter.Enable 是唯一入口,一次挂上 +// 上下文接管器(Compactor)、Compress 工具与三段常驻提示词,不调用 Enable 即为关闭 +// (内置 compaction 照常工作)。开关由每个 agent 注入的 noaEnabledFn 解析,每 run 读 +// 一次,故切换只影响之后启动的 run,无需重建 agent。 + +// enableNoa 在解析器报告开启时把 noa 接入 opts。archiveRoot 是压缩原文的持久化基目录 +// (取各 agent 本 run 的工作目录,任务级持久),sessionID 命名其下的归档子目录。 +// +// noa 是实验功能:接入失败不得中断真实任务。发生错误时经 onWarn 上报并回退内置压缩。 +// 启用成功时清掉 opts.Compaction,避免 agentcore 因「两个上下文管理器同时设置」告警。 +func enableNoa(opts *agentcore.Options, enabled func() bool, archiveRoot, sessionID string, onWarn func(string)) { + if enabled == nil || !enabled() { + return + } + if opts.OnWarn == nil { + opts.OnWarn = onWarn + } + if err := noaadapter.Enable(opts, noaadapter.Options{ + ArchiveBaseDir: filepath.Join(archiveRoot, "noa"), + SessionID: sessionID, + OnWarn: onWarn, + }); err != nil { + if onWarn != nil { + onWarn("noa 压缩启用失败,回退内置压缩:" + err.Error()) + } + return + } + // Compactor 覆盖 Compaction,但两者并存时 agentcore 每次会告警;明确清掉。 + opts.Compaction = nil +} diff --git a/agent/planner.go b/agent/planner.go index de50d20..d71eec4 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -35,6 +35,7 @@ type Planner struct { workDir string // shared work dir (surfaced in prompt as artifact-output target) injectConstraints func() bool // resolver: inject task operation constraints into system prompt? (nil = yes) nonStreamingFn func() bool // resolver: use non-streaming (Complete) path? (nil = streaming) + noaEnabledFn func() bool // resolver: use experimental noa compaction? (nil = off) maxTokensFn func() int // resolver: per-reply output cap (nil/0 = send no cap) compactor *Compactor // cold-node compaction (§7); nil = disabled @@ -63,6 +64,11 @@ func (p *Planner) SetNonStreaming(fn func() bool) { p.nonStreamingFn = fn } func (p *Planner) nonStreaming() bool { return p.nonStreamingFn != nil && p.nonStreamingFn() } +// SetNoaEnabled wires a resolver deciding whether runs use the experimental noa +// context-compression mechanism. nil/unset = off (built-in compaction). Read per +// run so the settings toggle takes effect without rebuilding the agent. +func (p *Planner) SetNoaEnabled(fn func() bool) { p.noaEnabledFn = fn } + // SetMaxTokens wires a resolver for the per-reply output cap. nil/unset or 0 = // send no cap and let the endpoint decide. Read per run, like nonStreaming. func (p *Planner) SetMaxTokens(fn func() int) { p.maxTokensFn = fn } @@ -429,6 +435,9 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts opts.Transcript = p.tx opts.SessionID = fmt.Sprintf("exp%d-planner", ts.ID()) } + // 实验功能:开启后由 noa 接管上下文压缩(归档落在本任务 taskDir 下,任务级持久)。 + noaSession := fmt.Sprintf("exp%d-planner", ts.ID()) + enableNoa(&opts, p.noaEnabledFn, taskDir, noaSession, noaWarn(noaSession)) // 态势(刚完成的意图 + 完整图)现在拼进本轮 user 输入(见下方 input)。user 里还有 // 指令 + 跨唤醒待办(todo 是模型自己的规划便签,可再生,放 user 即可)。 // 开场白按「本轮有无具体变动」分两种:有变动 → 指向下方【实际变动】块;无变动 diff --git a/agent/worker.go b/agent/worker.go index cf9aadd..a2ce7e1 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -77,6 +77,10 @@ type Worker struct { // path. Read per run so a profile/task toggle takes effect without rebuilding // the agent. nil = streaming (default). nonStreamingFn func() bool + // noaEnabledFn resolves whether this run uses the experimental noa context- + // compression mechanism. Read per run, like nonStreaming. nil = off (built-in + // compaction). + noaEnabledFn func() bool // maxTokensFn resolves the per-reply output cap in tokens, on the same // per-run basis. nil or 0 = send no cap and let the endpoint decide. maxTokensFn func() int @@ -113,6 +117,11 @@ func (w *Worker) SetNonStreaming(fn func() bool) { w.nonStreamingFn = fn } func (w *Worker) nonStreaming() bool { return w.nonStreamingFn != nil && w.nonStreamingFn() } +// SetNoaEnabled wires a resolver deciding whether runs use the experimental noa +// context-compression mechanism. nil/unset = off (built-in compaction). Read per +// run so the settings toggle takes effect without rebuilding the agent. +func (w *Worker) SetNoaEnabled(fn func() bool) { w.noaEnabledFn = fn } + // SetMaxTokens wires a resolver for the per-reply output cap. nil/unset or 0 = // send no cap and let the endpoint decide. Read per run, like nonStreaming. func (w *Worker) SetMaxTokens(fn func() int) { w.maxTokensFn = fn } @@ -478,6 +487,9 @@ func (w *Worker) execute(ctx context.Context, name string, taskID int64, as *db. input = "开始执行 system 里领到的意图:只做它、只产生事实、assets、finding、做完即停。" } + // 实验功能:开启后由 noa 接管上下文压缩(归档落在本意图 runDir 下,任务级持久)。 + noaSession := WorkerSessionID(ts.ID(), intent.ID) + enableNoa(&opts, w.noaEnabledFn, runDir, noaSession, noaWarn(noaSession)) ctx = attachSideCapture(ctx, &opts) s := agentcore.NewSession(opts) defer s.Close() // release the session's background-task manager (temp dir + processes) diff --git a/server/manager.go b/server/manager.go index fc3542e..2ee833a 100644 --- a/server/manager.go +++ b/server/manager.go @@ -271,6 +271,10 @@ const ( // 任务并发上限:开关 + 上限数。默认关闭;开启后默认上限 5(见 defaultConcurrencyLimit)。 settingConcurrencyOn = "task_concurrency_enabled" settingConcurrencyLimit = "task_concurrency_limit" + // 实验功能:noa 模型驱动上下文压缩(norma v0.4.0)。默认关闭——开启后平台接入的四类 + // agent(planner/worker/主 agent/对话)由 noa 接管上下文压缩,取代内置 compaction。 + // 每 run 读一次,切换只影响之后启动的 run。 + settingNoaCompaction = "noa_compaction" // defaultWebSearchBackend is used when web search is on but no backend was picked. defaultWebSearchBackend = "ddgs" // deepSeekWebSearchBackend borrows the active LLM profile instead of its own @@ -488,6 +492,19 @@ func (m *Manager) SetLLMRecordEnabled(on bool) error { return nil } +// NoaCompactionEnabled reports whether the experimental noa context-compression +// mechanism is on (默认关;settings.noa_compaction). Read per agent run via the +// injected resolver, so a toggle takes effect on the next run without rebuild. +func (m *Manager) NoaCompactionEnabled() bool { + return m.pg.GetBool(settingNoaCompaction, false) +} + +// SetNoaCompaction persists the noa toggle. Effective on the next agent run — +// the resolver reads it per run, so no rebuild is needed. +func (m *Manager) SetNoaCompaction(on bool) error { + return m.pg.SetBool(settingNoaCompaction, on) +} + // LLMPoolEnabled reports whether LLM failover ("轮询") is on (默认关; // settings.llm_pool_enabled). Read when the provider chain is built (applyLLM), // so a change requires a rebuild — putSettings does that. diff --git a/server/server.go b/server/server.go index 16a978f..e04bcac 100644 --- a/server/server.go +++ b/server/server.go @@ -427,6 +427,7 @@ func (s *Server) buildPlannerWorker(pinID *int64, gProv llm.Provider, gCfg agent wk.SetConstraintInject(s.constraintInjectWorker) // 操作约束注入 worker(可配置,默认开;每轮读) wk.SetNonStreaming(nonStreamingResolver(wCfg)) // 该 profile 选非流式时走 Provider.Complete wk.SetMaxTokens(maxTokensResolver(wCfg)) // 单次回复输出上限(0 = 不发) + wk.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(每 run 读) pProv, pCfg := s.providerForAgent("planner", pinID, gProv, gCfg) pl := agent.NewPlanner(pProv, pCfg.Model, s.m.dir, tx, pCfg.CompactionWindow(), s.agentMaxTurns("planner")) pl.SetFindingRecorder(s.evidenceStore()) @@ -437,6 +438,7 @@ func (s *Server) buildPlannerWorker(pinID *int64, gProv llm.Provider, gCfg agent pl.SetConstraintInject(s.constraintInjectPlanner) // 操作约束注入 planner(可配置,默认开;每轮读) pl.SetNonStreaming(nonStreamingResolver(pCfg)) // 该 profile 选非流式时走 Provider.Complete pl.SetMaxTokens(maxTokensResolver(pCfg)) // 单次回复输出上限(0 = 不发) + pl.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(每 run 读) // cold-digest §7: background cold-node compaction, on the planner's provider/ // model (§4 uses the running agent's model). Uses Complete (non-streaming) for // the one-shot body summarization. @@ -508,6 +510,7 @@ func (s *Server) applyLLM(cfg agent.Config) error { s.mainAgent.SetSteerWork(s.engine.SteerWork) // steer_work:人对运行中 work 实时纠偏 s.mainAgent.SetNonStreaming(nonStreamingResolver(mCfg)) s.mainAgent.SetMaxTokens(maxTokensResolver(mCfg)) + s.mainAgent.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(每 run 读) // chat agent serves MANY custom agents by key → it holds the GLOBAL opts // (backend/key) and gates Enabled per-conversation-agent at Chat time. 对话始终用激活配置。 s.chatAgent = agent.NewChatAgent(prov, cfg.Model, s.m.dir, tx, win) // chat page runner @@ -516,6 +519,7 @@ func (s *Server) applyLLM(cfg agent.Config) error { s.chatAgent.SetGuard(s.chatGuard()) s.chatAgent.SetNonStreaming(nonStreamingResolver(cfg)) s.chatAgent.SetMaxTokens(maxTokensResolver(cfg)) + s.chatAgent.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(每 run 读) s.llmProv = prov s.llmCfg = cfg s.llmOn = true @@ -699,6 +703,7 @@ func (s *Server) chatAgentForProfile(id int64) *agent.ChatAgent { ca.SetGuard(s.chatGuard()) ca.SetNonStreaming(nonStreamingResolver(cfg)) ca.SetMaxTokens(maxTokensResolver(cfg)) + ca.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(每 run 读) s.profMu.Lock() if ex := s.profChatAgents[id]; ex != nil { // lost the race → keep the winner ca = ex @@ -3210,6 +3215,9 @@ func (s *Server) settingsPayload() map[string]any { // 操作约束注入范围(默认都开):把本任务的 allow/deny 约束拼进对应 agent 的系统提示。 "constraints_inject_planner": s.constraintInjectPlanner(), "constraints_inject_worker": s.constraintInjectWorker(), + // 实验功能:noa 模型驱动上下文压缩(默认关)。开启后平台接入的四类 agent 由 noa + // 接管上下文压缩,取代内置 compaction;每 run 读一次,对之后启动的 run 生效。 + "noa_compaction": s.m.NoaCompactionEnabled(), } } @@ -3255,6 +3263,8 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) { // 操作约束注入范围开关(默认都开);即时生效(planner/worker 每轮读),无需重建 agent。 ConstraintsInjectPlanner *bool `json:"constraints_inject_planner"` ConstraintsInjectWorker *bool `json:"constraints_inject_worker"` + // 实验功能:noa 上下文压缩开关(默认关);每 run 读,对之后启动的 run 生效,无需重建 agent。 + NoaCompaction *bool `json:"noa_compaction"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeErr(w, 400, err.Error()) @@ -3272,6 +3282,13 @@ func (s *Server) putSettings(w http.ResponseWriter, r *http.Request) { return } } + if req.NoaCompaction != nil { + // 每 run 读的解析器,切换即时对之后启动的 run 生效,无需 applyLLM 重建。 + if err := s.m.SetNoaCompaction(*req.NoaCompaction); err != nil { + writeErr(w, 500, err.Error()) + return + } + } if req.Workers != nil { if err := s.m.SetWorkers(*req.Workers); err != nil { writeErr(w, 400, err.Error()) diff --git a/server/task_llm.go b/server/task_llm.go index 6e79935..f538cbb 100644 --- a/server/task_llm.go +++ b/server/task_llm.go @@ -566,6 +566,7 @@ func (s *Server) agentsForTask(t *Task) *taskAgentBundle { wk.SetCompactionWindowResolver(workerRuntime.CompactionWindow) wk.SetNonStreaming(workerRuntime.nonStreaming) // 按任务当前激活 profile 的流式开关(每轮读) wk.SetMaxTokens(workerRuntime.maxTokens) // 同上,输出上限也跟随当前激活 profile + wk.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(平台级开关,每 run 读) wk.SetRunTimeout(time.Duration(s.agentRunSeconds("worker")) * time.Second) wk.SetProxy(s.m.ProxyAddr(), s.m.ProxyCACert()) wk.SetWebSearch(s.webSearchFor("worker")) @@ -575,6 +576,7 @@ func (s *Server) agentsForTask(t *Task) *taskAgentBundle { pl.SetCompactionWindowResolver(plannerRuntime.CompactionWindow) pl.SetNonStreaming(plannerRuntime.nonStreaming) pl.SetMaxTokens(plannerRuntime.maxTokens) + pl.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(平台级开关,每 run 读) pl.SetKillWork(s.engine.KillWork) pl.SetSteerWork(s.engine.SteerWork) pl.SetProxy(s.m.ProxyAddr(), s.m.ProxyCACert()) @@ -585,6 +587,7 @@ func (s *Server) agentsForTask(t *Task) *taskAgentBundle { main.SetCompactionWindowResolver(mainRuntime.CompactionWindow) main.SetNonStreaming(mainRuntime.nonStreaming) main.SetMaxTokens(mainRuntime.maxTokens) + main.SetNoaEnabled(s.m.NoaCompactionEnabled) // 实验功能:noa 上下文压缩(平台级开关,每 run 读) main.SetProxy(s.m.ProxyAddr(), s.m.ProxyCACert()) main.SetWebSearch(s.webSearchFor("mainagent")) main.SetSteerWork(s.engine.SteerWork) // steer_work:人对运行中 work 实时纠偏 diff --git a/web/src/app/(main)/system/settings/page.tsx b/web/src/app/(main)/system/settings/page.tsx index ab5659a..2ef42ba 100644 --- a/web/src/app/(main)/system/settings/page.tsx +++ b/web/src/app/(main)/system/settings/page.tsx @@ -2,7 +2,7 @@ import * as React from "react"; -import { CpuIcon, KeyboardIcon, RadioTowerIcon, SearchIcon, ShieldAlertIcon } from "lucide-react"; +import { CpuIcon, FlaskConicalIcon, KeyboardIcon, RadioTowerIcon, SearchIcon, ShieldAlertIcon } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -41,6 +41,8 @@ export default function SystemSettingsPage() { // 操作约束注入范围(默认都开)。 const [injectPlanner, setInjectPlanner] = React.useState(true); const [injectWorker, setInjectWorker] = React.useState(true); + // 实验功能:noa 上下文压缩(默认关)。 + const [noaCompaction, setNoaCompaction] = React.useState(false); // 纯前端偏好:不走 /api/settings,直接读写 localStorage。 const sendMode = useChatSendMode(); @@ -57,6 +59,7 @@ export default function SystemSettingsPage() { setWorkers(String(s.workers ?? 3)); setInjectPlanner(s.constraints_inject_planner !== false); setInjectWorker(s.constraints_inject_worker !== false); + setNoaCompaction(!!s.noa_compaction); }, []); const saveWorkers = () => { @@ -146,6 +149,20 @@ export default function SystemSettingsPage() { .catch(() => setInjectWorker(!v)); // revert on failure }; + const toggleNoaCompaction = (v: boolean) => { + setNoaCompaction(v); // optimistic + api + .setSettings({ noa_compaction: v }) + .then((s) => { + apply(s); + toast.success(v ? "已开启 noa 上下文压缩(对之后启动的运行生效)" : "已关闭 noa 上下文压缩(恢复内置压缩)"); + }) + .catch((e) => { + setNoaCompaction(!v); // revert on failure + toast.error(`保存失败:${(e as Error).message}`); + }); + }; + // Persist a web-search patch (enable and/or backend). Optimistic with refetch. const saveWebSearch = (patch: Partial) => { setSaving(true); @@ -387,6 +404,34 @@ export default function SystemSettingsPage() { + + + + + 实验功能 + + + 尚在验证中的机制,默认关闭。可能改变 Agent 行为或影响稳定性,请在了解影响后启用。 +
+ noa 上下文压缩:由模型主动压缩长对话历史(norma v0.4.0)。开启后平台接入的四类 Agent( + 规划者 / 执行者 / 主 Agent / 对话)改用 noa 接管上下文,取代内置压缩, + 压缩原文会归档到任务工作目录下便于回溯。切换即时生效(对之后启动的运行生效),无需重建 Agent; + 关闭后立即恢复内置压缩。 +
+
+ + + + +
+ diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts index 7195f29..9332261 100644 --- a/web/src/lib/mock/data.ts +++ b/web/src/lib/mock/data.ts @@ -3326,6 +3326,7 @@ export const settings: Settings = { workers: 3, llm_pool_enabled: true, llm_pool_bind_fallback: false, + noa_compaction: false, }; // ── LLM 轮询(故障转移)────────────────────────────────────────────────────── diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 2a0963c..4d609cc 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -913,6 +913,10 @@ export interface Settings { // 操作约束注入范围(默认都开):把任务的 allow/deny 约束拼进对应 agent 的系统提示。 constraints_inject_planner?: boolean; constraints_inject_worker?: boolean; + // 实验功能:noa 模型驱动上下文压缩(默认关)。开启后平台接入的四类 agent(planner/ + // worker/主 agent/对话)由 noa 接管上下文压缩,取代内置 compaction;每 run 读一次,对 + // 之后启动的 run 生效。 + noa_compaction?: boolean; } // ---- LLM config ----