@linnlabs/linnkit 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +178 -0
  4. package/README.zh-CN.md +182 -0
  5. package/dist/agent-invocation-BHcNfrBV.d.cts +30 -0
  6. package/dist/agent-invocation-BznDaXDs.d.ts +30 -0
  7. package/dist/agentEvents-DEB7Fy_J.d.cts +81 -0
  8. package/dist/agentEvents-DEB7Fy_J.d.ts +81 -0
  9. package/dist/agentSpec-EkmviZjy.d.cts +2621 -0
  10. package/dist/agentSpec-EkmviZjy.d.ts +2621 -0
  11. package/dist/ai-engine.types-BpeU_XQG.d.cts +158 -0
  12. package/dist/ai-engine.types-vZRnQcJa.d.ts +158 -0
  13. package/dist/audit-BaRUGaqv.d.cts +307 -0
  14. package/dist/audit-BaRUGaqv.d.ts +307 -0
  15. package/dist/audit-CtcfART1.d.ts +33 -0
  16. package/dist/audit-LeOrm2hX.d.cts +33 -0
  17. package/dist/checkpointMarker-DAI3wUQu.d.cts +8 -0
  18. package/dist/checkpointMarker-DAI3wUQu.d.ts +8 -0
  19. package/dist/cli.cjs +8028 -0
  20. package/dist/cli.cjs.map +1 -0
  21. package/dist/cli.d.cts +4 -0
  22. package/dist/cli.d.ts +4 -0
  23. package/dist/cli.js +8025 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/context-manager.cjs +8704 -0
  26. package/dist/context-manager.cjs.map +1 -0
  27. package/dist/context-manager.d.cts +2190 -0
  28. package/dist/context-manager.d.ts +2190 -0
  29. package/dist/context-manager.js +8650 -0
  30. package/dist/context-manager.js.map +1 -0
  31. package/dist/context-trace-DRi5M4lX.d.ts +239 -0
  32. package/dist/context-trace-HE2qY5Q-.d.cts +239 -0
  33. package/dist/contracts.cjs +1333 -0
  34. package/dist/contracts.cjs.map +1 -0
  35. package/dist/contracts.d.cts +8 -0
  36. package/dist/contracts.d.ts +8 -0
  37. package/dist/contracts.js +1214 -0
  38. package/dist/contracts.js.map +1 -0
  39. package/dist/defaultGraphExecutor-BBswR8wn.d.ts +624 -0
  40. package/dist/defaultGraphExecutor-BIjJj7WF.d.cts +624 -0
  41. package/dist/execution-CAIypb41.d.cts +129 -0
  42. package/dist/execution-CAIypb41.d.ts +129 -0
  43. package/dist/index-CHqwkvGp.d.ts +149 -0
  44. package/dist/index-CJeWHopy.d.ts +584 -0
  45. package/dist/index-Cm-JbzTH.d.cts +1450 -0
  46. package/dist/index-Cvr23YCl.d.cts +23 -0
  47. package/dist/index-DDzuSb0n.d.ts +23 -0
  48. package/dist/index-DO4dQgf2.d.cts +584 -0
  49. package/dist/index-DRBWi1fy.d.ts +1450 -0
  50. package/dist/index-Dl5PLgAv.d.cts +149 -0
  51. package/dist/index.cjs +9577 -0
  52. package/dist/index.cjs.map +1 -0
  53. package/dist/index.d.cts +89 -0
  54. package/dist/index.d.ts +89 -0
  55. package/dist/index.js +9563 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/messages-XthmnHZ3.d.cts +8007 -0
  58. package/dist/messages-XthmnHZ3.d.ts +8007 -0
  59. package/dist/ports-DaatKJXp.d.cts +90 -0
  60. package/dist/ports-DnLuKfpE.d.ts +90 -0
  61. package/dist/ports.cjs +4 -0
  62. package/dist/ports.cjs.map +1 -0
  63. package/dist/ports.d.cts +7 -0
  64. package/dist/ports.d.ts +7 -0
  65. package/dist/ports.js +3 -0
  66. package/dist/ports.js.map +1 -0
  67. package/dist/quickstart.cjs +7697 -0
  68. package/dist/quickstart.cjs.map +1 -0
  69. package/dist/quickstart.d.cts +24 -0
  70. package/dist/quickstart.d.ts +24 -0
  71. package/dist/quickstart.js +7691 -0
  72. package/dist/quickstart.js.map +1 -0
  73. package/dist/runAgent-CPj_9e58.d.ts +88 -0
  74. package/dist/runAgent-HYKlXbVr.d.cts +88 -0
  75. package/dist/runHandle-CyXvzgzk.d.ts +239 -0
  76. package/dist/runHandle-D3gPsD7B.d.cts +239 -0
  77. package/dist/runtime-kernel/events.cjs +1485 -0
  78. package/dist/runtime-kernel/events.cjs.map +1 -0
  79. package/dist/runtime-kernel/events.d.cts +8 -0
  80. package/dist/runtime-kernel/events.d.ts +8 -0
  81. package/dist/runtime-kernel/events.js +1475 -0
  82. package/dist/runtime-kernel/events.js.map +1 -0
  83. package/dist/runtime-kernel.cjs +8656 -0
  84. package/dist/runtime-kernel.cjs.map +1 -0
  85. package/dist/runtime-kernel.d.cts +19 -0
  86. package/dist/runtime-kernel.d.ts +19 -0
  87. package/dist/runtime-kernel.js +8568 -0
  88. package/dist/runtime-kernel.js.map +1 -0
  89. package/dist/sse-vPyrOPa0.d.cts +1687 -0
  90. package/dist/sse-vPyrOPa0.d.ts +1687 -0
  91. package/dist/testkit.cjs +10613 -0
  92. package/dist/testkit.cjs.map +1 -0
  93. package/dist/testkit.d.cts +284 -0
  94. package/dist/testkit.d.ts +284 -0
  95. package/dist/testkit.js +10593 -0
  96. package/dist/testkit.js.map +1 -0
  97. package/dist/todo-B1PmDlp3.d.cts +2253 -0
  98. package/dist/todo-B1PmDlp3.d.ts +2253 -0
  99. package/dist/tokenizer-DFL4I7-I.d.ts +28 -0
  100. package/dist/tokenizer-DH_JXv-H.d.cts +28 -0
  101. package/dist/toolContracts-Blll0241.d.ts +463 -0
  102. package/dist/toolContracts-CLkQmhTG.d.cts +463 -0
  103. package/docs/README.md +76 -0
  104. package/docs/integration/01-installation.md +94 -0
  105. package/docs/integration/02-quickstart.md +104 -0
  106. package/docs/integration/README.md +223 -0
  107. package/docs/integration/agent-registration-guide.md +330 -0
  108. package/docs/integration/audit.md +64 -0
  109. package/docs/integration/child-runs.md +87 -0
  110. package/docs/integration/constraints-and-pitfalls.md +87 -0
  111. package/docs/integration/context-engineering.md +650 -0
  112. package/docs/integration/context-fences.md +289 -0
  113. package/docs/integration/glossary.md +69 -0
  114. package/docs/integration/llm-provider.md +76 -0
  115. package/docs/integration/persistence.md +44 -0
  116. package/docs/integration/realtime.md +76 -0
  117. package/docs/integration/run-supervisor.md +69 -0
  118. package/docs/integration/telemetry.md +48 -0
  119. package/docs/integration/testing.md +95 -0
  120. package/docs/integration/tool-development-guide.md +362 -0
  121. package/docs/integration/tool-history.md +202 -0
  122. package/docs/integration/tools.md +188 -0
  123. package/package.json +115 -0
@@ -0,0 +1,650 @@
1
+ # Context Engineering · linnkit 的上下文工程总览
2
+
3
+ > **What** · 所有作用在 messages 上的机制总览 —— `contextPolicy` 12 大分组 + `ContextTrace` 可观测闭环 + `TokenizerPort` + 摘要 / 围栏 / 工具历史压缩。
4
+ > **When to read** · 想精确控制每个 token;上下文超长被裁;要诊断"为什么这条消息被丢了";自定义 tokenizer;做 token 预算选型。
5
+ > **Prerequisites** · [`agent-registration-guide.md`](./agent-registration-guide.md) ⭐(先理解 `AgentSpec.contextPolicy` 字段结构)。
6
+ > **Key exports** · `ContextTrace` from `@linnlabs/linnkit/contracts` · `TokenizerPort` from `@linnlabs/linnkit/ports` · `formatAgentLlmMessages` / `createMessageFormatter` from `@linnlabs/linnkit/context-manager`。
7
+ > **Related** · [`context-fences.md`](./context-fences.md) ⭐ · [`tool-history.md`](./tool-history.md) · [`agent-registration-guide.md`](./agent-registration-guide.md) ⭐
8
+
9
+ > linnkit 的宗旨:**让上下文工程变成精细化、可自由配置、可观测、可审计的事**。
10
+ >
11
+ > 本文是一份**全机制速查表**:列出 linnkit 当前所有作用在"发给 LLM 的 messages"上的机制,**说人话**讲清楚它在做什么、什么时候触发、在哪里改它、当前可以配到什么粒度。
12
+ >
13
+ > 想看 fence 一等接入面的具体落地骨架,跳到 [`context-fences.md`](./context-fences.md)。
14
+
15
+ ---
16
+
17
+ ## 0. 一张总览图(一轮请求里发生了什么)
18
+
19
+ ```text
20
+ host 发出 invoke request
21
+
22
+
23
+ [A] AgentMessageOrchestrator 装配
24
+ ├─ contextPolicy.mustKeep(哪些消息绝不能被裁)
25
+ ├─ FenceRegistry(host 注册了哪些围栏家族)
26
+ └─ Preprocessor pipeline 按 request 重建
27
+
28
+
29
+ [B] Preprocessor Pipeline(按优先级跑)
30
+ 1. ToolHistoryCompressorPreprocessor ─ 工具历史压缩
31
+ 2. ToolReplayProtocolGuardPreprocessor ─ 工具回放协议守卫
32
+ 3. HistoryPurificationPreprocessor ─ 历史净化(清孤儿 / 同 ID 去重)
33
+ 4. FenceLifetimePreprocessor ─ 剥离旧轮 turn-only fence
34
+
35
+
36
+ [C] ContextProvider 三阶段填充
37
+ 1. AgentCoreContextProvider ─ 不可裁的核心层(system / user)
38
+ 2. AgentWorkingMemoryProvider ─ 工作记忆按 P1-P4 优先级填到预算上限
39
+ 3. CheckpointSummarizationProvider ─ checkpoint 前的旧轮裁干净
40
+ 4. (自动触发) SummarizationProvider ─ 超预算时整段历史摘要
41
+
42
+
43
+ [D] applySystemReminderStage 注入
44
+ 根据 stepCount / phase / 工具调用次数等触发规则
45
+ 在最后一条 message 末尾追加 <system-reminder>...</system-reminder>
46
+
47
+
48
+ [E] formatAgentLlmMessages 出关
49
+ 根据 fence formatter / 物理 role 把所有 AiMessage 翻译成 LLM wire messages
50
+
51
+
52
+ LLM provider
53
+ ```
54
+
55
+ 每个阶段都有独立的配置面。
56
+
57
+ ---
58
+
59
+ ## 1. 消息的三大角色与物理位置
60
+
61
+ linnkit 的内部消息(`AiMessage` union)最终都会按 LLM 协议的三个 role 出关:
62
+
63
+ | Role | 这里有什么 |
64
+ |------|-----------|
65
+ | `system` | `system_prompt`、`placement: 'after-system'` 的 fence(不常变化的固定上下文),例如长期记忆、项目元信息、用户偏好等 |
66
+ | `assistant` | LLM 自己产的 `final_answer` / `thought`(reasoning_content)/ `tool_calls`;以及配对的 `tool_output`(在物理 wire 上挂 `tool` role) |
67
+ | `user` | 用户的 `user_input`、`placement: 'before-current-user'` / `'after-current-user'` 的 fence(经常变化的高频上下文)、例如用户上传的文件、当前时间等以及触发后的 `<system-reminder>` 注入 |
68
+
69
+ **重要不变量**:`tool_calls` 和 `tool_output` **必须成对出现**——任何一边丢了另一边就废了。这条不变量贯穿所有压缩 / 裁剪机制。
70
+
71
+ ---
72
+
73
+ ## 2. Fence 围栏家族(高度可配置 ✅)
74
+
75
+ 把任何"想塞给 LLM 的额外上下文"声明成一个家族(kind),告诉 linnkit"放哪、活多久、是否必保留、最多占多少预算",linnkit 帮你按规则塞进 messages、按生命周期清掉。
76
+
77
+ **配置位置**:host 启动时调 `createFenceRegistry(descriptors)`,每条 `FenceDescriptor` 包含:
78
+
79
+ | 字段 | 含义 | 取值 |
80
+ |------|------|------|
81
+ | `kind` | 围栏家族名 | host 自定义 kebab-case |
82
+ | `llmRole` | 物理挂到哪个 role | `'system'` / `'user'` |
83
+ | `placement` | 物理位置 | `'after-system'` / `'before-current-user'` / `'after-current-user'` / `'after-last-tool-result'` |
84
+ | `lifetime` | 活多久 | `'turn-only'`(只本轮)/ `'persisted'`(进 history) |
85
+ | `mustKeep` | 是否在 working memory 抽稀时必保留 | `boolean` |
86
+ | `maxBudgetFraction` | 单类 fence 最多占总预算多少 | `(0, 1]` |
87
+ | `formatter` | 怎么把内容包装成 LLM 看到的字面 | host 提供函数 |
88
+
89
+ **开放状态**:完全开放。详细装配骨架见 [`context-fences.md`](./context-fences.md)。
90
+
91
+ ---
92
+
93
+ ## 3. MustKeepPolicy(通过 `contextPolicy.mustKeep` 配置 ✅)
94
+
95
+ 声明"哪些消息**永远不能**被工作记忆抽稀机制裁掉"——比如 `system_prompt`、最新的 `user_input`、某类 fence kind。
96
+
97
+ **配置位置**:优先写在 `AgentSpec.contextPolicy.mustKeep`。host 也可以提供 fallback policy,作为所有 agent 的默认值。
98
+
99
+ ```ts
100
+ contextPolicy: {
101
+ profileId: 'agent',
102
+ mustKeep: {
103
+ alwaysKeepTypes: ['system_prompt', 'user_input'], // 按 AiMessage.type
104
+ alwaysKeepFenceKinds: ['system-event'], // 按 fence kind
105
+ truncationRules: [
106
+ // 想限量截断(不丢但只保留预算的 X%)
107
+ { fenceKind: 'memory-context', maxBudgetFraction: 0.2, strategyName: 'memory-truncate' },
108
+ ],
109
+ },
110
+ }
111
+ ```
112
+
113
+ **默认值**:`DEFAULT_MUST_KEEP_POLICY` 已经把 `system_prompt` / `user_input` 等核心 type 列进 alwaysKeepTypes。
114
+
115
+ **开放状态**:`AgentSpec.contextPolicy.mustKeep` 已完成运行时接线。host 可以提供 fallback policy,单个 agent 可以通过 spec 覆盖;例如 `additional-context` 这类产品语义应属于 host fallback,不写进 linnkit framework。
116
+
117
+ ---
118
+
119
+ ## 4. Preprocessor Pipeline(4 个内置预处理器)
120
+
121
+ 这一层在"消息进 ContextProvider 之前"跑。按 priority 顺序执行;任何一个抛 fatal `ContextProviderError` 都会中断 pipeline。
122
+
123
+ ### 4.1 ToolHistoryCompressorPreprocessor —— 工具历史压缩(AgentSpec 高度可配置 ✅)
124
+
125
+ 把旧的 `tool_calls` + `tool_output` 配对压缩成简短 assistant 文本,控制工具历史不无限膨胀。三种策略:`'per-run'`(默认,按 user_input 边界保留最近 K 个 run)/ `'per-pair'`(保留最近 N 个工具对)/ `'none'`(不压缩)。安全阀:`maxInteractionGroups` 硬上限 + `overflowStrategy`(`'keep-latest'` / `'fail-fast'`)。
126
+
127
+ 完整字段、默认值与选型对比见 [`tool-history.md`](./tool-history.md) ⭐。
128
+
129
+ ### 4.2 ToolReplayProtocolGuardPreprocessor —— 工具回放协议守卫(无需配置,自动开启)
130
+
131
+ 避免旧的工具组被 provider 误认为是结构化 replay。host 装配时已自动启用,不需要管。
132
+
133
+ **配置位置**:无;如果你完全自定义 pipeline 才需要手动 register。
134
+
135
+ **开放状态**:默认开启;属于协议级保护,没设计成可配置项。
136
+
137
+ ### 4.3 HistoryPurificationPreprocessor —— 历史净化(无需配置)
138
+
139
+ 清理孤儿 `tool_calls`(没有对应 `tool_output`)、同 ID 重复消息、空消息等异常状态。
140
+
141
+ **开放状态**:默认开启,无配置;这是数据卫生层。
142
+
143
+ ### 4.4 FenceLifetimePreprocessor —— 旧轮 turn-only 剥离(自动跟 FenceRegistry 走)
144
+
145
+ 上一轮注入的 `lifetime: 'turn-only'` 的 fence(比如临时引用文本、临时记忆片段),这一轮自动剥掉。
146
+
147
+ **配置位置**:注册 fence 时通过 `lifetime` 字段控制;不需要单独配 preprocessor。
148
+
149
+ ---
150
+
151
+ ## 5. ContextProvider 三阶段填充
152
+
153
+ ### 5.1 AgentCoreContextProvider —— 核心层(无需配置)
154
+
155
+ 把 must-keep 的核心消息(system_prompt + 最新 user_input + alwaysKeep 的 fence)按物理 role 钉到 messages 数组里,**永不裁剪**。
156
+
157
+ **开放状态**:行为完全由 MustKeepPolicy 决定(见 §3)。
158
+
159
+ ### 5.2 AgentWorkingMemoryProvider —— 工作记忆按 P1-P4 优先级填充(AgentSpec 已运行时接线 ✅)
160
+
161
+ 扣掉核心层之后剩多少预算,按 4 个优先级倒着塞进消息:
162
+
163
+ | 优先级 | 内容 |
164
+ |--------|------|
165
+ | **P1** | 最近的工具交互对(tool_calls + tool_output) |
166
+ | **P2** | 纯文本对话(final_answer + user 消息) |
167
+ | **P3** | 更早的工具交互(可能已被 ToolHistoryCompressor 压成 assistant 文本) |
168
+ | **P4** | 循环填充剩余空间 |
169
+
170
+ **可配置字段**(写在 `AgentSpec.contextPolicy`):
171
+
172
+ | 字段 | 默认 | 含义 | 开放状态 |
173
+ |------|------|------|---------|
174
+ | `budget.maxTokens` | `120000` | 总预算上限 | ✅ AgentSpec + runtime |
175
+ | `budget.reservedForResponse` | `2400` | 留给 LLM 输出的 token | ✅ AgentSpec + runtime |
176
+ | `budget.workingMemoryBudgetPercentage` | `0.70` | 工作记忆占可用预算的比例 | ✅ AgentSpec + runtime |
177
+ | `reasoningRetention.keepLatestThoughts` | `1` | 最近保留多少条 thought | ✅ AgentSpec + runtime |
178
+ | `workingMemory.minToolInteractionsToKeep` | `2` | 即便预算不够也至少保留多少组工具对 | ✅ AgentSpec + runtime |
179
+ | `workingMemory.maxRecentToolInteractions` | `2` | 原始 tool_calls 形态保留的最大组数 | ✅ AgentSpec + runtime |
180
+ | `workingMemory.toolPairingSearchRange` | `10` | 搜工具配对的窗口范围 | ✅ AgentSpec + runtime |
181
+ | P1-P4 优先级数字 | `1/2/3/4` | 优先级编号 | ❌ 不开放|
182
+
183
+ ### 5.3 CheckpointSummarizationProvider —— Checkpoint 主动压缩(开放方式特殊 ✅)
184
+
185
+ Agent 主动调一个约定为 `context_checkpoint` 的工具。工具执行成功后,history 里会出现一组普通的 `tool_calls -> tool_output`;只要这个 tool output 的原始结果里带有 linnkit 认可的 checkpoint marker,`CheckpointSummarizationProvider` 就会在下一次上下文构建时清理 checkpoint 之前的旧历史。
186
+
187
+ - **保留**:must-keep + 这个 checkpoint 工具对本身 + checkpoint 之前最近 N 对工具交互(默认 N=2,可用 `checkpoint.keepPairsBefore` 覆盖)
188
+ - **清掉**:checkpoint 之前更旧的 tool_calls / tool_output / final_answer / thought / 旧 history_summary
189
+
190
+ linnkit 提供协议(`CHECKPOINT_MARKER_TYPE` / `CheckpointSummarizationProvider` / SystemReminder & step-reset 联动)+ 最小工具(`ContextCheckpointTool`);`taskstate` / shared memory 等 host 状态扩展由接入方负责,可通过下面的 hook 接入。
191
+
192
+ 最小接入:
193
+
194
+ ```ts
195
+ import { ContextCheckpointTool } from '@linnlabs/linnkit/runtime-kernel';
196
+
197
+ export const tools = [
198
+ new ContextCheckpointTool(),
199
+ ];
200
+ ```
201
+
202
+ 如果你改工具名,必须同时改 `contextPolicy.checkpoint.triggerToolName`:
203
+
204
+ ```ts
205
+ const checkpointTool = new ContextCheckpointTool({ name: 'phase_checkpoint' });
206
+
207
+ contextPolicy: {
208
+ profileId: 'agent',
209
+ checkpoint: {
210
+ triggerToolName: 'phase_checkpoint',
211
+ },
212
+ }
213
+ ```
214
+
215
+ 如果 host 有自己的状态系统,可以用 hook 扩展 payload / observation:
216
+
217
+ ```ts
218
+ const checkpointTool = new ContextCheckpointTool({
219
+ extraParameters: {
220
+ taskstate: {
221
+ type: 'object',
222
+ description: 'Host task state snapshot',
223
+ },
224
+ },
225
+ buildPayloadExtension: async ({ args, context }) => {
226
+ // 可选:写入 host 自己的 TaskState / Memory / 文件系统。
227
+ // 返回值会合并到 tool result data 中,但 _type 与 summary 由 linnkit 固定写回。
228
+ return {
229
+ conversation_id: context.conversationId,
230
+ taskstate: args.taskstate,
231
+ };
232
+ },
233
+ });
234
+ ```
235
+
236
+ > 注意:`CheckpointSummarizationProvider` 严格读取 `tool_output.metadata.raw_output` 里的 marker,而不是解析展示给模型看的 observation 文本。这是为了避免普通工具输出碰巧像 JSON 时误触发 checkpoint。
237
+
238
+ **与 summarization 的关键区别**:
239
+
240
+ | 维度 | summarization | checkpoint |
241
+ |------|---------------|-----------|
242
+ | 谁触发 | linnkit 在 token 超阈值时**被动**触发 | agent 主动判断("完成了一个阶段任务"等)**主动**触发 |
243
+ | 体感 | 文本摘要,丢得多 | 工具对形态,保留 agent 自己的总结 + task state |
244
+ | 何时 | 上限附近 | 任何时候 |
245
+ | 颗粒度 | 较粗 | agent 自己控制颗粒 |
246
+
247
+ **配置位置 / 开放状态**(完整开放面清单见 §11):
248
+
249
+ - ✅ **启用方式**:把 `ContextCheckpointTool` 注册进 agent 的工具集即可;不想用就不注册
250
+ - ✅ **改工具名**:`contextPolicy.checkpoint.triggerToolName` 同时控制裁剪识别、GraphExecutor step-reset、SystemReminder 文案——改名必须确保对应工具真实注册进 agent
251
+ - ✅ **marker 协议**:`CHECKPOINT_MARKER_TYPE` framework 内固定;自定义工具也必须输出这个 marker
252
+
253
+ ### 5.4 自动 SummarizationProvider —— 超预算被动摘要(注册 agent 可配置 ✅)
254
+
255
+ 当 token 总用量超过阈值,把最旧的一批消息丢给一个**专用的摘要 agent**,让它生成一段总结,替换掉这批旧消息。
256
+
257
+ **可配置字段**:
258
+
259
+ | 字段 | 默认 | 含义 | 开放状态 |
260
+ |------|------|------|---------|
261
+ | `summarization.triggerThreshold` | `0.70` | 超过总预算的多少比例触发 | ✅ AgentSpec |
262
+ | `summarization.budgetPercentage` | `0.12` | 摘要文本本身的 token 长度上限占比 | ✅ AgentSpec |
263
+ | `summarization.oldestMessagesPercentage` | `0.75` | 选取多大比例的最老消息进摘要 | ✅ AgentSpec |
264
+ | 摘要 agent + 失败行为 | `summarization.agentId` / `failureBehavior`;摘要必须通过 host 注册 agent/chat 调用 | ✅ AgentSpec + runtime |
265
+ | 摘要失败的 fatal 判断 | 通过 `ContextProviderError({ code: 'SUMMARIZATION_FAILED', fatal: true })` 抛出 | ✅ 协议化 |
266
+
267
+ **摘要 agent 的注册边界**:framework 不持有摘要 prompt 正文,也不直接发起裸 LLM call。它只把 `summarization.agentId` 放进 `GenerateRequest.promptKey`,由 host 通过自己的注册表解析成一个无工具摘要 agent/chat,再按该注册项的 prompt、模型策略与执行方式完成调用。host 可以把默认摘要 agent 注册为 `history_compression`,也可以在 `contextPolicy.summarization.agentId` 中为单个 agent 指定别的注册项。
268
+
269
+ **最小注册示例**(在 host 侧先注册摘要 agent,再在业务 spec 里填 `agentId`)见 [`agent-registration-guide.md`](./agent-registration-guide.md) §4.2。
270
+
271
+ **失败行为**:
272
+
273
+ | `failureBehavior` | 行为 |
274
+ |-------------------|------|
275
+ | `'fail-fast'`(默认)| 摘要失败立即抛 typed fatal `ContextProviderError`,保持旧行为 |
276
+ | `'continue-if-within-budget'` | 只有当前上下文仍在预算内时才允许继续使用原始消息;如果已经超预算,仍然 fail-fast |
277
+
278
+ ---
279
+
280
+ ## 6. System Reminder(注册表 + AgentSpec 已接线 ✅,**有一项不开放**)
281
+
282
+ 根据当前 tick 的状态(步数、phase、工具调用次数等)在**最后一条 message 末尾**追加一段 `<system-reminder>...</system-reminder>`,利用 LLM 注意力的末尾效应做行为引导。
283
+
284
+ **核心设计原则**(重要,你草稿里有一处理解偏差,看下面):
285
+
286
+ | 不变量 | 说明 |
287
+ |--------|------|
288
+ | ✅ 只对当前 tick 生效 | 不写入 history、不持久化、不产生 RuntimeEvent |
289
+ | ✅ 注入位置固定 | 最后一条 message 的 content 末尾,包裹在 `<system-reminder>` 标签 |
290
+ | ✅ 配置驱动 | 内置规则与 host extraRules 都通过 trigger + contentTemplate 注册表解释 |
291
+ | ❌ **不能配置进短期对话历史** | "可以配置允许进入短期对话历史并持久化"——这条**当前不支持**,是反协议的:reminder 本质是"瞬态状态注入",进 history 会污染缓存与回放语义。如果产品真有"持久化提示"需求,应该走 fence 通道(`lifetime: 'persisted'`),而不是 reminder |
292
+
293
+ **触发方式**:framework 内置 5 条规则,按顺序判定:
294
+
295
+ | 规则 ID | 触发条件 | 用途 |
296
+ |---------|---------|------|
297
+ | `max_steps_force_final_answer` | `phase === 'force_final_answer'` | 最后一步强制收尾,禁用工具 |
298
+ | `last_steps_hint` | `remainingSteps <= threshold` | 剩余步数提示 |
299
+ | `tool_call_streak_every_ten` | 本轮工具调用次数 ≥ 10 且为 10 的倍数 | 工具循环过深告警 |
300
+ | `periodic_taskstate_reflection` | `stepCount` 是 30 的倍数 | 长程任务定期反思 |
301
+ | `context_budget_warning` | `stepCount` 达 maxSteps 的 90% 且 agent 有 `checkpoint.triggerToolName` 对应工具 | 上下文即将耗尽,引导调 checkpoint |
302
+
303
+ **配置开放状态**:
304
+
305
+ | 项 | 开放状态 | 备注 |
306
+ |---|---------|------|
307
+ | 规则触发的阈值(10 / 30 / 90% 等数字)| ✅ AgentSpec + runtime | `systemReminder.thresholds` 覆盖 |
308
+ | 规则文案 | ✅ 注册表 | 内置文案在 runtime template;host extraRules 通过 `contentTemplate` 引用 host 注册模板 |
309
+ | 是否启用某条规则(白名单/黑名单)| ✅ AgentSpec + runtime | `enabledRuleIds` 与 `disabledRuleIds` 二选一 |
310
+ | host 自定义新规则 | ✅ AgentSpec + runtime | `systemReminder.extraRules` 通过 trigger/template 注册表解释 |
311
+ | reminder 进 history(持久化)| 🔴 **不开放且不计划开放** | 见上面不变量第 4 条 |
312
+
313
+ **注册式扩展边界**:
314
+
315
+ - spec 只写 `extraRules: [{ id, trigger, contentTemplate, contentArgs }]`,不允许写函数。
316
+ - trigger 由 `SystemReminderRegistry.registerTriggerKind(kind, evaluator)` 注册。
317
+ - 文案由 `SystemReminderRegistry.registerContentTemplate(name, template)` 注册。
318
+ - 内置 5 条规则也走同一套解释链路,因此自定义规则、阈值覆盖、启用/禁用规则的行为一致。
319
+
320
+ ---
321
+
322
+ ## 7. Tool Output 截断与落盘(AgentSpec 阈值可配置 ✅)
323
+
324
+ 工具返回结果太长会**两端各处理一次**——一次在执行期、一次在上下文构建期:
325
+
326
+ ### 7.1 执行期落盘(ToolNode observationGovernance)
327
+
328
+ - 工具刚执行完,原始 observation 字符串如果超过阈值,就通过 host 提供的 `ObservationPreviewPort` **写一份完整副本到 ToolOutputStore / 本地文件 / 对象存储**,messages 里只保留 preview + `tool_output_store.blob_id` 指针
329
+ - 截断治理由 `AgentSpec.contextPolicy.toolOutput.observationGovernance` 控制;**存储后端、目录、文件命名规则由 host 的 `ObservationPreviewPort` 配置**,不进入 AgentSpec
330
+ - **开放状态**:阈值与启停已进 AgentSpec + runtime;落盘实现仍由 host 的 `ObservationPreviewPort` 决定
331
+
332
+ ```ts
333
+ contextPolicy: {
334
+ profileId: 'agent',
335
+ toolOutput: {
336
+ observationGovernance: {
337
+ enabled: true,
338
+ maxChars: 20_000,
339
+ maxLines: 1_200,
340
+ },
341
+ },
342
+ }
343
+ ```
344
+
345
+ 接入方实现自己的 `ObservationPreviewPort`,把存储后端 / 路径 / bucket 等参数放在 host 配置里,再传给 `createDefaultGraphExecutor({ observationPreview })`。详细规范见 [`tools.md §6`](./tools.md#6-observationpreviewport配置超长-observation-存储路径)。
346
+
347
+ > **续读约束**:如果 host 自定义存储路径,读取 `tool_output://blobs/<blob_id>` 的工具必须使用同一个 store,否则模型拿到 `blob_id` 后无法续读。
348
+
349
+ ### 7.2 上下文构建期截断(MAX_TOOL_PAIR_TOKENS)
350
+
351
+ 工具历史进 working memory 时,单对工具 token 总量超过 `maxPairTokens` 会触发 `ToolOutputSummarizer` 把 `tool_output` 压缩成短文本(目标长度由 `maxOutputSummaryTokens` 控制)。默认值见 [`tool-history.md §2`](./tool-history.md)。
352
+
353
+ **两层独立、各管各的**:执行期落盘解决"原始观察值过大不该塞进 wire";上下文构建期截断解决"历史工具结果占用过多预算"。
354
+
355
+ ---
356
+
357
+ ## 8. Reasoning / Thought 保留策略(AgentSpec 已运行时接线 ✅)
358
+
359
+ 部分 LLM provider 会返回 `reasoning_content`(思考过程文本),有些模型在能看到之前 reasoning 历史时表现更好——所以 linnkit 把 `thought` 当作一类 AiMessage 保留。
360
+
361
+ **当前可配置且已接入 runtime**:
362
+
363
+ | 项 | 默认 | 开放状态 |
364
+ |---|------|---------|
365
+ | 工作记忆里保留的最近 thought 数量 | `reasoningRetention.keepLatestThoughts = 1` | ✅ AgentSpec + runtime |
366
+ | Provider sidecar replay 行为(reasoning_details 缺失时怎么办)| `'allow'` / `'degrade_to_text'` / `'provider_empty_replay_field'` | ✅ `contextPolicy.providerReplay` 可覆盖;未配置时 host 仍可按模型默认注入 |
367
+
368
+ 默认生产运行时保留**1 条**(最新的那条 thought)。如果需要保留多轮 reasoning,可在 AgentSpec 中设置 `reasoningRetention.keepLatestThoughts`,该字段会透传到 `AgentWorkingMemoryProvider`。
369
+
370
+ Provider replay 是另一件事:它不决定"保留几条 thought",而决定"历史工具组缺少 provider sidecar 时怎么回放"。配置例子:
371
+
372
+ ```ts
373
+ contextPolicy: {
374
+ profileId: 'agent',
375
+ providerReplay: {
376
+ provider: 'system_default',
377
+ requiresReasoningDetailsForToolReplay: true,
378
+ missingSidecarBehavior: 'provider_empty_replay_field',
379
+ },
380
+ }
381
+ ```
382
+
383
+ 边界:如果 `providerReplay` 不配置,linnkit 不会按 `model_id` 自己猜 provider;host 仍可以通过 `resolveToolReplayProtocolPolicy` 按模型提供默认策略。单个 agent 的 `contextPolicy.providerReplay` 优先级高于 host 的模型默认策略。
384
+
385
+ ---
386
+
387
+ ## 9. Token 预算与估算
388
+
389
+ ### 9.1 字段速查
390
+
391
+ | 字段 | 默认 | 含义 | 开放状态 |
392
+ |------|------|------|---------|
393
+ | `budget.maxTokens` | `120000` | 总预算 | ✅ AgentSpec |
394
+ | `budget.reservedForResponse` | `2400` | 留给响应的 token | ✅ AgentSpec |
395
+ | `budget.workingMemoryBudgetPercentage` | `0.70` | 工作记忆占可用预算的比例 | ✅ AgentSpec |
396
+ | `tokenEstimation.encoding` | `'cl100k_base'` | 估算用的 tiktoken encoding 名 | ✅ AgentSpec + runtime |
397
+ | `tokenEstimation.avgCharsPerToken` | `2.0` | tiktoken 不可用或未配置 encoding 时的字符/token 兜底比 | ✅ AgentSpec + runtime |
398
+ | `tokenEstimation.toolCallOverhead` | `50` | 工具调用本身的额外开销估算 | ✅ AgentSpec + runtime |
399
+
400
+ ### 9.2 谁来算 token?
401
+
402
+ > 本文档把"计算 token 的方法"统称为 **tokenizer**。这是一个**总称**——既包括 linnkit 内置的默认 tokenizer(基于 tiktoken + 字节比兜底),也包括 host 注入的任何自定义实现。所有运行期上下文预算决策都通过当前生效的 tokenizer 完成。
403
+
404
+ **linnkit 协议层既定事实**:
405
+
406
+ - linnkit **内置一个默认 tokenizer**(实现:`TokenCalculator` + `tiktoken@^1.0.22` 硬依赖)—— 主路径走 OpenAI 编码族 + CJK 检测;非 OpenAI 模型(Claude / Gemini / DeepSeek)映射到 `cl100k_base` 近似;tiktoken 失败时退到字节比兜底(`avgCharsPerToken`)。
407
+ - runtime 统一通过 `tokenizer.estimateMessage(...)` 估算 message token,预算判断会同时计入基础 message overhead、内容 token、tool call 参数 token 与 `tokenEstimation.toolCallOverhead`。如果 `encoding` 不可用,才回退到 `avgCharsPerToken`。
408
+ - 这个 tokenizer **仅用于 budget 决策**("还能塞多少消息"),**不用于**计费——计费 token 数由 provider 返回的 `usage` 字段决定,host 自己消费。
409
+ - linnkit **不发明跨 provider 统一 token 数协议**——每个 host / agent 决定自己用什么 tokenizer(默认内置 / 调三参数 / 完全替换)。
410
+
411
+ ### 9.3 何时该担心估算不准?
412
+
413
+ | 场景 | 估算精度 | 是否需要担心 |
414
+ |------|---------|------------|
415
+ | host 用 GPT-3.5/4 + 默认 `cl100k_base` | 几乎精确(OpenAI tiktoken 就是这个)| ❌ 不需要 |
416
+ | host 用 GPT-4o + `encoding: 'o200k_base'` | 几乎精确 | ❌ 不需要 |
417
+ | host 用 Claude / Gemini / DeepSeek + 默认 `cl100k_base` | ±10-30% 偏差 | ⚠️ 大多数场景**够用**(budget 有 `reservedForResponse` 安全垫);如果你严格按真实计费做预算 → 需要担心 |
418
+ | host 主要场景是中文 / CJK | `TokenCalculator` 自动按字节比兜底 → 比较准;但 tiktoken 主路径仍按 OpenAI 编码估,CJK 字符占 token 偏高 | ⚠️ 调 `avgCharsPerToken: 1.6 ~ 1.8` 提升精度 |
419
+ | host 自动化复杂任务(步骤多、工具链长,单次 run 几十万 token)| 默认估算累积偏差可能放大 | ⚠️ 考虑注入 `TokenizerPort`(已可用) |
420
+ | host 严格按计费 token 数 = 预算 token 数(无安全垫)| 默认估算不够 | ❌ **必须**注入 `TokenizerPort`(已可用) |
421
+
422
+ ### 9.4 用自定义 tokenizer 替换默认实现(`TokenizerPort` 注入)
423
+
424
+ `TokenizerPort` 是**所有 tokenizer 的协议接口**——linnkit 默认 tokenizer (`DefaultTokenizerPort`) 实现它,host 想替换默认实现时也实现它即可。
425
+
426
+ **何时该替换默认 tokenizer**:如果你需要**真实**的 Anthropic / Gemini tokenizer(不接受 OpenAI 编码近似),或者你接的是 linnkit 不认识的私有模型,就实现 `TokenizerPort` 注入到装配链路。
427
+
428
+ #### 9.4.1 接入点 · `ContextManagerBaseOptions.tokenizer`
429
+
430
+ `tokenizer` 注入点在 **context-manager 装配链路**(不是 `GraphExecutor`——GraphExecutor 不负责上下文构建,真正做 token budget / trimming 的是 context-manager)。常见三个装配入口都接受 `tokenizer` 选项:
431
+
432
+ | 装配入口 | 字段 | 适用场景 |
433
+ |---------|------|---------|
434
+ | `new AgentContextManager({ ..., tokenizer, tokenizerModelId })` | ✅ | host 直接装配 agent context manager |
435
+ | `new AgentMessageOrchestrator({ ..., tokenizer })` | ✅ | host 装配 orchestrator(orchestrator 透传给底层 context-manager)|
436
+ | `new ChatContextManager({ ..., tokenizer })` / `new ChatMessageOrchestrator({ ..., tokenizer })` | ✅ | chat profile |
437
+
438
+ 参考实现:`defaultGraphExecutorContextBuilder.ts` 已经把可选 `tokenizer` 依赖透传到 `AgentMessageOrchestrator` / `ChatMessageOrchestrator` 装配,外部接入方可以照抄。
439
+
440
+ #### 9.4.2 完整示例
441
+
442
+ ```ts
443
+ import type { TokenizerPort, LlmRequestMessage } from '@linnlabs/linnkit/ports';
444
+ import { agentOrchestration } from '@linnlabs/linnkit/context-manager';
445
+
446
+ class MyMultiProviderTokenizer implements TokenizerPort {
447
+ estimateText(text: string, modelId?: string): number {
448
+ if (modelId?.startsWith('claude-')) {
449
+ return runClaudeTokenizer(text); // host 自接 Anthropic 官方
450
+ }
451
+ if (modelId?.startsWith('gemini-')) {
452
+ return runGeminiTokenizer(text);
453
+ }
454
+ return runTiktoken(text, 'o200k_base');
455
+ }
456
+ estimateMessage(message: LlmRequestMessage, modelId?: string): number {
457
+ // 必须包含 message overhead + tool_call overhead + tool_call_id 层级
458
+ // 否则 budget 决策会系统性低估
459
+ // ...
460
+ }
461
+ }
462
+
463
+ const orchestrator = new agentOrchestration.AgentMessageOrchestrator({
464
+ tokenBudget,
465
+ processing,
466
+ taskResolver,
467
+ providerRegistry,
468
+ tokenizer: new MyMultiProviderTokenizer(),
469
+ });
470
+ ```
471
+
472
+ 如果你的 host 有自己的 `GraphExecutorContextBuilder`,就在创建 `AgentMessageOrchestrator` / `ChatMessageOrchestrator` 的地方把同一个 `tokenizer` 透传进去。`GraphExecutor` 本身不构建上下文,因此不接收 tokenizer。
473
+
474
+ #### 9.4.3 `tokenizerModelId` 字段
475
+
476
+ `ContextManagerBaseOptions` 还提供 `tokenizerModelId?: string`——host 在装配期声明"这个 context-manager 接的是哪个模型",linnkit 内部会把它透传给 `tokenizer.estimateMessage(message, modelId)` / `tokenizer.estimateText(text, modelId)`。当 host 在多 agent 场景下用不同模型时,这个字段让自定义 tokenizer 能精准路由。
477
+
478
+ #### 9.4.4 与 `tokenEstimation` 三参数的关系
479
+
480
+ - **host 注入的自定义 tokenizer 优先级最高**:注入后,`tokenEstimation` 三参数不再影响预算决策。
481
+ - **`tokenEstimation` 仅服务"用默认 tokenizer"的 host**:它是 `DefaultTokenizerPort` 的配置点(调 encoding / 字节比 / 工具开销)。默认 tokenizer 仍响应 `tokenEstimation` config 的运行时更新。
482
+ - **同时配置不会冲突**:linnkit 内部维护 `hasCustomTokenizer` 标志,注入后即认定 host 完全接管 token 估算。
483
+
484
+ #### 9.4.5 C12 不变量 · 协议守门
485
+
486
+ `testkit/context-harness` 提供 `C12_HOST_TOKENIZER_DRIVES_BUDGET` 严格不变量:
487
+
488
+ - 校验 `message-decision.tokens` 必须等于 host 注入的 `TokenizerPort.estimateMessage(...)`
489
+ - 校验 `trace.finalTokens` 必须等于 host tokenizer 对 finalMessages 的总估算
490
+ - **只在测试上下文传入 tokenizer 时启用**——不打扰沿用默认 tokenizer 的旧测试
491
+
492
+ 这条不变量是 host 集成自定义 tokenizer 后**最有价值**的回归测试守门——它能在协议层证明"host 的 tokenizer 真的在驱动 budget 决策",而不是被默认实现静默兜底。
493
+
494
+ #### 9.4.6 testkit · `createMockTokenizerPort`
495
+
496
+ 写测试时不需要自接真实 tokenizer。testkit 提供 `createMockTokenizerPort()`,让你能注入"每条 message 返回固定 N token"的 mock,方便验证 overflow / trimming / contextTrace 行为:
497
+
498
+ ```ts
499
+ import { createMockTokenizerPort } from '@linnlabs/linnkit/testkit';
500
+
501
+ const mockTokenizer = createMockTokenizerPort({ tokensPerMessage: 100 });
502
+
503
+ // 注入到测试 harness,验证当总 token 超过 budget.maxTokens 时的 trimming 行为
504
+ ```
505
+
506
+ ### 9.5 边界提醒
507
+
508
+ - **tokenizer 是装配期一次性注入**——无论 host 注入还是用默认实现,运行时不支持热替换(避免 budget 决策因为 tokenizer 抖动而失真)。
509
+ - **replay 场景**:未来 Replay SDK 重演 run 时,host 需要重新注入对应的 tokenizer——这是 host 责任,不是 framework 协议负担。
510
+ - **自定义 tokenizer 必须保持与默认实现一致的"额外开销层级"**(message overhead / tool_call overhead / tool_call_id),否则 budget 决策会偏低、消息塞超。
511
+
512
+ ---
513
+
514
+ ## 10. 出关:`formatAgentLlmMessages`
515
+
516
+ 把所有 AiMessage 翻译成最终 LLM 协议 wire 格式(具体调哪个 provider 这里无关);fence 消息走 host 提供的 `formatter` 包成字面标签;`tool_calls` / `tool_output` 按 LLM 协议挂正确的 role。
517
+
518
+ **配置面**:
519
+ - fence formatter(host 决定围栏字面长什么样)
520
+ - LLM provider 自己的 codec(OpenAI Chat / Anthropic Messages / DeepSeek 等)—— 详见 [`llm-provider.md`](./llm-provider.md)
521
+
522
+ ---
523
+
524
+ ## 10.5 ContextTrace:解释这次上下文为什么长这样
525
+
526
+ `contextTrace` 是本次 context build 的机器可读旁路记录。它不进入 LLM messages、不落成历史事实,只跟随 `ContextBuildResult.contextTrace` 返回,用来解释 effective policy、每个 provider 的 token 增减、以及每条消息最终被保留还是裁掉。
527
+
528
+ **配置面**:
529
+
530
+ ```ts
531
+ contextPolicy: {
532
+ profileId: 'agent',
533
+ contextTrace: {
534
+ enabled: true,
535
+ includeMessageIds: true,
536
+ includeTokenBreakdown: true,
537
+ maxTraceEvents: 200,
538
+ },
539
+ }
540
+ ```
541
+
542
+ **输出里会看到**:
543
+
544
+ - `effectivePolicy`:本次实际生效的 `contextPolicy`(已经合并 framework 默认、host fallback、agent spec)。
545
+ - `provider` 事件:每个 provider 执行前后保留消息数、token delta、剩余预算、命中的策略名。
546
+ - `message-decision` 事件:每条候选消息的 `keep/drop` 结果、阶段、token、原因;`includeMessageIds=false` 时不会带 message id。
547
+ - `overflowed`:trace 事件超过 `maxTraceEvents` 时为 `true`,防止观测数据反过来膨胀。
548
+ - GraphExecutor 会把 `contextTrace` 从 context builder 透传到 context audit record;runtime-kernel 只按 `unknown` 透传,不反向依赖 context-manager 类型。
549
+
550
+ **边界**:ContextTrace 不是 DevTools,也不是 PromptTrace 可视化;它只提供最小可观测闭环。跨 run prompt diff、图形化时间线、长期审计落库属于阶段 2。
551
+
552
+ ---
553
+
554
+ ## 11. 当前开放面 vs 未开放面
555
+
556
+ ### ✅ 已通过 AgentSpec 协议化开放,且 runtime 已接线
557
+
558
+ - `budget.maxTokens` / `reservedForResponse` / `workingMemoryBudgetPercentage`
559
+ - `toolHistory.{strategy, keepLatestToolPairs, keepLatestRuns, maxInteractionGroups, overflowStrategy, maxPairTokens, maxOutputSummaryTokens}`
560
+ - `toolOutput.observationGovernance.{enabled, maxChars, maxLines}`
561
+ - `providerReplay.{provider, requiresReasoningDetailsForToolReplay, missingSidecarBehavior}`
562
+ - `summarization.{triggerThreshold, budgetPercentage, oldestMessagesPercentage, agentId, failureBehavior}`
563
+ - `MustKeepPolicy.{alwaysKeepTypes, alwaysKeepFenceKinds, truncationRules}`
564
+ - `workingMemory.{maxRecentToolInteractions, minToolInteractionsToKeep, toolPairingSearchRange}`
565
+ - `checkpoint.{keepPairsBefore, triggerToolName}`
566
+ - `reasoningRetention.keepLatestThoughts`
567
+ - `tokenEstimation.{encoding, avgCharsPerToken, toolCallOverhead}`
568
+ - `systemReminder.{enabledRuleIds, disabledRuleIds, thresholds, extraRules}`
569
+ - `contextTrace.{enabled, includeMessageIds, includeTokenBreakdown, maxTraceEvents}`
570
+ - `defineContextPolicy()` 可补齐 12 大分组默认值,便于外部接入方生成完整策略
571
+ - fence 注册(`FenceRegistry`,host 自由扩展)
572
+
573
+ ### 🔴 当前不开放(未来可能开放)
574
+
575
+ - Preprocessor pipeline **顺序与白名单**(host 现在只能在默认 pipeline 之外追加,不能改默认顺序)
576
+
577
+ ### 🚫 协议性不开放(不计划开放)
578
+
579
+ - system reminder **持久化进 history**(违反 reminder 协议本质——若需持久化请走 fence `lifetime: 'persisted'`)
580
+ - ContextProvider 三阶段顺序(核心 → 工作记忆 → 摘要)
581
+ - 工具压缩的 P1-P4 优先级数字
582
+ - `tool_calls` / `tool_output` 配对不变量
583
+
584
+ ---
585
+
586
+ ## 12. 想动哪一层
587
+
588
+ | 你想做什么 | 应该动哪里 | 验证方式 |
589
+ |------------|------------|----------|
590
+ | 控制总预算 / 预留响应 token | `contextPolicy.budget` | `ContextTrace.effectivePolicy` + final token usage |
591
+ | 控制工具历史保留方式 | `contextPolicy.toolHistory` | `ContextTrace.message-decision` 中 tool_calls / tool_output 的 keep/drop |
592
+ | 控制摘要何时触发、用哪个摘要 agent | `contextPolicy.summarization` | summary event + `ContextTrace.provider` 中 summarization provider token delta |
593
+ | 必保留某类 host 上下文 | `contextPolicy.mustKeep.alwaysKeepFenceKinds` | fence 对应 message 的 decision 为 `kept_by_CORE_CONTEXT` |
594
+ | 调整工作记忆工具组数量 | `contextPolicy.workingMemory` | working-memory provider 后的 kept count / token delta |
595
+ | 改 checkpoint 工具名或保留窗口 | `contextPolicy.checkpoint` | checkpoint provider 策略命中 + GraphExecutor step-reset 行为 |
596
+ | 控制 thought 保留数量 | `contextPolicy.reasoningRetention.keepLatestThoughts` | thought message 的 keep/drop 数量 |
597
+ | 控制工具 observation 执行期预览阈值 | `contextPolicy.toolOutput.observationGovernance` | `tool_output_store.blob_id` 是否生成 + tool node 单测 |
598
+ | 控制 provider sidecar 缺失时的历史工具回放 | `contextPolicy.providerReplay` | `ToolReplayProtocolGuardPreprocessor` 是否降级 / 标记 |
599
+ | 调整 token 估算口径 | `contextPolicy.tokenEstimation` | provider token delta 曲线变化 |
600
+ | 自定义 transient system reminder | `contextPolicy.systemReminder` + registry | `systemReminderHitRuleIds` + final LLM input |
601
+ | 看清最终 token 决策 | `contextPolicy.contextTrace.enabled=true` | `ContextBuildResult.contextTrace` |
602
+
603
+ ---
604
+
605
+ ## 13. 声明你的第一个 system reminder
606
+
607
+ SystemReminder 是**当前 tick 的瞬态提醒**,不会进入历史。内置规则可通过 `enabledRuleIds` / `disabledRuleIds` / `thresholds` 控制;host 自定义规则走 trigger/template 注册表。
608
+
609
+ ```ts
610
+ contextPolicy: {
611
+ profileId: 'agent',
612
+ systemReminder: {
613
+ enabledRuleIds: ['last_steps_hint', 'context_budget_warning'],
614
+ thresholds: {
615
+ lastStepsHintThreshold: 2,
616
+ budgetWarningRatio: 0.85,
617
+ },
618
+ },
619
+ }
620
+ ```
621
+
622
+ 自定义规则示意:
623
+
624
+ ```ts
625
+ import { systemReminder } from '@linnlabs/linnkit/runtime-kernel';
626
+
627
+ systemReminder.defaultSystemReminderRegistry.registerContentTemplate(
628
+ 'memoryDensityWarning',
629
+ (_ctx, args) => `请先整理 ${String(args.resourceName ?? 'memory')} 的关键信息,再继续调用工具。`,
630
+ );
631
+
632
+ contextPolicy: {
633
+ profileId: 'agent',
634
+ systemReminder: {
635
+ extraRules: [
636
+ {
637
+ id: 'memory-density-warning',
638
+ trigger: { kind: 'tool-call-streak', threshold: 5 },
639
+ contentTemplate: 'memoryDensityWarning',
640
+ contentArgs: { resourceName: 'memory_recall' },
641
+ },
642
+ ],
643
+ },
644
+ }
645
+ ```
646
+
647
+ **注意**:
648
+
649
+ - 不要把 reminder 持久化进 history;需要持久化提示时走 fence。
650
+ - spec 里只放 trigger/template ID 和可序列化参数,不放函数。