# Context 管理

## 文档维护规范

0. **先改文档，再动代码** - 新功能或重大修改需先完成文档更新、并完成审阅后，再进行代码实现；除非改动较小、不被文档涵盖
1. **文档分层，链接代码** - 重要或复杂设计可以另有详细文档；关键实现需标注代码文件路径；格式：`module/file.py:function_name`
2. **简洁快照，日志分离** - 只记录最重要的、与代码准确对应的或者明确的已完成的设计的信息，避免推测、建议、决策历史、修改日志、大量代码；决策依据或修改日志若有必要，可在 `docs/decisions.md` 另行记录

---

## 概述

Context = 每次 LLM 调用时发送的完整消息列表。管理目标：

1. **注入**：确保模型在正确的时间看到正确的信息（system prompt、plan、skill 策略）
2. **压缩**：在 token 预算内去除冗余，保留核心上下文

两者互相影响：压缩可能清掉已注入的内容，需要重新注入。

---

## Context 构成

```
[system]     System Prompt（角色 + 内置 skills）        ← 静态注入，trace 创建时
[user]       第一条用户消息
[assistant]  ...
[tool]       ...                                         ← 可能包含周期注入的 context
[assistant]  ...
[tool]       ...                                         ← 可能包含动态注入的 skill
[user]       第 N 条用户消息
[assistant]  待模型生成
```

---

## 注入机制

### 1. System Prompt（静态注入）

Trace 创建时构建一次，后续续跑不重复发送。

内容：角色 prompt + 按 preset/call-site 过滤的内置/项目 skills。

优先级：`messages 中的 system message` > `config.system_prompt` > `preset.system_prompt` > `DEFAULT_SYSTEM_PREFIX`

**实现**：`agent/core/runner.py:_build_system_prompt`

---

### 2. Context 周期注入（当前实现）

每 `CONTEXT_INJECTION_INTERVAL`（当前=5）轮迭代，框架自动向模型的 tool_calls 中追加一条 `get_current_context` 调用（如果模型本轮未主动调用）。

注入内容（`_build_context_injection` 构建）：
- 当前时间
- GoalTree（精简视图）+ focus 提醒
- Active Collaborators 状态
- IM 消息通知（如有）
- Context Injection Hooks 注册的自定义内容

注入形态：**合成的 tool_call + tool_result 消息对**。模型看到的效果等同于"我之前主动调用了 get_current_context"。

```
[assistant]  tool_calls: [..., get_current_context()]   ← 框架自动追加
[tool]       { 当前时间, GoalTree, Collaborators... }   ← 工具正常执行
```

**特点**：
- 固定间隔，不做去重（每次内容都是最新状态快照）
- 仅在主路径执行（侧分支中跳过）
- 检查模型是否已主动调用，避免重复

**实现**：`agent/core/runner.py`（`CONTEXT_INJECTION_INTERVAL`, 工具执行后的自动注入逻辑）

**详细文档**：[架构设计 § Context Injection Hooks](./architecture.md#context-injection-hooks上下文注入钩子)

---

### 3. Skill 指定注入（设计中，未实现）

区别于模型通过 `skill()` 工具主动加载 skill，指定注入是**调用方在 `runner.run()` 时声明**需要哪些 skill，由框架自动注入。

#### 动机

System Prompt 中的内置 skills 是启动时一次性注入的。但某些 skill（如 Librarian 的查询策略/上传策略）：
- 不是每次都需要（按任务类型按需加载）
- 在长期续跑的 agent 中会被压缩掉，需要重新注入
- 应该和现有的 `skill` 工具统一管理

#### 启用方式

调用方通过 `runner.run()` 的 `inject_skills` 参数声明（不放在 RunConfig 中，因为同一个 agent 的不同调用可能需要不同的 skill）：

```python
# Librarian ask 场景
async for item in runner.run(
    messages=[{"role": "user", "content": "[ASK] 查询内容"}],
    config=config,
    inject_skills=["ask_strategy"],         # 本次调用需要的 skill
    skill_recency_threshold=10,             # 最近 N 条消息内有就不重复注入
):
    ...

# Librarian upload 场景（同一个 agent，不同 skill）
async for item in runner.run(
    messages=[{"role": "user", "content": "[UPLOAD:BATCH] 数据"}],
    config=config,
    inject_skills=["upload_strategy"],
    skill_recency_threshold=10,
):
    ...
```

#### 注入机制

框架在工具执行阶段自动注入（与 context 周期注入同级处理）。

**注入形态**：合成的 `skill("xxx")` tool_call + tool_result 消息对，复用现有 `skill` 工具。

```
[user]       [ASK] 有没有工具能做人物姿态控制？
[assistant]  tool_calls: [skill("ask_strategy")]        ← 框架自动生成
[tool]       { ask_strategy skill 完整内容 }             ← skill 工具正常执行
[assistant]  让我来检索...                               ← 模型真实输出
```

#### 防重复：message ID 追踪

在 `trace.context` 中维护已注入 skill 的记录：

```python
trace.context["injected_skills"] = {
    "ask_strategy": {
        "message_id": "msg_abc123",   # tool_result 消息的 ID
        "sequence": 42                # 最新注入的消息序列号
    }
}
```

注入前检测流程：

1. 查 `injected_skills[skill_id].message_id`
2. 检查该 message_id 是否仍在当前提交给模型的消息列表中
3. 在且 sequence 在最近 `recency_threshold` 条消息内（从当前提交给模型的列表中查，不按编号计算） → 跳过
4. 不在（被压缩掉）或超过阈值 → 重新注入，更新记录

#### Skill 的两种加载方式

| 方式 | 触发者 | 场景 |
|---|---|---|
| 指定注入（本节） | 调用方，通过 `run(inject_skills=...)` | 已知任务类型，确保策略在场 |
| 主动加载 | 模型自己调 `skill()` 工具 | 遇到未预见场景，自主判断需要什么 |

两者都走 `skill` 工具，消息格式一致。

#### Skill 文件位置

指定注入的 skill 文件遵循现有 skill 体系（Markdown + frontmatter），按项目组织：

```
knowhub/agents/skills/         # KnowHub Librarian 的 skills
├── ask_strategy.md            # 查询检索策略
└── upload_strategy.md         # 上传编排策略
```

---

## 压缩机制

压缩分两级，通过 `RunConfig.goal_compression` 控制 Level 1 行为：

| 模式 | 值 | Level 1 行为 | Level 2 行为 |
|------|---|---|---|
| 不压缩 | `"none"` | 跳过 Level 1 | 超限时直接进入 Level 2 |
| 完成后压缩 | `"on_complete"` | 每个 goal 完成时立刻压缩该 goal 的消息 | 超限时进入 Level 2 |
| 超长时压缩 | `"on_overflow"` | 超限时遍历所有 completed goal 逐个压缩 | Level 1 后仍超限则进入 Level 2 |

默认值：`"on_overflow"`

### Level 1：Goal 完成压缩（确定性，零 LLM 成本）

对单个 completed/abandoned goal 的压缩逻辑：

1. **识别目标消息**：找到该 goal 关联的所有消息（`msg.goal_id == goal.id`）
2. **区分 goal 工具消息和非 goal 消息**：检查 assistant 消息的 tool_calls 中是否调用了 `goal` 工具
3. **保留 goal 工具消息**：保留所有调用 `goal(...)` 的 assistant 消息及其对应的 tool result（包括 add、focus、under、done 等操作）
4. **删除非 goal 消息**：从 history 中移除该 goal 的其他 assistant 消息及其 tool result（read_file、bash、search 等中间工具调用）
5. **替换 done 的 tool result**：将 `goal(done=...)` 的 tool result 内容替换为 `"具体执行过程已清理"`
6. **纯内存操作**：压缩仅操作内存中的 history 列表，不涉及新增消息或持久化变更，原始消息永远保留在存储层

压缩后的 history 片段示例：

```
... (前面的消息)
[assistant] tool_calls: [goal(focus="1.1")]
[tool] goal result: "## 更新\n- 焦点切换到: 1.1\n\n## Current Plan\n..."
[assistant] tool_calls: [goal(done="1.1", summary="前端使用 React...")]
[tool] goal result: "具体执行过程已清理"
... (后面的消息)
```

#### `on_complete` 模式

在 goal 工具执行 `done` 操作后，立刻对该 goal 执行压缩。优点是 history 始终保持精简，缺点是如果后续需要回溯到该 goal 的中间过程，信息已丢失（存储层仍保留原始消息）。

**触发点**：`agent/core/runner.py`（工具执行后检测 `goal(done=...)` 调用）

#### `on_overflow` 模式

在 `_manage_context_usage` 检测到 token 超限时，遍历所有 completed goal，逐个执行压缩，直到 token 数降到阈值以下或所有 completed goal 都已压缩。如果仍超限，进入 Level 2。

**触发点**：`agent/core/runner.py:_manage_context_usage`

**实现**：`agent/trace/compaction.py:compress_completed_goals`

### Level 2：LLM 摘要压缩

触发条件：Level 1 之后 token 数仍超过阈值（默认 `context_window × 0.5`）。

通过侧分支队列机制执行，`force_side_branch` 为列表类型：

1. **反思**（可选，由 `knowledge.enable_extraction` 控制）：进入 `reflection` 侧分支，LLM 可多轮调用工具提取知识
2. **知识评估**（可选）：进入 `knowledge_eval` 侧分支，评估待评估知识
3. **压缩**：进入 `compression` 侧分支，LLM 生成 summary

侧分支队列示例：
- 启用知识提取：`["reflection", "compression"]`
- 有待评估知识：`["knowledge_eval", "compression"]`
- 仅压缩：`["compression"]`

压缩完成后重建 history 为：`system prompt + 第一条 user message + summary（含详细 GoalTree）`

**实现**：`agent/core/runner.py:_agent_loop`（侧分支状态机）, `agent/core/runner.py:_rebuild_history_after_compression`

### 任务完成后反思

主路径无工具调用（任务完成）时，如果 `knowledge.enable_completion_extraction` 为 True，通过侧分支进入反思，完成后退出循环。

### GoalTree 双视图

`to_prompt()` 支持两种模式：
- `include_summary=False`（默认）：精简视图，用于日常周期性注入
- `include_summary=True`：含所有 completed goals 的 summary，用于 Level 2 压缩时提供上下文

### 压缩存储

- 原始消息永远保留在 `messages/`
- 压缩 summary 作为普通 Message 存储
- 侧分支消息通过 `branch_type` 和 `branch_id` 标记，查询主路径时自动过滤
- 通过 `parent_sequence` 树结构实现跳过，无需 compression events 或 skip list
- Rewind 到压缩区域内时，summary 脱离主路径，原始消息自动恢复

---

## 注入与压缩的交互

压缩会移除消息，包括之前注入的 skill 和 context 内容。不同注入类型的应对策略：

| 注入类型 | 压缩影响 | 恢复策略 |
|---------|---------|---------|
| System Prompt | Level 2 重建时保留 | 无需额外处理 |
| Context 周期注入 | 可能被 Level 1/2 移除 | 固定间隔自动重注入（下一个 interval 即恢复） |
| Skill 动态注入 | 可能被 Level 1/2 移除 | message ID 检测 → 自动重注入 |

Skill 重注入的触发点：每次 runner 准备构建 LLM 调用前，检查 `injected_skills` 中记录的 message_id 是否仍在当前 history 中。不在则标记为需要重新注入，在下一轮工具执行阶段合成 tool_call。

---

## 相关文档

| 文档 | 内容 |
|------|------|
| [架构设计](./architecture.md) | Agent 框架完整架构 |
| [Skills 指南](./skills.md) | Skill 文件格式、分类、加载机制 |
| [Prompt 规范](./prompt-guidelines.md) | 信息分层、条件注入原则 |
| [Trace API](./trace-api.md) | 压缩和反思的 REST API |
