feat(config): add model context window limits
This commit is contained in:
parent
2d7bd090fc
commit
93e74120ac
@ -94,7 +94,7 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del
|
||||
- **Scheduler** supports legacy direct-delivery jobs and managed `task`/`monitor` jobs; managed agents cannot call `send_message`, and `on_alert` suppresses only healthy informational results
|
||||
- **AgentLoop** is stateless across turns; it receives prepared history, drains same-Turn steering only at safe model boundaries, calls LLM providers, executes tools, and returns one result
|
||||
- **Context overflow recovery** is type-driven: before tool progress Session may commit one checkpoint and retry once; after any tool batch AgentLoop may retry the current Provider step once from its in-memory transcript, preserving current tool calls/results, and Session must never restart that Turn from durable history
|
||||
- **Context compaction** keeps `messages` append-only and uses one active checkpoint per Session (`summary + first_retained_seq`) for deterministic Provider projection; `/compact`, Turn-boundary auto compaction, and overflow share the same compactor/CAS commit path, Session restoration never derives context from Timeline or calls a Provider, summary input is bounded from the current model `token_limit` rather than a fixed cap, and the only automatic threshold is `context_tokens > context_window - effective_reserve`
|
||||
- **Context compaction** keeps `messages` append-only and uses one active checkpoint per Session (`summary + first_retained_seq`) for deterministic Provider projection; `/compact`, Turn-boundary auto compaction, and overflow share the same compactor/CAS commit path, Session restoration never derives context from Timeline or calls a Provider, the Model `token_limit` (default 128K) is the hard window ceiling and an optional Agent `token_limit` can only narrow it via `min(agent, model)`, summary input is bounded from that effective window rather than a fixed cap, and the only automatic threshold is `context_tokens > context_window - effective_reserve`
|
||||
- **AgentCatalog** is immutable per runtime generation; candidate preparation strictly validates trusted Markdown definitions, Provider profiles, tool/Skill allowlists, and delegation edges before activation. A definition that fails per-file validation (bad YAML, unknown provider/profile/model/tool/skill, or an explicit delegate edge to an absent target) is disabled for that generation only and reported via `load_errors` (exposed by `GET /api/agents`), never blocking startup or reload; config- and directory-trust-level failures remain fatal. Sub-Agent orchestration is an intrinsic, always-on mechanism (no feature switch). Named Agents support foreground (single/batch) and Root-initiated single background execution with durable run/inbox delivery; a built-in general-purpose definition is released to `~/.picobot/agents/` on first run. Background batches and nested background remain restricted
|
||||
- **WebUI management APIs** only expose allowlisted config/profile/log/storage operations; keep response limits, secret redaction, atomic config writes, and profile path allowlists intact
|
||||
- **WebUI styling** uses the local Fluent 2 semantic tokens in `webui/src/styles.css`; page and component styles should consume the aliases instead of introducing independent hard-coded palettes, and must preserve selectable light/dark and brand-color themes, keyboard focus, responsive layout, and reduced-motion behavior. Browser-only appearance settings belong in `lib/theme.js`/`localStorage`, and `public/theme-init.js` must restore them before Svelte mounts
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "picobot"
|
||||
version = "1.19.0"
|
||||
version = "1.20.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -61,6 +61,7 @@ Gateway 首次启动时会把模板释放到 `~/.picobot/config.example.json`。
|
||||
"model_id": "gpt-4o",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 4096,
|
||||
"token_limit": 128000,
|
||||
"input_type": ["text", "image"]
|
||||
}
|
||||
},
|
||||
@ -68,8 +69,7 @@ Gateway 首次启动时会把模板释放到 `~/.picobot/config.example.json`。
|
||||
"default": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"max_tool_iterations": 99,
|
||||
"token_limit": 128000
|
||||
"max_tool_iterations": 99
|
||||
}
|
||||
},
|
||||
"workspace_dir": "~/.picobot/workspace"
|
||||
@ -328,7 +328,7 @@ PicoBot 有两类记忆:
|
||||
|
||||
每轮处理用户消息时,MemoryManager 会按用户输入召回 Knowledge,并作为运行时上下文附加到本轮用户消息。当前召回上限固定为 5;`memory.recall_limit` 已支持解析但尚未接入 worker。长会话使用一个活动 checkpoint:累计摘要加 `first_retained_seq` 之后的原始消息尾部构成模型上下文,原始消息、工具调用结果、ID 和 seq 均不会被压缩改写。旧工具结果会保留在原始历史中,但 checkpoint 边界推进后不再永久占用 Provider 上下文。成功的语义摘要还会 best-effort 保存为 Timeline,供 `timeline_recall` 检索;Timeline 不参与会话恢复正确性。Scheduler 默认创建一个每日维护巡检,按 `memory.timeline_retention_days` 清理过期 Timeline;Knowledge 不会被自动删除。
|
||||
|
||||
自动压缩使用保留量阈值 `context_tokens > context_window - effective_reserve`,默认 reserve 为 16,384 tokens,并尽量原样保留最近 20,000 tokens。小窗口会自动把 reserve 限制为窗口的一半、把近期保留量限制为有效阈值的一半。摘要请求不使用固定 32K 输入上限,而是按当前模型的 `token_limit` 扣除摘要输出、提示词和安全余量;超大历史只在摘要请求副本中按“已有 checkpoint + 最新消息优先”生成有界 head/tail 转录,SQLite 原文不变。手动 `/compact` 跳过自动阈值;换成小模型后若发送前预检已发现硬超限,或首次请求返回真实 context overflow,语义摘要不可用时才使用明确标记的确定性降级裁剪,正式请求最多重试一次。若 overflow 发生在工具已经执行之后,AgentLoop 只在当前内存转录上裁掉旧完整 Turn 并重试当前模型步骤一次,不会从数据库历史重跑工具。
|
||||
模型的 `models.<name>.token_limit` 给出上下文窗口上限,未配置时默认为 128,000;Agent 的 `agents.<name>.token_limit` 是可选的收紧上限,两者都有配置时有效窗口取二者最小值,因此 Agent 不能扩大模型窗口。自动压缩使用保留量阈值 `context_tokens > context_window - effective_reserve`,默认 reserve 为 16,384 tokens,并尽量原样保留最近 20,000 tokens。小窗口会自动把 reserve 限制为窗口的一半、把近期保留量限制为有效阈值的一半。摘要请求不使用固定 32K 输入上限,而是按有效窗口扣除摘要输出、提示词和安全余量;超大历史只在摘要请求副本中按“已有 checkpoint + 最新消息优先”生成有界 head/tail 转录,SQLite 原文不变。手动 `/compact` 跳过自动阈值;换成小模型后若发送前预检已发现硬超限,或首次请求返回真实 context overflow,语义摘要不可用时才使用明确标记的确定性降级裁剪,正式请求最多重试一次。若 overflow 发生在工具已经执行之后,AgentLoop 只在当前内存转录上裁掉旧完整 Turn 并重试当前模型步骤一次,不会从数据库历史重跑工具。
|
||||
|
||||
### 工具
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"model_id": "qwen-plus",
|
||||
"temperature": 0.0,
|
||||
"max_tokens": 100,
|
||||
"token_limit": 128000,
|
||||
"input_type": ["text"]
|
||||
}
|
||||
},
|
||||
@ -19,8 +20,7 @@
|
||||
"default": {
|
||||
"provider": "aliyun",
|
||||
"model": "qwen-plus",
|
||||
"max_tool_iterations": 20,
|
||||
"token_limit": 128000
|
||||
"max_tool_iterations": 20
|
||||
}
|
||||
},
|
||||
"agent_orchestration": {
|
||||
|
||||
@ -204,7 +204,7 @@ Session ID 格式为:
|
||||
|
||||
当前 WebUI/TUI Turn 通过 `send_message(files=...)` 向自身 session 投递文件时,文件先进入 task-local Turn delivery 暂存区,成功结束后附加到最终 assistant 消息,与工具链一起原子提交;因此持久化和刷新后的顺序都是工具调用/结果在前、携带附件的最终回复在后,也不会生成带 `[message from ...]` 的自投递气泡。其他同 Turn 自投递仍是受控例外:只有 task-local Turn ID 仍匹配该 session 的 active Turn,写入才允许不递增 `state_version`。跨 Turn、跨 session 以及无法证明所有权的写入仍必须递增版本。Provider 回放历史附件时,只有 user 输入和当前工具结果可生成模型原生媒体块;assistant/system 附件只回放文本清单,避免把图片放到供应商不接受的角色。
|
||||
|
||||
SessionManager 负责组装会话上下文:系统提示、Skills、召回的 Knowledge、可选的 active plan 摘要,以及由活动 `ContextCheckpoint` 投影出的会话历史。`messages` 始终是 append-only 原始日志;每个 Session 最多有一个活动 checkpoint,模型历史确定为“一条累计摘要 + `seq >= first_retained_seq` 的原始尾部”,Timeline 只是 checkpoint 提交后的 best-effort 检索副本,恢复流程不读取 Timeline、也不调用 Provider。`session::turn_input` 只在 Session 锁外并行读取 Knowledge 和 active plan;完整请求草稿随后使用同一个 reserve 预算评估,自动触发公式唯一为 `context_tokens > context_window - effective_reserve`。手动、自动和首次 context-overflow 共用唯一的 `compact_session_context` 编排与 checkpoint CAS 提交路径;candidate 自带快照 generation,提交成功后必须从当前 raw log 重新投影,不能返回摘要前的旧尾部向量。摘要请求输入预算由当前模型 `token_limit` 扣除动态摘要输出、固定提示词和安全余量得到;超大压缩源只在 request-local 副本中保留已有 checkpoint 和最新材料、对单条内容做确定性 head/tail 截取,不能用固定 32K 上限拒绝压缩,也不能改写 durable raw log。每个 Turn 最多进行一次语义摘要;换模后的发送前预检若已硬超限,可在摘要失败时直接生成明确标记的确定性降级 checkpoint,避免先发送必然失败的普通请求;首次 Provider 请求 overflow 最多正式重试一次。AgentLoop 把 Provider overflow 转成类型化错误:工具尚未执行时交回 Session;工具已执行后只在同一个 AgentLoop 中删除旧完整 Turn 的请求副本并重试当前 Provider step 一次,保留本 Turn tool call/result,绝不从 durable history 重启并重复副作用工具。首次请求与重试必须复用同一个 runtime assembly,不能复制系统提示、丢失 mailbox steering 或把压缩投影写回原始历史。Provider prompt usage 仅在 provider/model/checkpoint generation/raw seq 和完整请求摘要都匹配时复用,否则完整保守估算;不再按消息数外推。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态,但通过本 Turn 的 mailbox 在安全边界接收追加用户输入。执行工具时额外传递只包含 session/turn 身份的 `ToolExecutionContext`;无状态工具使用默认实现忽略它,有状态外部适配器用它路由资源,但可按明确的单用户配置跨 dialog 共享,且不能自行反向查询 SessionManager。完整压缩设计见 [CONTEXT_COMPACTION_DESIGN.md](CONTEXT_COMPACTION_DESIGN.md)。
|
||||
SessionManager 负责组装会话上下文:系统提示、Skills、召回的 Knowledge、可选的 active plan 摘要,以及由活动 `ContextCheckpoint` 投影出的会话历史。`messages` 始终是 append-only 原始日志;每个 Session 最多有一个活动 checkpoint,模型历史确定为“一条累计摘要 + `seq >= first_retained_seq` 的原始尾部”,Timeline 只是 checkpoint 提交后的 best-effort 检索副本,恢复流程不读取 Timeline、也不调用 Provider。所选 Model 的可选 `token_limit` 定义上下文硬上限,缺失时固定回退 128000;Agent 的可选 `token_limit` 只允许收紧该上限,有效窗口为 `min(agent_token_limit, model_token_limit_or_128K)`,Agent 不能扩大模型窗口。主 Agent Profile、具名 Agent 的内联 Provider/Model 和 `llm_profile` 引用必须使用同一规则,definition 上的显式值也只能收紧已解析 Profile。`session::turn_input` 只在 Session 锁外并行读取 Knowledge 和 active plan;完整请求草稿随后使用同一个 reserve 预算评估,自动触发公式唯一为 `context_tokens > context_window - effective_reserve`。手动、自动和首次 context-overflow 共用唯一的 `compact_session_context` 编排与 checkpoint CAS 提交路径;candidate 自带快照 generation,提交成功后必须从当前 raw log 重新投影,不能返回摘要前的旧尾部向量。摘要请求输入预算由有效 `token_limit` 扣除动态摘要输出、固定提示词和安全余量得到;超大压缩源只在 request-local 副本中保留已有 checkpoint 和最新材料、对单条内容做确定性 head/tail 截取,不能用固定 32K 上限拒绝压缩,也不能改写 durable raw log。每个 Turn 最多进行一次语义摘要;换模后的发送前预检若已硬超限,可在摘要失败时直接生成明确标记的确定性降级 checkpoint,避免先发送必然失败的普通请求;首次 Provider 请求 overflow 最多正式重试一次。AgentLoop 把 Provider overflow 转成类型化错误:工具尚未执行时交回 Session;工具已执行后只在同一个 AgentLoop 中删除旧完整 Turn 的请求副本并重试当前 Provider step 一次,保留本 Turn tool call/result,绝不从 durable history 重启并重复副作用工具。首次请求与重试必须复用同一个 runtime assembly,不能复制系统提示、丢失 mailbox steering 或把压缩投影写回原始历史。Provider prompt usage 仅在 provider/model/checkpoint generation/raw seq 和完整请求摘要都匹配时复用,否则完整保守估算;不再按消息数外推。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态,但通过本 Turn 的 mailbox 在安全边界接收追加用户输入。执行工具时额外传递只包含 session/turn 身份的 `ToolExecutionContext`;无状态工具使用默认实现忽略它,有状态外部适配器用它路由资源,但可按明确的单用户配置跨 dialog 共享,且不能自行反向查询 SessionManager。完整压缩设计见 [CONTEXT_COMPACTION_DESIGN.md](CONTEXT_COMPACTION_DESIGN.md)。
|
||||
|
||||
当前 Turn 的工具进度只从 `AgentLoop` 的结构化 `TurnEvent` 进入 `TurnController`,不能另建字符串 notification 通道重复投递。具名子 Agent 基础已接入:候选运行代从受信任配置目录严格加载不可变 `AgentCatalog`,Definition 固定 Provider/Model(内联或 `llm_profile`)、工具/Skill allowlist、委托边和执行限制,工具集完全由定义文件决定;单个定义校验失败(坏 YAML、未知 provider/profile/model/tool/skill、或显式委托到缺失目标)仅停用该定义并记入 `load_errors`(`GET /api/agents` 返回),不会阻塞启动或热重载,配置与目录信任级错误仍然致命;`delegate` 使用 `foreground/background` 两个 canonical 生命周期词,批量并发与生命周期正交。具名 foreground 支持批量并发、显式 `AgentExecutionContext`、祖先环路和委托边校验;Root 对具名 Agent 的 background(单任务或批量,批量并发、每个 run 独立 completion 事件)走 durable run/inbox + continuation 投递,空闲时完成即返回。内置 general-purpose 定义随二进制释放,WebUI「子 Agent」页可增删改与启停定义。旧匿名 general 兼容路径已移除。自动标题属于非关键派生工作:Turn 持久化完成后由 `TaskSupervisor` 调度,Session worker 不等待模型生成;同一 Session 同时最多有一个标题任务,提交时仍校验标题保持默认值,避免覆盖用户改名。
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# PicoBot 上下文压缩架构设计与实施方案
|
||||
|
||||
> 状态:已于 PicoBot 1.19.0 实施;2026-08-20 完成实现符合性修正
|
||||
> 状态:已于 PicoBot 1.19.0 实施;1.20.0 增加 Model 级窗口配置与 Agent 上限收紧
|
||||
> 编写日期:2026-08-20
|
||||
> 本文定义并记录 PicoBot 上下文压缩的架构、数据模型、触发规则、失败语义、迁移步骤和验收标准。运行时事实仍以代码和测试为准;第 2 节保留 1.19.0 之前的基线,便于解释迁移动机。
|
||||
|
||||
@ -529,7 +529,7 @@ summary_source_budget = context_window
|
||||
- summary_safety
|
||||
```
|
||||
|
||||
`context_window` 来自当前 Agent 配置的 `token_limit`;Provider 在 overflow 错误中返回更小真实窗口时使用校准后的值。摘要源超过 `summary_source_budget` 时不能拒绝压缩,也不做多轮/递归摘要,而是只对发送给摘要 Provider 的副本执行有界序列化:已有 checkpoint 优先保留一部分,再从待压缩前缀的最新消息向前选择;装不下的单条记录保留 head/tail,并记录源消息数、省略数、截短数和策略。原始数据库历史保持不变。
|
||||
`context_window` 使用当前 Agent 的有效窗口:Model 的 `token_limit` 缺失时先回退 128000,再与可选 Agent `token_limit` 取最小值;Agent 因而只能收紧、不能扩大模型窗口。Provider 在 overflow 错误中返回更小真实窗口时使用校准后的值。摘要源超过 `summary_source_budget` 时不能拒绝压缩,也不做多轮/递归摘要,而是只对发送给摘要 Provider 的副本执行有界序列化:已有 checkpoint 优先保留一部分,再从待压缩前缀的最新消息向前选择;装不下的单条记录保留 head/tail,并记录源消息数、省略数、截短数和策略。原始数据库历史保持不变。
|
||||
|
||||
若窗口小到连最小摘要提示和材料都无法容纳,语义摘要视为失败:手动入口保留旧 checkpoint 并返回错误;自动入口在完整请求仍低于硬窗口时保留旧投影,已经硬超限时直接进入确定性 overflow 降级;overflow 入口同样进入确定性降级。
|
||||
|
||||
@ -990,6 +990,7 @@ cargo build
|
||||
11. 普通压缩失败不会无声丢弃历史;只有 overflow 使用明确标记的整 Turn 降级。
|
||||
12. 运行时不存在第二套按 70%/90% 比例判断的旧压缩路径。
|
||||
13. 摘要输入上限从当前模型窗口推导,超大历史通过 request-local 有界转录进入一次摘要调用,不因固定上限拒绝压缩。
|
||||
14. 主 Agent 与具名 Agent 都先以 Model 配置或 128K 默认值确定硬上限,再与可选 Agent 上限取最小值。
|
||||
|
||||
## 22. 最终取舍
|
||||
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
"qwen-plus": {
|
||||
"model_id": "qwen-plus",
|
||||
"temperature": 0.0,
|
||||
"max_tokens": 8192
|
||||
"max_tokens": 8192,
|
||||
"token_limit": 128000
|
||||
},
|
||||
"gpt-4o": {
|
||||
"model_id": "gpt-4o",
|
||||
@ -42,8 +43,7 @@
|
||||
"default": {
|
||||
"provider": "aliyun",
|
||||
"model": "qwen-plus",
|
||||
"max_tool_iterations": 99,
|
||||
"token_limit": 128000
|
||||
"max_tool_iterations": 99
|
||||
}
|
||||
},
|
||||
"context_compaction": {
|
||||
|
||||
@ -77,7 +77,7 @@ Scheduler → SessionManager.handle_cron_message → AgentLoop → send_message
|
||||
|
||||
## 上下文压缩
|
||||
|
||||
`messages` 是 append-only 原始日志,压缩不会改写消息、工具结果、ID 或 seq。每个 session 最多有一个活动 `ContextCheckpoint`;Provider 历史确定为“一条累计摘要 + `seq >= first_retained_seq` 的原始尾部”。自动压缩使用 `context_tokens > context_window - effective_reserve`,默认 reserve 16,384、近期原样保留 20,000 tokens;小窗口会自适应缩小两者。摘要输入预算由当前模型 `token_limit` 扣除摘要输出、提示词和安全余量得到;超大历史生成 checkpoint 加最新材料优先的有界 request-local 转录,不受固定 32K 上限约束。`/compact`、自动入口和首次 overflow 复用同一压缩编排和 checkpoint 原子提交路径,每次最多一次摘要调用。真实 Provider overflow 或换模后发送前已检测到的硬超限,能在摘要不可用时生成明确标记的确定性降级 checkpoint,并且正式 Provider 请求只重试一次。工具已经执行后若发生 overflow,只在同一个 AgentLoop 中保留本 Turn 工具链、裁掉旧完整 Turn 的请求副本并重试当前模型步骤一次,不从 durable history 重跑工具。
|
||||
`messages` 是 append-only 原始日志,压缩不会改写消息、工具结果、ID 或 seq。每个 session 最多有一个活动 `ContextCheckpoint`;Provider 历史确定为“一条累计摘要 + `seq >= first_retained_seq` 的原始尾部”。Model `token_limit` 是上下文硬上限,缺失时默认 128K;可选 Agent `token_limit` 只能收紧它,有效窗口取二者最小值。自动压缩使用 `context_tokens > context_window - effective_reserve`,默认 reserve 16,384、近期原样保留 20,000 tokens;小窗口会自适应缩小两者。摘要输入预算由有效窗口扣除摘要输出、提示词和安全余量得到;超大历史生成 checkpoint 加最新材料优先的有界 request-local 转录,不受固定 32K 上限约束。`/compact`、自动入口和首次 overflow 复用同一压缩编排和 checkpoint 原子提交路径,每次最多一次摘要调用。真实 Provider overflow 或换模后发送前已检测到的硬超限,能在摘要不可用时生成明确标记的确定性降级 checkpoint,并且正式 Provider 请求只重试一次。工具已经执行后若发生 overflow,只在同一个 AgentLoop 中保留本 Turn 工具链、裁掉旧完整 Turn 的请求副本并重试当前模型步骤一次,不从 durable history 重跑工具。
|
||||
|
||||
## Skill 系统
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ Gateway WebUI 的“配置”页可以编辑实际加载的配置文件。读取
|
||||
| `model_id` | 模型标识名称 |
|
||||
| `temperature` | 采样温度,可选 |
|
||||
| `max_tokens` | 最大输出 token 数,可选 |
|
||||
| `token_limit` | 模型上下文窗口硬上限,可选;未配置时默认为 128000,Agent 只能进一步收紧 |
|
||||
| `input_type` | 模型支持的输入类型,如 `["text"]` 或 `["text", "image"]`,默认 `["text"]`. 纯内部使用,不会传递给 LLM API |
|
||||
|
||||
## agents 字段
|
||||
@ -51,7 +52,7 @@ Gateway WebUI 的“配置”页可以编辑实际加载的配置文件。读取
|
||||
| `provider` | string | - | 提供商名称(对应 providers key) |
|
||||
| `model` | string | - | 模型名称(对应 models key) |
|
||||
| `max_tool_iterations` | int | 99 | 最大工具调用轮数 |
|
||||
| `token_limit` | int | 128000 | 上下文 token 限制 |
|
||||
| `token_limit` | int | 使用模型上限 | 可选的 Agent 上限;有效窗口取 Agent 与模型(模型未配置时为 128000)的最小值 |
|
||||
|
||||
## context_compaction 字段
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ Skill 安装后默认启用,可在 WebUI「工具 → Skills」页用开关禁
|
||||
|
||||
## Q: 上下文压缩是什么意思?
|
||||
|
||||
对话接近模型 token 限制时,PicoBot 用一份累计 checkpoint 摘要替代 Provider 上下文中的旧前缀,并原样保留近期消息尾部。原始消息和工具结果仍永久保存在聊天历史/SQLite 中,只是不再永久占用模型上下文;语义摘要还可通过 `timeline_recall` 检索。自动阈值采用窗口减 reserve 的机制,摘要输入按当前模型窗口动态限制而非固定 32K;换成更小模型后若历史已经超限,会在首次普通模型请求前压缩或明确降级。`/compact` 可在阈值前手动强制执行。
|
||||
对话接近模型 token 限制时,PicoBot 用一份累计 checkpoint 摘要替代 Provider 上下文中的旧前缀,并原样保留近期消息尾部。原始消息和工具结果仍永久保存在聊天历史/SQLite 中,只是不再永久占用模型上下文;语义摘要还可通过 `timeline_recall` 检索。Model 的 `token_limit` 是窗口硬上限,未配置时为 128K;Agent 的可选 `token_limit` 只能收紧它,两者都有时取最小值。自动阈值采用窗口减 reserve 的机制,摘要输入按有效窗口动态限制而非固定 32K;换成更小模型后若历史已经超限,会在首次普通模型请求前压缩或明确降级。`/compact` 可在阈值前手动强制执行。
|
||||
|
||||
## Q: 如何修改 gateway 监听端口?
|
||||
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
"qwen-plus": {
|
||||
"model_id": "qwen-plus",
|
||||
"temperature": 0.0,
|
||||
"max_tokens": 8192
|
||||
"max_tokens": 8192,
|
||||
"token_limit": 128000
|
||||
},
|
||||
"gpt-4o": {
|
||||
"model_id": "gpt-4o",
|
||||
@ -42,8 +43,7 @@
|
||||
"default": {
|
||||
"provider": "aliyun",
|
||||
"model": "qwen-plus",
|
||||
"max_tool_iterations": 99,
|
||||
"token_limit": 128000
|
||||
"max_tool_iterations": 99
|
||||
}
|
||||
},
|
||||
"context_compaction": {
|
||||
|
||||
@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||
|
||||
use crate::config::{
|
||||
AgentOrchestrationConfig, LLMProviderConfig, ModelConfig, ProviderConfig, expand_path,
|
||||
resolve_token_limit,
|
||||
};
|
||||
use crate::skills::SkillsLoader;
|
||||
use crate::tools::ToolRegistry;
|
||||
@ -427,7 +428,7 @@ fn resolve_provider(
|
||||
max_tokens: model.max_tokens,
|
||||
model_extra: model.extra.clone(),
|
||||
max_tool_iterations: spec.max_tool_iterations.unwrap_or(99),
|
||||
token_limit: spec.token_limit.unwrap_or(128_000),
|
||||
token_limit: resolve_token_limit(spec.token_limit, model.token_limit),
|
||||
workspace_dir: workspace_dir.to_path_buf(),
|
||||
input_types: model.input_type.clone(),
|
||||
price_input_per_million: None,
|
||||
@ -435,13 +436,14 @@ fn resolve_provider(
|
||||
});
|
||||
}
|
||||
let profile = spec.llm_profile.as_deref().unwrap_or_default();
|
||||
provider_profiles
|
||||
.get(profile)
|
||||
.cloned()
|
||||
.ok_or_else(|| AgentCatalogError::UnknownProfile {
|
||||
let mut resolved = provider_profiles.get(profile).cloned().ok_or_else(|| {
|
||||
AgentCatalogError::UnknownProfile {
|
||||
agent: spec.id.clone(),
|
||||
profile: profile.to_string(),
|
||||
})
|
||||
}
|
||||
})?;
|
||||
resolved.token_limit = resolve_token_limit(spec.token_limit, Some(resolved.token_limit));
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn validate_definition_tools(
|
||||
@ -639,6 +641,150 @@ mod tests {
|
||||
assert_eq!(catalog.runtime_generation(), 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_agent_context_window_uses_agent_model_minimum_then_default() {
|
||||
let providers = HashMap::from([(
|
||||
"provider".to_string(),
|
||||
ProviderConfig {
|
||||
provider_type: "openai".to_string(),
|
||||
base_url: "https://example.invalid/v1".to_string(),
|
||||
api_key: "test".to_string(),
|
||||
extra_headers: HashMap::new(),
|
||||
},
|
||||
)]);
|
||||
let mut models = HashMap::from([(
|
||||
"model".to_string(),
|
||||
ModelConfig {
|
||||
model_id: "model-id".to_string(),
|
||||
temperature: None,
|
||||
max_tokens: None,
|
||||
token_limit: Some(64_000),
|
||||
input_type: vec!["text".to_string()],
|
||||
extra: HashMap::new(),
|
||||
},
|
||||
)]);
|
||||
let mut spec = ProviderSpec {
|
||||
id: "inline".to_string(),
|
||||
llm_profile: None,
|
||||
provider: Some("provider".to_string()),
|
||||
model: Some("model".to_string()),
|
||||
token_limit: None,
|
||||
max_tool_iterations: None,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&HashMap::new(),
|
||||
&providers,
|
||||
&models,
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
64_000
|
||||
);
|
||||
|
||||
spec.token_limit = Some(32_000);
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&HashMap::new(),
|
||||
&providers,
|
||||
&models,
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
32_000
|
||||
);
|
||||
|
||||
spec.token_limit = Some(128_000);
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&HashMap::new(),
|
||||
&providers,
|
||||
&models,
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
64_000
|
||||
);
|
||||
|
||||
spec.token_limit = None;
|
||||
models.get_mut("model").unwrap().token_limit = None;
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&HashMap::new(),
|
||||
&providers,
|
||||
&models,
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
128_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_agent_context_window_can_only_narrow_resolved_profile() {
|
||||
let profiles = HashMap::from([("profile".to_string(), provider())]);
|
||||
let mut spec = ProviderSpec {
|
||||
id: "profile-agent".to_string(),
|
||||
llm_profile: Some("profile".to_string()),
|
||||
provider: None,
|
||||
model: None,
|
||||
token_limit: None,
|
||||
max_tool_iterations: None,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&profiles,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
4_096
|
||||
);
|
||||
|
||||
spec.token_limit = Some(2_048);
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&profiles,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
2_048
|
||||
);
|
||||
|
||||
spec.token_limit = Some(8_192);
|
||||
assert_eq!(
|
||||
resolve_provider(
|
||||
&spec,
|
||||
&profiles,
|
||||
&HashMap::new(),
|
||||
&HashMap::new(),
|
||||
Path::new("/tmp")
|
||||
)
|
||||
.unwrap()
|
||||
.token_limit,
|
||||
4_096
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegation_semantics_default_empty_any_and_list() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
@ -159,6 +159,9 @@ pub struct ModelConfig {
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(default)]
|
||||
pub max_tokens: Option<u32>,
|
||||
/// Model context-window capacity. Agent profiles may narrow it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub token_limit: Option<usize>,
|
||||
#[serde(default = "default_input_type")]
|
||||
pub input_type: Vec<String>,
|
||||
#[serde(flatten)]
|
||||
@ -175,8 +178,10 @@ pub struct AgentConfig {
|
||||
pub model: String,
|
||||
#[serde(default = "default_max_tool_iterations")]
|
||||
pub max_tool_iterations: usize,
|
||||
#[serde(default = "default_token_limit")]
|
||||
pub token_limit: usize,
|
||||
/// Optional per-Agent context-window cap. It may narrow but never expand
|
||||
/// the selected model's limit; a missing model limit falls back to 128K.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub token_limit: Option<usize>,
|
||||
}
|
||||
|
||||
fn default_max_tool_iterations() -> usize {
|
||||
@ -187,6 +192,17 @@ fn default_token_limit() -> usize {
|
||||
128_000
|
||||
}
|
||||
|
||||
/// Resolve the effective context window. Agent values are optional caps: they
|
||||
/// may narrow a model window but can never expand it. A missing model window
|
||||
/// is the product's conservative 128K default.
|
||||
pub(crate) fn resolve_token_limit(
|
||||
agent_token_limit: Option<usize>,
|
||||
model_token_limit: Option<usize>,
|
||||
) -> usize {
|
||||
let model_token_limit = model_token_limit.unwrap_or_else(default_token_limit);
|
||||
agent_token_limit.map_or(model_token_limit, |agent| agent.min(model_token_limit))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct ContextCompactionConfig {
|
||||
@ -915,7 +931,7 @@ impl Config {
|
||||
max_tokens: model.max_tokens,
|
||||
model_extra: model.extra.clone(),
|
||||
max_tool_iterations: agent.max_tool_iterations,
|
||||
token_limit: agent.token_limit,
|
||||
token_limit: resolve_token_limit(agent.token_limit, model.token_limit),
|
||||
workspace_dir: expand_path(&self.workspace_dir),
|
||||
input_types: model.input_type.clone(),
|
||||
price_input_per_million: None,
|
||||
@ -1193,6 +1209,38 @@ mod tests {
|
||||
assert_eq!(provider_config.name, "aliyun");
|
||||
assert_eq!(provider_config.model_id, "qwen-plus");
|
||||
assert_eq!(provider_config.temperature, Some(0.0));
|
||||
assert_eq!(provider_config.token_limit, 128_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_window_uses_agent_model_minimum_then_128k_default() {
|
||||
let file = write_test_config();
|
||||
let mut config = Config::load(file.path().to_str().unwrap()).unwrap();
|
||||
|
||||
config.models.get_mut("qwen-plus").unwrap().token_limit = Some(64_000);
|
||||
assert_eq!(
|
||||
config.get_provider_config("default").unwrap().token_limit,
|
||||
64_000
|
||||
);
|
||||
|
||||
config.agents.get_mut("default").unwrap().token_limit = Some(32_000);
|
||||
assert_eq!(
|
||||
config.get_provider_config("default").unwrap().token_limit,
|
||||
32_000
|
||||
);
|
||||
|
||||
config.agents.get_mut("default").unwrap().token_limit = Some(128_000);
|
||||
assert_eq!(
|
||||
config.get_provider_config("default").unwrap().token_limit,
|
||||
64_000
|
||||
);
|
||||
|
||||
config.agents.get_mut("default").unwrap().token_limit = None;
|
||||
config.models.get_mut("qwen-plus").unwrap().token_limit = None;
|
||||
assert_eq!(
|
||||
config.get_provider_config("default").unwrap().token_limit,
|
||||
128_000
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -1013,7 +1013,13 @@ pub async fn get_agent_options(
|
||||
.config
|
||||
.models
|
||||
.iter()
|
||||
.map(|(name, model)| json!({ "name": name, "model_id": model.model_id }))
|
||||
.map(|(name, model)| {
|
||||
json!({
|
||||
"name": name,
|
||||
"model_id": model.model_id,
|
||||
"token_limit": model.token_limit,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let registry = state.session_manager.tools();
|
||||
let mut tools: Vec<Value> = registry
|
||||
|
||||
4
webui/package-lock.json
generated
4
webui/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "picobot-webui",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "picobot-webui",
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"dependencies": {
|
||||
"bits-ui": "^2.0.0",
|
||||
"dompurify": "^3.4.12",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "picobot-webui",
|
||||
"private": true,
|
||||
"version": "1.19.0",
|
||||
"version": "1.20.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
||||
@ -338,13 +338,13 @@
|
||||
<label>Model
|
||||
<select bind:value={editing.model}>
|
||||
<option value="">(选择)</option>
|
||||
{#each options.models as m (m.name)}<option value={m.name}>{m.name}</option>{/each}
|
||||
{#each options.models as m (m.name)}<option value={m.name}>{m.name}{m.token_limit ? ` · ${m.token_limit} tokens` : " · 默认 128K"}</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>token_limit
|
||||
<input type="number" bind:value={editing.token_limit} placeholder="128000" />
|
||||
<label>token_limit(可选上限)
|
||||
<input type="number" bind:value={editing.token_limit} placeholder="留空使用模型;不能超过模型上限" />
|
||||
</label>
|
||||
<label>max_tool_iterations
|
||||
<input type="number" bind:value={editing.max_tool_iterations} placeholder="99" />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user