@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.
- package/CHANGELOG.md +84 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/README.zh-CN.md +182 -0
- package/dist/agent-invocation-BHcNfrBV.d.cts +30 -0
- package/dist/agent-invocation-BznDaXDs.d.ts +30 -0
- package/dist/agentEvents-DEB7Fy_J.d.cts +81 -0
- package/dist/agentEvents-DEB7Fy_J.d.ts +81 -0
- package/dist/agentSpec-EkmviZjy.d.cts +2621 -0
- package/dist/agentSpec-EkmviZjy.d.ts +2621 -0
- package/dist/ai-engine.types-BpeU_XQG.d.cts +158 -0
- package/dist/ai-engine.types-vZRnQcJa.d.ts +158 -0
- package/dist/audit-BaRUGaqv.d.cts +307 -0
- package/dist/audit-BaRUGaqv.d.ts +307 -0
- package/dist/audit-CtcfART1.d.ts +33 -0
- package/dist/audit-LeOrm2hX.d.cts +33 -0
- package/dist/checkpointMarker-DAI3wUQu.d.cts +8 -0
- package/dist/checkpointMarker-DAI3wUQu.d.ts +8 -0
- package/dist/cli.cjs +8028 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +8025 -0
- package/dist/cli.js.map +1 -0
- package/dist/context-manager.cjs +8704 -0
- package/dist/context-manager.cjs.map +1 -0
- package/dist/context-manager.d.cts +2190 -0
- package/dist/context-manager.d.ts +2190 -0
- package/dist/context-manager.js +8650 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/context-trace-DRi5M4lX.d.ts +239 -0
- package/dist/context-trace-HE2qY5Q-.d.cts +239 -0
- package/dist/contracts.cjs +1333 -0
- package/dist/contracts.cjs.map +1 -0
- package/dist/contracts.d.cts +8 -0
- package/dist/contracts.d.ts +8 -0
- package/dist/contracts.js +1214 -0
- package/dist/contracts.js.map +1 -0
- package/dist/defaultGraphExecutor-BBswR8wn.d.ts +624 -0
- package/dist/defaultGraphExecutor-BIjJj7WF.d.cts +624 -0
- package/dist/execution-CAIypb41.d.cts +129 -0
- package/dist/execution-CAIypb41.d.ts +129 -0
- package/dist/index-CHqwkvGp.d.ts +149 -0
- package/dist/index-CJeWHopy.d.ts +584 -0
- package/dist/index-Cm-JbzTH.d.cts +1450 -0
- package/dist/index-Cvr23YCl.d.cts +23 -0
- package/dist/index-DDzuSb0n.d.ts +23 -0
- package/dist/index-DO4dQgf2.d.cts +584 -0
- package/dist/index-DRBWi1fy.d.ts +1450 -0
- package/dist/index-Dl5PLgAv.d.cts +149 -0
- package/dist/index.cjs +9577 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +89 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +9563 -0
- package/dist/index.js.map +1 -0
- package/dist/messages-XthmnHZ3.d.cts +8007 -0
- package/dist/messages-XthmnHZ3.d.ts +8007 -0
- package/dist/ports-DaatKJXp.d.cts +90 -0
- package/dist/ports-DnLuKfpE.d.ts +90 -0
- package/dist/ports.cjs +4 -0
- package/dist/ports.cjs.map +1 -0
- package/dist/ports.d.cts +7 -0
- package/dist/ports.d.ts +7 -0
- package/dist/ports.js +3 -0
- package/dist/ports.js.map +1 -0
- package/dist/quickstart.cjs +7697 -0
- package/dist/quickstart.cjs.map +1 -0
- package/dist/quickstart.d.cts +24 -0
- package/dist/quickstart.d.ts +24 -0
- package/dist/quickstart.js +7691 -0
- package/dist/quickstart.js.map +1 -0
- package/dist/runAgent-CPj_9e58.d.ts +88 -0
- package/dist/runAgent-HYKlXbVr.d.cts +88 -0
- package/dist/runHandle-CyXvzgzk.d.ts +239 -0
- package/dist/runHandle-D3gPsD7B.d.cts +239 -0
- package/dist/runtime-kernel/events.cjs +1485 -0
- package/dist/runtime-kernel/events.cjs.map +1 -0
- package/dist/runtime-kernel/events.d.cts +8 -0
- package/dist/runtime-kernel/events.d.ts +8 -0
- package/dist/runtime-kernel/events.js +1475 -0
- package/dist/runtime-kernel/events.js.map +1 -0
- package/dist/runtime-kernel.cjs +8656 -0
- package/dist/runtime-kernel.cjs.map +1 -0
- package/dist/runtime-kernel.d.cts +19 -0
- package/dist/runtime-kernel.d.ts +19 -0
- package/dist/runtime-kernel.js +8568 -0
- package/dist/runtime-kernel.js.map +1 -0
- package/dist/sse-vPyrOPa0.d.cts +1687 -0
- package/dist/sse-vPyrOPa0.d.ts +1687 -0
- package/dist/testkit.cjs +10613 -0
- package/dist/testkit.cjs.map +1 -0
- package/dist/testkit.d.cts +284 -0
- package/dist/testkit.d.ts +284 -0
- package/dist/testkit.js +10593 -0
- package/dist/testkit.js.map +1 -0
- package/dist/todo-B1PmDlp3.d.cts +2253 -0
- package/dist/todo-B1PmDlp3.d.ts +2253 -0
- package/dist/tokenizer-DFL4I7-I.d.ts +28 -0
- package/dist/tokenizer-DH_JXv-H.d.cts +28 -0
- package/dist/toolContracts-Blll0241.d.ts +463 -0
- package/dist/toolContracts-CLkQmhTG.d.cts +463 -0
- package/docs/README.md +76 -0
- package/docs/integration/01-installation.md +94 -0
- package/docs/integration/02-quickstart.md +104 -0
- package/docs/integration/README.md +223 -0
- package/docs/integration/agent-registration-guide.md +330 -0
- package/docs/integration/audit.md +64 -0
- package/docs/integration/child-runs.md +87 -0
- package/docs/integration/constraints-and-pitfalls.md +87 -0
- package/docs/integration/context-engineering.md +650 -0
- package/docs/integration/context-fences.md +289 -0
- package/docs/integration/glossary.md +69 -0
- package/docs/integration/llm-provider.md +76 -0
- package/docs/integration/persistence.md +44 -0
- package/docs/integration/realtime.md +76 -0
- package/docs/integration/run-supervisor.md +69 -0
- package/docs/integration/telemetry.md +48 -0
- package/docs/integration/testing.md +95 -0
- package/docs/integration/tool-development-guide.md +362 -0
- package/docs/integration/tool-history.md +202 -0
- package/docs/integration/tools.md +188 -0
- package/package.json +115 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# Context Engineering · Fence 注册 + 注入 ⭐
|
|
2
|
+
|
|
3
|
+
> **What** · 上下文围栏(fence)注册与注入 —— linnkit 一等接入面,host 把"项目状态 / 当前文件 / 引用段落 / 长记忆"等结构化上下文喂给 LLM 都走这里。
|
|
4
|
+
> **When to read** · 要把 host 状态喂给 agent;要 `mustKeep` 关键信息不被裁;要做"按生命周期管理上下文"(current-turn / persisted / boot-only)。
|
|
5
|
+
> **Prerequisites** · [`context-engineering.md`](./context-engineering.md) ⭐(先理解 `contextPolicy` 总体结构)。
|
|
6
|
+
> **Key exports** · `FenceRegistry` / `FenceDescriptor` / `FenceInjection` / `MustKeepPolicy` / `FenceLifetimePreprocessor` from `@linnlabs/linnkit/context-manager`。
|
|
7
|
+
> **Related** · [`context-engineering.md`](./context-engineering.md) ⭐ · [`agent-registration-guide.md`](./agent-registration-guide.md) ⭐
|
|
8
|
+
|
|
9
|
+
> **第一周必读**。如果你的产品需要把不同来源的上下文注入到 LLM 不同位置("项目元信息塞 system 之后"、"被引用的段落塞当前用户输入之前"、"长记忆只塞当前轮"),**这是 linnkit 的一等接入面**。
|
|
10
|
+
>
|
|
11
|
+
> **不要**自己在 system prompt 里手工拼 `<my_tag>...</my_tag>`——会被 boundary guard 拦下,且生命周期治理失控。
|
|
12
|
+
|
|
13
|
+
## 1. 为什么有 fence 机制
|
|
14
|
+
|
|
15
|
+
linnkit 设计原则:
|
|
16
|
+
|
|
17
|
+
- **framework 不知道任何 host 产品语义**——`document_fragment` / `project_context` / `<additional_context>` 这种字面绝不出现在 framework 源码里
|
|
18
|
+
- **任意 host 都能注册自己的围栏家族**——例如 `<additional_context>`、`<memory-context>`、`<system-event>`、`<file_context>`,都通过同一套机制插入,不需要任何 host 改 framework 源码
|
|
19
|
+
- **注入消息有稳定协议载体**——`AiMessage.type = 'context_injection'` 是唯一通用类型,`metadata.fenceKind` 表达开放的 host kind
|
|
20
|
+
|
|
21
|
+
正确的做法:把每类上下文声明成一个"围栏家族"(fence kind),通过 `FenceRegistry` 注册,运行时由 `BaseAgentTask` 把 host 请求里的 `fences[]` 自动展开成 `context_injection` 消息,按 `placement` 落到正确位置;旧轮 `lifetime: 'turn-only'` 的注入由 `FenceLifetimePreprocessor` 自动剥离。
|
|
22
|
+
|
|
23
|
+
## 2. 概念三元组
|
|
24
|
+
|
|
25
|
+
| 概念 | 类型 | 谁产 | 谁消 |
|
|
26
|
+
|---|---|---|---|
|
|
27
|
+
| `FenceDescriptor` | 来自 `@linnlabs/linnkit/context-manager` | host 启动时声明(每类一个)| linnkit `MessageFormatter` / `FenceLifetimePreprocessor` / `MustKeepPolicy` |
|
|
28
|
+
| `FenceInjection` | 来自 `@linnlabs/linnkit/context-manager` | host 请求适配层每轮产 | linnkit `BaseAgentTask` 展开为 `context_injection` 消息 |
|
|
29
|
+
| `context_injection` 消息 | 来自 `@linnlabs/linnkit/contracts` 的 `AiMessage` 一种 type | linnkit 自动产 | 整条 context pipeline |
|
|
30
|
+
|
|
31
|
+
## 3. 注册一个 fence 家族(host 启动时一次)
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// app-hosts/your-app/context/agent/registerFences.ts
|
|
35
|
+
import {
|
|
36
|
+
createFenceRegistry,
|
|
37
|
+
type FenceDescriptor,
|
|
38
|
+
type FenceRegistry,
|
|
39
|
+
} from '@linnlabs/linnkit/context-manager';
|
|
40
|
+
|
|
41
|
+
export function createMyFenceRegistry(): FenceRegistry {
|
|
42
|
+
return createFenceRegistry(createMyFenceDescriptors());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createMyFenceDescriptors(): FenceDescriptor[] {
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
kind: 'memory-context', // host 自定义 kebab-case
|
|
49
|
+
llmRole: 'user', // 物理 role(注入时挂到 user 还是 system)
|
|
50
|
+
placement: 'before-current-user', // 在 system 后 / 当前 user 前 / 当前 user 后 / 上一组 tool result 后
|
|
51
|
+
lifetime: 'turn-only', // 'turn-only' 只在本轮;'persisted' 进 history
|
|
52
|
+
maxBudgetFraction: 0.2, // 可选:按总 token 预算上限
|
|
53
|
+
formatter: (content, attrs) =>
|
|
54
|
+
`<memory-context source="${attrs.source ?? 'unknown'}">\n${content}\n</memory-context>`,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
kind: 'system-event',
|
|
58
|
+
llmRole: 'system',
|
|
59
|
+
placement: 'after-system',
|
|
60
|
+
lifetime: 'persisted',
|
|
61
|
+
mustKeep: true, // 自动 must-keep(不会被 working memory 裁掉)
|
|
62
|
+
formatter: (content) => `<system-event>\n${content}\n</system-event>`,
|
|
63
|
+
},
|
|
64
|
+
// 想要多少类就声明多少类
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const myFenceRegistry = createMyFenceRegistry();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**约束**:
|
|
72
|
+
|
|
73
|
+
- `kind` 必须 kebab-case(`/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/`)
|
|
74
|
+
- `placement` 当前枚举:`'after-system'` / `'before-current-user'` / `'after-current-user'` / `'after-last-tool-result'`
|
|
75
|
+
- 同一个 `kind` 在同一个 registry 不能重复 register
|
|
76
|
+
- `maxBudgetFraction` 必须落在 `(0, 1]`
|
|
77
|
+
|
|
78
|
+
## 4. 写一个 host 适配器:把请求字段转成 `FenceInjection[]`
|
|
79
|
+
|
|
80
|
+
这是把 host 自己的产品语义("项目名"、"被选中的段落"、"用户引用的句子"等)翻成通用 fence 注入的关键一层。
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// app-hosts/your-app/context/agent/createMyFenceInjections.ts
|
|
84
|
+
import type { FenceInjection } from '@linnlabs/linnkit/context-manager';
|
|
85
|
+
import type { MyAgentInvokeRequest } from './contracts';
|
|
86
|
+
|
|
87
|
+
export function createMyFenceInjections(request: MyAgentInvokeRequest): FenceInjection[] {
|
|
88
|
+
const fences: FenceInjection[] = [];
|
|
89
|
+
|
|
90
|
+
if (request.memorySnapshot?.trim()) {
|
|
91
|
+
fences.push({
|
|
92
|
+
kind: 'memory-context',
|
|
93
|
+
content: request.memorySnapshot,
|
|
94
|
+
attrs: { source: request.memorySource ?? 'memory-store' },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (request.systemEvent) {
|
|
99
|
+
fences.push({ kind: 'system-event', content: request.systemEvent });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return [...fences, ...(request.fences ?? [])];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function withMyFenceInjections(request: MyAgentInvokeRequest): MyAgentInvokeRequest {
|
|
106
|
+
return { ...request, fences: createMyFenceInjections(request) };
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**关键点**:
|
|
111
|
+
|
|
112
|
+
- 适配器**不直接拼字符串**——只产 `{ kind, content, attrs }` 三元组。字符串拼接由 `FenceDescriptor.formatter` 统一负责
|
|
113
|
+
- 必须保留 `request.fences` 旧值(外部已经显式塞过的注入不被吞掉)
|
|
114
|
+
- 不要在这里写 `<memory-context>` 字面 tag——tag 名归 fence descriptor 拥有,否则两边各拼一次会重复
|
|
115
|
+
|
|
116
|
+
## 5. 把 registry 接到运行时(一处装配,三处消费)
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
// app-hosts/your-app/adapters/context-injection/myContextBuilder.ts
|
|
120
|
+
import {
|
|
121
|
+
formatAgentLlmMessages,
|
|
122
|
+
agentOrchestration,
|
|
123
|
+
contextPolicyToProviderOptions,
|
|
124
|
+
mergeContextPolicy,
|
|
125
|
+
} from '@linnlabs/linnkit/context-manager';
|
|
126
|
+
import { myFenceRegistry } from '../../context/agent/registerFences';
|
|
127
|
+
import { withMyFenceInjections } from '../../context/agent/createMyFenceInjections';
|
|
128
|
+
|
|
129
|
+
const hostContextPolicyFallback = {
|
|
130
|
+
mustKeep: {
|
|
131
|
+
alwaysKeepFenceKinds: ['system-event'], // 注:只列那些"事件本身就是事实"的 kind
|
|
132
|
+
truncationRules: [
|
|
133
|
+
{ fenceKind: 'memory-context', maxBudgetFraction: 0.2, strategyName: 'memory-truncate' },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const orchestrator = new agentOrchestration.AgentMessageOrchestrator({
|
|
139
|
+
tokenBudget: { maxTokens: 32_000, reservedForResponse: 4_000 },
|
|
140
|
+
processing: { debugMode: false, preserveMetadata: true },
|
|
141
|
+
taskResolver: myAgentTaskResolver,
|
|
142
|
+
providerRegistry: myProviderRegistry,
|
|
143
|
+
fenceRegistry: myFenceRegistry, // ← 关键:让 BaseAgentTask 认识 host 的 fence
|
|
144
|
+
// host 可以保留模型级 provider replay 默认;单个 agent 可用 contextPolicy.providerReplay 覆盖。
|
|
145
|
+
resolveToolReplayProtocolPolicy: ({ modelId }) => myToolReplayPolicy(modelId),
|
|
146
|
+
resolveContextPolicy: request => mergeContextPolicy({
|
|
147
|
+
hostFallback: hostContextPolicyFallback,
|
|
148
|
+
agentSpec: myAgentRegistry.get(request.promptKey)?.config?.contextPolicy,
|
|
149
|
+
}),
|
|
150
|
+
createProviderRegistry: ({ contextPolicy, contextBuilderConfig }) =>
|
|
151
|
+
createMyProviderRegistry({
|
|
152
|
+
customConfig: contextBuilderConfig,
|
|
153
|
+
providerOptions: contextPolicyToProviderOptions(contextPolicy),
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 调 orchestrator 之前,把 host 字段转成 fences
|
|
158
|
+
const requestWithFences = withMyFenceInjections(request);
|
|
159
|
+
|
|
160
|
+
const processingResult = await orchestrator.processAgentConversation(
|
|
161
|
+
requestWithFences,
|
|
162
|
+
history,
|
|
163
|
+
toolManager,
|
|
164
|
+
summarizationCallbacks,
|
|
165
|
+
{ generate },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// 出关到 LLM 时,fence formatter 会被调用,每个 context_injection 消息变成具体 tag
|
|
169
|
+
const llmMessages = formatAgentLlmMessages(processingResult.messages, {
|
|
170
|
+
fenceRegistry: myFenceRegistry,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`FenceLifetimePreprocessor` 通常已经被 `createDefaultAgentPreprocessorPipeline` 内置(orchestrator 内部根据 `fenceRegistry` 自动接好);你**不需要手动 new**,只要保证 orchestrator 拿到了同一个 `fenceRegistry` 实例。
|
|
175
|
+
|
|
176
|
+
如果你完全自定义了 preprocessor pipeline,那 `FenceLifetimePreprocessor` 要从 `@linnlabs/linnkit/context-manager` 导入并手动加进去(构造参数:`{ fenceRegistry }`)。
|
|
177
|
+
|
|
178
|
+
## 6. 配 MustKeepPolicy(控制 working memory 裁剪)
|
|
179
|
+
|
|
180
|
+
`AgentCoreContextProvider` 通过 `contextPolicy.mustKeep` 决定哪些消息一律不被裁。它有两类输入:
|
|
181
|
+
|
|
182
|
+
1. `alwaysKeepTypes`:按 `AiMessage.type` 列表(`'system_prompt' | 'user_input' | ...`)。默认值 `DEFAULT_MUST_KEEP_POLICY`
|
|
183
|
+
2. `alwaysKeepFenceKinds`:按 fence kind 列表(host 注入的 `metadata.fenceKind`)
|
|
184
|
+
|
|
185
|
+
**搭配规则**(很重要):
|
|
186
|
+
|
|
187
|
+
- `lifetime: 'persisted'` 的 fence kind,多半也想 must-keep → 加进 `alwaysKeepFenceKinds`
|
|
188
|
+
- `lifetime: 'turn-only'` 的 fence kind,本身就只在本轮,**不要**加进 `alwaysKeepFenceKinds`
|
|
189
|
+
- 想限量截断(不丢但只保留预算的 X%):用 `truncationRules`
|
|
190
|
+
|
|
191
|
+
推荐把全局业务默认放进 host fallback,把单个 agent 的差异写在 `AgentDefinition.config.contextPolicy`:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// host fallback:所有 agent 都默认保留 system-event
|
|
195
|
+
const hostContextPolicyFallback = {
|
|
196
|
+
mustKeep: {
|
|
197
|
+
alwaysKeepFenceKinds: ['system-event'],
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// 单个 agent:额外把 memory-context 限量保留
|
|
202
|
+
config: {
|
|
203
|
+
contextPolicy: {
|
|
204
|
+
profileId: 'agent',
|
|
205
|
+
mustKeep: {
|
|
206
|
+
alwaysKeepFenceKinds: ['system-event', 'memory-context'],
|
|
207
|
+
truncationRules: [
|
|
208
|
+
{ fenceKind: 'memory-context', maxBudgetFraction: 0.2, strategyName: 'memory-truncate' },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
开启 `contextTrace.enabled` 后,你可以在 `ContextBuildResult.contextTrace.events` 里看到 fence 对应消息为什么被 `kept_by_CORE_CONTEXT` 或被截断。
|
|
216
|
+
|
|
217
|
+
## 7. Fence 消费的全链路一图
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
host invoke request (含 host 业务字段)
|
|
221
|
+
│
|
|
222
|
+
▼
|
|
223
|
+
withMyFenceInjections() ← 你写的适配
|
|
224
|
+
│ request.fences: FenceInjection[] = [{ kind, content, attrs }, ...]
|
|
225
|
+
▼
|
|
226
|
+
AgentMessageOrchestrator ← linnkit
|
|
227
|
+
│ · BaseAgentTask 展开为 AiMessage(type='context_injection', metadata.fenceKind=...)
|
|
228
|
+
│ · FenceLifetimePreprocessor 剥离旧轮 turn-only 注入
|
|
229
|
+
│ · AgentCoreContextProvider 按 MustKeepPolicy 决定 working memory 是否裁掉
|
|
230
|
+
▼
|
|
231
|
+
formatAgentLlmMessages(..., { fenceRegistry })
|
|
232
|
+
│ · 找到 metadata.fenceKind → registry.get(kind).formatter(content, attrs)
|
|
233
|
+
│ · 出关成具体 LLM messages(system / user 各按 llmRole)
|
|
234
|
+
▼
|
|
235
|
+
AgentAiEngine.chatCompletionStream(llmMessages, ...)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 8. 你不要做的
|
|
239
|
+
|
|
240
|
+
- 不要把 `<my_tag>...</my_tag>` 写进 system prompt 字符串拼装(会绕过 fence lifetime / must-keep 治理)
|
|
241
|
+
- 不要在不同链路用两个不同的 `FenceRegistry`(注册侧和 formatter 侧必须是同一个实例)
|
|
242
|
+
- 不要继续借用 `document_fragment` / `context_before` / `context_after` 这些 legacy type 表达新的产品注入。它们是迁移期兼容字段,host 一律转成 fence 注入
|
|
243
|
+
- 不要把 fence 概念漏到 system prompt 文案里去——fence kind 是 host-internal 命名,对 LLM 不可见;LLM 只看 formatter 输出的标签
|
|
244
|
+
|
|
245
|
+
## 9. 最小验证
|
|
246
|
+
|
|
247
|
+
- 单测 1:注册 fence → `BaseAgentTask` 能展开成 `context_injection` 消息(断言"3 类 fence 注入后,最终 LLM messages 第 N 条是 system 角色 + 包含 `<my_tag>`")
|
|
248
|
+
- 单测 2:`lifetime: 'turn-only'` 的 fence 在 history 里能被自动剥离
|
|
249
|
+
- 单测 3:`mustKeep` 或 `alwaysKeepFenceKinds` 列出的 fence 在 working memory 抽稀时不被裁
|
|
250
|
+
|
|
251
|
+
## 10. 设计原理(深度参考)
|
|
252
|
+
|
|
253
|
+
如果你想理解 fence 机制为什么这么设计,下面几条核心决策可以解释绝大部分疑惑:
|
|
254
|
+
|
|
255
|
+
**为什么 `FenceRegistry` 是 host 显式 register,而不是 framework 内置一组围栏?**
|
|
256
|
+
|
|
257
|
+
- "常用"本质是某个产品的需求——其他 agent 不一定需要 `<system-event>`
|
|
258
|
+
- 一旦 framework 内置 4 类,命名权落 framework,host 想换名字就破坏接口契约
|
|
259
|
+
- 内置等于"把 host 产品语义渗漏进 framework"——本质就是要避免的
|
|
260
|
+
|
|
261
|
+
**为什么不引入 FenceProvider 让 framework 自动生成 fence 内容?**
|
|
262
|
+
|
|
263
|
+
- 内容生成(去哪 query memory / 怎么做关键词 ranking / 是否依赖向量数据库)**纯属 host 产品决策**
|
|
264
|
+
- framework 提供"插槽",host 提供"内容"——这是边界
|
|
265
|
+
|
|
266
|
+
**为什么要新增 `context_injection` type,而不是只放 `metadata.fenceKind`?**
|
|
267
|
+
|
|
268
|
+
- `AiMessage` 是 zod 闭合枚举,没有稳定 type 就只能继续借用 `document_fragment` / `user_input`
|
|
269
|
+
- 借用 `user_input` 会破坏一个重要不变量:用户真实输入和 host 注入上下文混在同一个消息里,生命周期管理时容易误删或正则改坏用户输入
|
|
270
|
+
- `metadata.fenceKind` 表达开放 kind;物理类别仍需要一个稳定 type
|
|
271
|
+
|
|
272
|
+
**`MustKeepPolicy` 为什么用配置对象而不是函数式 hook?**
|
|
273
|
+
|
|
274
|
+
- 函数 hook 把行为藏进闭包,难以审计 / 难以序列化进 telemetry
|
|
275
|
+
- 实际场景 99% 的判断就是"按 type 列表 + 按 fenceKind 列表"——配置对象足够,且能直接 dump 出来 debug
|
|
276
|
+
- 复杂场景留 escape hatch:未来需要时再加 `customMatcher?: (msg) => boolean`
|
|
277
|
+
|
|
278
|
+
## 11. 兼容期注意
|
|
279
|
+
|
|
280
|
+
linnkit 0.4.x 起,agent profile 的公开请求合同已经收窄:
|
|
281
|
+
|
|
282
|
+
- host 产品字段不再挂在 `AgentProfileRequest` 上
|
|
283
|
+
- `MessageFormatter` 也不再替 `document_fragment` / `additional_context` 这类产品语义做包装
|
|
284
|
+
|
|
285
|
+
新接入方应当:
|
|
286
|
+
|
|
287
|
+
- 把 host 产品字段全部走 `fences[]` 通道
|
|
288
|
+
- 不引用 `chatContext` / `chatTasks` 等 namespace;需要兼容旧 chat 形态时,先使用主入口的扁平导出,后续迁到 tools-disabled `AgentSpec`
|
|
289
|
+
- 不 deep import `profiles/chat/*`;这仍是迁移期兼容层,不是新功能扩展点
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Glossary · 术语对照
|
|
2
|
+
|
|
3
|
+
> **What** · 术语对照表 —— `Checkpoint` / `Event` / `Fence` / `Run` / `Trace` / `Tokenizer` 等同名异义概念在 linnkit 各处的精确含义。
|
|
4
|
+
> **When to read** · 看文档时被术语搞混;review 接入实现时需要对齐措辞;多人协作 / PR review 前对齐术语。
|
|
5
|
+
> **Prerequisites** · 无。
|
|
6
|
+
> **Key exports** · 无(本文是术语表)。
|
|
7
|
+
> **Related** · [`constraints-and-pitfalls.md`](./constraints-and-pitfalls.md) · 所有 §7.2 单点接入文档
|
|
8
|
+
|
|
9
|
+
agent 生态有几个名字相同语义不同的概念,第一次踩坑后才会意识到。先记住这几条。
|
|
10
|
+
|
|
11
|
+
## 1. "Checkpoint" 的两种含义
|
|
12
|
+
|
|
13
|
+
| 维度 | **Engine-state Checkpoint**(linnkit 拥有)| **应用层 Context Checkpoint**(你产品自有)|
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| 接口 | `Checkpointer` port(`@linnlabs/linnkit/runtime-kernel`)| 不是 linnkit 接口;通常是你定义的一个 LLM tool |
|
|
16
|
+
| 存什么 | `EngineState`:`nodeId / pendingToolCalls / executorLocal.stepCount / local` | LLM 主动写的"阶段总结摘要" |
|
|
17
|
+
| 谁触发 | `GraphExecutor` 在循环内自动 save / load | LLM 模型自己在判断对话过长时主动调用工具 |
|
|
18
|
+
| 解决什么 | 执行控制:中断恢复、为长 run / 异步 run 铺路 | 上下文工程:压缩 LLM context window、保留语义 |
|
|
19
|
+
| 落到哪 | 你提供的 `Checkpointer` 适配器(SQLite/Redis/文件…)| 通常是个 RuntimeEvent,落你自己的 `EventStore` |
|
|
20
|
+
| linnkit 知不知道? | 知道(公开 port)| **不知道**(产品自有)|
|
|
21
|
+
|
|
22
|
+
接入时**绝对不要**把这两件事混到一起:
|
|
23
|
+
|
|
24
|
+
- 实现 `Checkpointer` adapter 时,**只**要能 save/load `EngineState` 就够了。不要试图在里面塞"摘要 / 对话压缩"语义。
|
|
25
|
+
- 想做"对话太长时压缩上下文",那是另一条产品功能:定义你自己的 LLM 工具、它的输出走你的 `EventStore`、由你自己的 context-manager pipeline 在下一轮上下文构建时识别 marker 并裁剪。
|
|
26
|
+
|
|
27
|
+
## 2. "Event" 的三层
|
|
28
|
+
|
|
29
|
+
| 名字 | 所在层 | 用途 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `AnyAgentEvent` | runtime-kernel 内部领域事件 | graph node 内部产出的原始事件 |
|
|
32
|
+
| `RuntimeEvent` | runtime-kernel → host 持久化事件 | 持久化、上下文重建、history 回放的事实来源 |
|
|
33
|
+
| 实时通道事件(如 SSE)| host realtime adapter | 前端实时渲染(**接入方自己负责**)|
|
|
34
|
+
|
|
35
|
+
`RuntimeEvent` 持久化由你的 `EventStore` adapter 落地;实时推送由你自己的 realtime adapter 决定。linnkit 不规定这一层。
|
|
36
|
+
|
|
37
|
+
## 3. "Fence"
|
|
38
|
+
|
|
39
|
+
| 维度 | 说明 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| 什么是 fence | host 自定义的"上下文围栏家族",把不同来源的上下文(项目元数据 / 长记忆 / 系统事件 / 子 agent 摘要 / 用户引用 / ……)按 placement + lifetime + role 组织,注入到 LLM 不同位置 |
|
|
42
|
+
| 谁拥有 fence kind 的命名 | host(kebab-case,如 `memory-context` / `system-event`)|
|
|
43
|
+
| linnkit 提供什么 | `FenceDescriptor` 声明 schema、`FenceRegistry` 容器、`FenceLifetimePreprocessor` 生命周期清理、`MustKeepPolicy` 必保留判定、`context_injection` 这类 `AiMessage.type` 稳定载体 |
|
|
44
|
+
| host 提供什么 | descriptors(fence 家族定义)+ injections adapter(请求字段 → `FenceInjection[]`)+ 起码一个 `FenceRegistry` 实例供 orchestrator/formatter 共用 |
|
|
45
|
+
| 为什么这么设计 | 同一套 host 适配能支持任意"项目上下文 / 文档片段 / 长记忆 / 子 agent 摘要 / 系统事件"的混搭,框架不需要任何改动 |
|
|
46
|
+
|
|
47
|
+
## 4. "Run" / "Turn" / "Conversation" / "Trace"
|
|
48
|
+
|
|
49
|
+
| ID | 语义 | 谁拥有 |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `runId` | 一次 agent 执行的唯一 id;从注册到终态(completed/failed/cancelled/awaiting_user/paused)有完整生命周期 | linnkit RunSupervisor(host 可在 register 时显式传入对齐自己的 `turnId`)|
|
|
52
|
+
| `turnId` | host 概念:一次用户输入 → 一次 agent 回答 的轮次 id | host |
|
|
53
|
+
| `conversationId` | host 概念:一段连续对话 id(一个 conversation 含多个 turn / run)| host |
|
|
54
|
+
| `traceId` | 可选:跨服务/跨进程追踪 id(如 OpenTelemetry trace id)| host |
|
|
55
|
+
| `parentRunId` | 当前 run 的父 run id(同步 child-run / detached spawned run 都会用)| linnkit RunSupervisor |
|
|
56
|
+
|
|
57
|
+
**推荐做法**:`runId = turnId`。这样 `RuntimeEvent.metadata.run_context.runId` 在 host 的 EventStore 里有现成索引,`RunHandle.observe({ includePersisted: true })` 可以直接 replay。
|
|
58
|
+
|
|
59
|
+
## 5. "Subrun" / "Child-run" / "Internal Agent"
|
|
60
|
+
|
|
61
|
+
linnkit 0.5.0 起统一术语:
|
|
62
|
+
|
|
63
|
+
| 概念 | 用什么 |
|
|
64
|
+
|---|---|
|
|
65
|
+
| 父 agent 调用子 agent 这件事 | **child run**(公开 namespace:`runtimeKernel.childRunTrace`)|
|
|
66
|
+
| 子调用的观测协议(前端 trace UI 用)| 事件 type 仍叫 `subrun_trace`(向后兼容)|
|
|
67
|
+
| 子调用的"调用器"组件 | `ChildRunInvoker`(内部代码命名)|
|
|
68
|
+
|
|
69
|
+
**不要**在新代码里用 `subrun` 或 `internalAgent` 作为新命名——它们是历史遗留。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# LLM Provider · 接 LLM provider
|
|
2
|
+
|
|
3
|
+
> **What** · 实现 `AgentAiEngine` 接 OpenAI / Anthropic / DeepSeek / OpenRouter 等 provider,含流式 + 非流式 + reasoning sidecar。
|
|
4
|
+
> **When to read** · 选定 LLM provider 后做适配器;要扩展支持的模型族;做多 provider 路由 / fallback。
|
|
5
|
+
> **Prerequisites** · [`02-quickstart.md`](./02-quickstart.md)。
|
|
6
|
+
> **Key exports** · `AgentAiEngine` / `LlmCallOptions` / `LlmRequestMessage` / `ToolCall` / `ProviderReasoningDetails` from `@linnlabs/linnkit/ports`。
|
|
7
|
+
> **Related** · [`agent-registration-guide.md`](./agent-registration-guide.md) ⭐ · [`context-engineering.md` §10](./context-engineering.md)
|
|
8
|
+
|
|
9
|
+
## 1. linnkit 给你的合同
|
|
10
|
+
|
|
11
|
+
- `AgentAiEngine`(来自 `@linnlabs/linnkit/ports`):必须实现 `chatCompletion` + `chatCompletionStream` 两个方法。流式接口的回调签名详见类型定义。
|
|
12
|
+
- `LlmRequestMessage` / `LlmCallOptions` / `ProviderReasoningDetails` / `ToolCallChunk` / `ToolCallExtraContent`(来自 `@linnlabs/linnkit/ports`):调用入参与流式 chunk 形状。
|
|
13
|
+
- `runtimeKernel.llm.LlmCaller`(来自 `@linnlabs/linnkit/runtime-kernel`):runtime 内部用的统一调用器,host 在 runtime-assembly 时把 `AgentAiEngine` 通过 `LlmCaller` 包一层。
|
|
14
|
+
|
|
15
|
+
## 2. linnkit 自带的 mock primitive
|
|
16
|
+
|
|
17
|
+
- `createScriptedAiEngineHarness`(来自 `@linnlabs/linnkit/testkit`):满足 `AgentAiEngine` 接口的脚本化实现。它的 `getLlmCaller()` 直接产出可注入的 `LlmCaller`,写测试零样板。
|
|
18
|
+
|
|
19
|
+
## 3. Provider replay sidecar(多家 reasoning model 必读)
|
|
20
|
+
|
|
21
|
+
部分 provider(DeepSeek `reasoning_content`、OpenRouter / Claude reasoning blocks 等)会返回**必须随下一轮工具调用原样回传的不透明载荷**。linnkit 的 vendor-neutral 槽位是:
|
|
22
|
+
|
|
23
|
+
| 链路位置 | 字段 | 谁负责往里塞 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| 流式 chunk / 非流式响应 | `AgentAiEngineStreamContent.reasoning_details` | 你的 provider adapter |
|
|
26
|
+
| RuntimeEvent | `tool_call_decision.payload.reasoning_details` | linnkit 自动 |
|
|
27
|
+
| 回放后的 AiMessage | `metadata.reasoning_details` 与 `metadata.tool_calls[*].extra_content` | linnkit 自动 |
|
|
28
|
+
| 工具调用扩展 | `tool_calls[*].extra_content` | 你的 provider adapter(写);linnkit 回放时透传 |
|
|
29
|
+
|
|
30
|
+
你的 adapter 只负责字段互译——**把 provider 私有字段归一化进上面的通用槽位**,不要把私有字段散到 graph-engine 或 context-manager。
|
|
31
|
+
|
|
32
|
+
出关到 LLM 时,host 默认装配应当用 `formatAgentLlmMessages(messages, { fenceRegistry })`(来自 `@linnlabs/linnkit/context-manager`);它走 native tool 回放形态,会自动把 sidecar 写回去。
|
|
33
|
+
|
|
34
|
+
> ⚠️ **注意**:被工具历史压缩 / 历史摘要替换 / chat formatter 处理过的旧工具组,不再保证 sidecar 可回放——这是 token 预算与 chat 兼容层的设计取舍。如果某个 provider 强要求 reasoning blocks 必须随回传,请确保该工具组以原始 `tool_call_decision + tool_output` 结构进入下一轮上下文。
|
|
35
|
+
|
|
36
|
+
### 3.1 缺 sidecar 时怎么办
|
|
37
|
+
|
|
38
|
+
默认情况下,linnkit 不会根据 `model_id` 自己猜 provider 的 replay 约束。host 可以在装配 `AgentMessageOrchestrator` 时通过 `resolveToolReplayProtocolPolicy({ request, modelId })` 提供模型级默认策略;单个 agent 也可以用 `AgentSpec.contextPolicy.providerReplay` 覆盖它。
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
contextPolicy: {
|
|
42
|
+
profileId: 'agent',
|
|
43
|
+
providerReplay: {
|
|
44
|
+
provider: 'system_default',
|
|
45
|
+
requiresReasoningDetailsForToolReplay: true,
|
|
46
|
+
missingSidecarBehavior: 'provider_empty_replay_field',
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`missingSidecarBehavior` 的含义:
|
|
52
|
+
|
|
53
|
+
| 值 | 行为 |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `allow` | 不治理旧工具组,保持原样 |
|
|
56
|
+
| `degrade_to_text` | 把缺少 sidecar 的历史工具组降级成普通 assistant 文本 |
|
|
57
|
+
| `provider_empty_replay_field` | 保留结构化工具组,但标记 `provider_empty_replay_field`,交给 provider adapter 出关时填空字段 |
|
|
58
|
+
|
|
59
|
+
优先级:`contextPolicy.providerReplay`(agent 级) > `resolveToolReplayProtocolPolicy`(host/model 级) > 默认 `allow`。
|
|
60
|
+
|
|
61
|
+
## 4. 你必须做的
|
|
62
|
+
|
|
63
|
+
1. 实现一个符合 `AgentAiEngine` 的 adapter,把 HTTP / SDK 调用封进 `chatCompletion[Stream]`。
|
|
64
|
+
2. 在 runtime-assembly 里把 `aiEngine` 通过 `runtimeKernel.llm.LlmCaller` 包一层。
|
|
65
|
+
3. 实现 `ModelResolver` / `ModelCatalog`(来自 `@linnlabs/linnkit/runtime-kernel` 的 `llm` namespace),把 host 的 modelId 解析为 provider + provider modelId。
|
|
66
|
+
|
|
67
|
+
## 5. 你不要做的
|
|
68
|
+
|
|
69
|
+
- 不要让 graph-side 代码直接知道你家 SDK 的 HTTP 形态。
|
|
70
|
+
- 不要在测试里 patch 模块级全局 ai engine——通过依赖注入替换。
|
|
71
|
+
- 不要把 provider 重试 / 审计 / fallback 逻辑散落在 host 业务文件里——收敛到 adapter 内。
|
|
72
|
+
|
|
73
|
+
## 6. 最小验证
|
|
74
|
+
|
|
75
|
+
- 用 `createScriptedAiEngineHarness()` 写红绿测试。
|
|
76
|
+
- 在 host harness 里覆盖:多 provider 切换、`reasoning_details` 流式累积、`tool_call` sidecar 回放。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Persistence · 接持久化(3 个 port)
|
|
2
|
+
|
|
3
|
+
> **What** · 三个持久化适配 port —— `Checkpointer`(断点续推)/ `EventStore`(事件归档)/ `RunRegistryStore`(run 元数据)。
|
|
4
|
+
> **When to read** · 要让 run 跨进程崩溃后恢复;要审计 / 回放 agent 历史;要把 in-memory 默认实现换成真实 DB。
|
|
5
|
+
> **Prerequisites** · [`02-quickstart.md`](./02-quickstart.md);建议与 [`run-supervisor.md`](./run-supervisor.md) 并读。
|
|
6
|
+
> **Key exports** · `Checkpointer` / `EventStore` / `RunRegistryStore` from `@linnlabs/linnkit/runtime-kernel`。
|
|
7
|
+
> **Related** · [`run-supervisor.md`](./run-supervisor.md) · [`audit.md`](./audit.md) · [`realtime.md`](./realtime.md) · [`glossary.md`](./glossary.md)
|
|
8
|
+
|
|
9
|
+
> **术语提醒**:这里的 `Checkpointer` 是 **engine-state checkpoint**——保存 graph engine 执行状态(`nodeId / pendingToolCalls / executorLocal.stepCount / local`),用来"中断后从断点继续推理"。它**不是**任何"对话总结/上下文裁剪"语义;后者是上下文工程层面的 RuntimeEvent,应当走你自己的 `EventStore`,跟本接口无关。详见 [glossary.md](./glossary.md)。
|
|
10
|
+
|
|
11
|
+
## 1. linnkit 给你的合同
|
|
12
|
+
|
|
13
|
+
- `Checkpointer`(来自 `@linnlabs/linnkit/runtime-kernel`,在 `graph` namespace 下):`load` / `save` / `clear` 三个必需方法 + `peekMeta` / `list` 两个可选。
|
|
14
|
+
- `EventStore`(来自 `@linnlabs/linnkit/runtime-kernel`,在 `graph` namespace 下):`append` / `range` / `latestEventId` 三个必需 + `truncate` 可选。配套 `createMonotonicEventIdFactory()` 帮你生成单调 id。
|
|
15
|
+
- `RunRegistryStore`(来自 `@linnlabs/linnkit/runtime-kernel`,在 `runSupervisor` namespace 下):run lifecycle 元数据落库。
|
|
16
|
+
- `RuntimeEvent` / `EventEnvelope` / `PersistedEvent` 类型来自 `@linnlabs/linnkit/contracts` 与 `runtime-kernel`。
|
|
17
|
+
|
|
18
|
+
## 2. linnkit 自带的 mock primitive
|
|
19
|
+
|
|
20
|
+
`memoryCheckpointer` / `memoryEventStore` / `memoryRunRegistryStore` 都是 in-memory contract-test 用实现。它们藏在 runtime-kernel 内部,外部消费者一般不需要直接引用——通过 `@linnlabs/linnkit/runtime-kernel` 的 namespace 访问。如果某个未导出,请告诉框架维护方补出口。
|
|
21
|
+
|
|
22
|
+
## 3. 你必须做的
|
|
23
|
+
|
|
24
|
+
1. 决定真后端:SQLite / Postgres / IndexedDB / 文件 都行。linnkit 不规定。
|
|
25
|
+
2. 实现 3 个 port,作为 host runtime-assembly 的依赖注入点。
|
|
26
|
+
3. 写入时使用 `createMonotonicEventIdFactory()` 生成 eventId;旧数据可保持 `NULL` 并在读取时 fallback。
|
|
27
|
+
4. 使用**短事务**:每个 lifecycle 调用各自独立 commit,**不要**跨整个 LLM/tool 执行过程持有数据库事务。
|
|
28
|
+
|
|
29
|
+
## 4. 实现 EventStore 的常见落地形态
|
|
30
|
+
|
|
31
|
+
- 已有 `conversations / runs / events / messages` 表?采用 **schema-preserving event-grained core**:保留既有表结构,不新增第二张事件事实表。
|
|
32
|
+
- 你的 `EventStore` 实现可以同时对外暴露两组 API:
|
|
33
|
+
- host 主写链直接用的短事务会话 API(`beginRunSession` / `appendEventToRun` / `completeRun` / `failRun`);
|
|
34
|
+
- 给 linnkit `EventStore` port 消费的 adapter(把 `append/range/latestEventId` 桥接到底层)。
|
|
35
|
+
|
|
36
|
+
## 5. 你不要做的
|
|
37
|
+
|
|
38
|
+
- 不要把"数据库就是平台默认实现"的假设写死。
|
|
39
|
+
- 不要跳过 `schemaVersion` / `CheckpointMeta` 这些契约字段。
|
|
40
|
+
- 不要一边写库一边偷偷吞掉冲突或重复事件——push 到上层做幂等判断。
|
|
41
|
+
|
|
42
|
+
## 6. 最小验证
|
|
43
|
+
|
|
44
|
+
linnkit 在内部对每个 port 都跑了 contract test。你的实现必须通过这些**等价的契约测试**。建议在 host 测试里 mirror linnkit 的 contract test,把 memory 实现 → 你的实现做参数化,确保行为 1:1。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Realtime · 实时通道(host 完全自有)
|
|
2
|
+
|
|
3
|
+
> **What** · 实时通道(SSE / WebSocket / IPC)接入 —— `RuntimeEvent` 映射到 host 自己的 wire 协议;前端用 browser-safe events seam 做事件治理决策。
|
|
4
|
+
> **When to read** · 要把 agent 进度推到前端;做 daemon ↔ renderer IPC;做 Web SSE;想知道"哪些事件该回放给 UI / 哪些只该写 EventStore"。
|
|
5
|
+
> **Prerequisites** · [`02-quickstart.md`](./02-quickstart.md);浏览器使用规则见 [`README §5`](./README.md)。
|
|
6
|
+
> **Key exports** · `RuntimeEvent` / `EventEnvelope` / `SSEEvent` from `@linnlabs/linnkit/contracts` · `shouldReplayRuntimeEventToUi` / `shouldEmitRuntimeEventToSse` / `getRuntimeEventUiProjectionKind` from `@linnlabs/linnkit/runtime-kernel/events`(**前端唯一可 import 的入口**)。
|
|
7
|
+
> **Related** · [`audit.md`](./audit.md) · [`persistence.md`](./persistence.md) · [`constraints-and-pitfalls.md`](./constraints-and-pitfalls.md)
|
|
8
|
+
|
|
9
|
+
`@linnlabs/linnkit` **不规定** SSE / WebSocket / MQTT 的接口形状——一个原因是不同部署形态(HTTP server / Electron IPC / 内嵌 RPC)天差地别。
|
|
10
|
+
|
|
11
|
+
但有两条**铁规**:
|
|
12
|
+
|
|
13
|
+
1. **唯一出口原则**:所有实时事件必须经由你自己的 EventBus → realtime adapter 单一路径推给前端。**禁止**在 graph node / tool / bridge 中直接调用 sink 推送实时事件(`WaitUserNode` 是唯一的协议级例外,它发出 `requires_user_interaction` 是暂停协议的一部分)。
|
|
14
|
+
2. **不要绕过 EventBus 写**:会导致 seq 断裂和审计遗漏。
|
|
15
|
+
|
|
16
|
+
## 1. 事件转换链路
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
graph 内部 AnyAgentEvent
|
|
20
|
+
│ eventMapper.agentToRuntime() ← 来自 @linnlabs/linnkit/runtime-kernel/events
|
|
21
|
+
▼
|
|
22
|
+
RuntimeEvent
|
|
23
|
+
│ shouldEmitRuntimeEventToSse(event) ← 你的实时 adapter 决定
|
|
24
|
+
▼
|
|
25
|
+
你的 SSEEvent / WS message / IPC payload
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 2. eventGovernance 决策函数(前端可用)
|
|
29
|
+
|
|
30
|
+
事件**生命周期治理**统一走 `eventGovernance` 纯函数:
|
|
31
|
+
|
|
32
|
+
| 函数 | 用途 |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `shouldPersistRuntimeEvent` | 是否写入 host EventStore(`ephemeral=true` 或 `tool_process` 不持久化) |
|
|
35
|
+
| `shouldReplayRuntimeEventToUi` | 页面 reload 时是否从 EventStore 回放给前端 |
|
|
36
|
+
| `shouldEnterAgentContext` | 是否进入 LLM 上下文窗口 |
|
|
37
|
+
| `shouldEmitRuntimeEventToSse` | 是否走实时通道 |
|
|
38
|
+
| `getRuntimeEventUiProjectionKind` | UI 投影类别(不同 kind 走不同前端组件) |
|
|
39
|
+
|
|
40
|
+
这些函数都在 `@linnlabs/linnkit/runtime-kernel/events` slim seam,**浏览器安全**。前端 renderer / 任意 browser bundle 都可直接 import。
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// renderer/src/agentEventPolicy.ts
|
|
44
|
+
import {
|
|
45
|
+
shouldReplayRuntimeEventToUi,
|
|
46
|
+
getRuntimeEventUiProjectionKind,
|
|
47
|
+
} from '@linnlabs/linnkit/runtime-kernel/events';
|
|
48
|
+
import type { RuntimeEvent } from '@linnlabs/linnkit/contracts';
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 3. 三种事件模型
|
|
52
|
+
|
|
53
|
+
| 模型 | 所在层 | 用途 |
|
|
54
|
+
|------|--------|------|
|
|
55
|
+
| `AnyAgentEvent` | runtime-kernel(领域事件)| graph node 内部产出的原始事件 |
|
|
56
|
+
| `RuntimeEvent` | runtime-kernel → host(持久化事件)| 持久化、上下文重建、history 回放的事实来源 |
|
|
57
|
+
| 实时通道事件(如 SSE)| host realtime adapter(表现层事件)| 前端实时渲染(**接入方自己负责**)|
|
|
58
|
+
|
|
59
|
+
`RuntimeEvent` 持久化由你的 `EventStore` adapter 落地;实时推送由你自己的 realtime adapter 决定。linnkit 不规定这一层。
|
|
60
|
+
|
|
61
|
+
## 4. 几个常见事件的处置 cheatsheet
|
|
62
|
+
|
|
63
|
+
| RuntimeEvent | persist | replayToUi | enterAgentContext | realtime |
|
|
64
|
+
|---|---|---|---|---|
|
|
65
|
+
| `final_answer_chunk` | ✗(ephemeral)| ✗ | ✗ | ✓ |
|
|
66
|
+
| `final_answer` | ✓ | ✓ | ✓ | ✓ |
|
|
67
|
+
| `tool_process` | ✗ | ✗ | ✗ | ✓ |
|
|
68
|
+
| `tool_output` | ✓ | ✓ | ✓ | ✓ |
|
|
69
|
+
| `thought`(增量)| ✗ | ✗ | ✗ | ✓ |
|
|
70
|
+
| `thought`(完成)| ✓ | ✓ | ✓ | ✓ |
|
|
71
|
+
| `tool_call_decision` | ✓ | ✓ | ✓ | ✓ |
|
|
72
|
+
| `requires_user_interaction` | ✓ | ✓ | ✗ | ✓ |
|
|
73
|
+
| `stream_end` | ✓ | ✓ | ✗ | ✓ |
|
|
74
|
+
| `audit_envelope` | ✓ | ✗ | ✗ | ✗ |
|
|
75
|
+
|
|
76
|
+
实际决策一律以 `shouldXxxRuntimeEvent()` 函数返回值为准;这张表只是速查。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# RunSupervisor · RunHandle / cost / cancel / observe
|
|
2
|
+
|
|
3
|
+
> **What** · 每次 agent 调用的"身份证 + 遥控器" —— `RunSupervisor` 注册 + `RunHandle` v2 暴露 `cancel` / `observe` / `cost` + `spawnDetached` 异步后台 run。
|
|
4
|
+
> **When to read** · 要支持用户取消 agent;要做长任务后台运行;要做父子 run 成本聚合;要崩溃恢复 `recoverOnBoot`。
|
|
5
|
+
> **Prerequisites** · [`02-quickstart.md`](./02-quickstart.md)。
|
|
6
|
+
> **Key exports** · `RunSupervisor` / `RunHandle` / `createInMemoryRunRegistry` from `@linnlabs/linnkit/runtime-kernel`。
|
|
7
|
+
> **Related** · [`child-runs.md`](./child-runs.md) · [`persistence.md`](./persistence.md) · [`audit.md`](./audit.md)
|
|
8
|
+
|
|
9
|
+
RunSupervisor 是每次 agent 调用的"身份证 + 遥控器"。host 应在一次用户请求进入 runner 前注册 run,拿到 `RunHandle` 后再把 `handle.signal` 交给 GraphExecutor / 工具上下文。
|
|
10
|
+
|
|
11
|
+
## 1. 5 行骨架
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { runtimeKernel } from '@linnlabs/linnkit';
|
|
15
|
+
|
|
16
|
+
const supervisor = new runtimeKernel.runSupervisor.DefaultRunSupervisor({
|
|
17
|
+
registryStore: new runtimeKernel.runSupervisor.MemoryRunRegistryStore(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const handle = await supervisor.registerRun({
|
|
21
|
+
runId: turnId, // 推荐:host 用 turnId 对齐 RuntimeEvent / EventStore / RunRecord
|
|
22
|
+
parentSignal: requestSignal, // HTTP request cancel / 上层 run cancel 会级联到 handle.signal
|
|
23
|
+
conversationId,
|
|
24
|
+
agentSpec,
|
|
25
|
+
request: invokeRequest,
|
|
26
|
+
eventBus,
|
|
27
|
+
eventStore,
|
|
28
|
+
costCollector,
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 2. 接入规则
|
|
33
|
+
|
|
34
|
+
- `runId` 建议由 host 显式传入。如果 host 已有稳定的 `turnId` / request id,可以直接用 `runId = turnId`,这样 `RunHandle.observe({ includePersisted: true })` 能复用 EventStore 里的 runId 索引。
|
|
35
|
+
- `RunHandle.signal` 是 runner 内部唯一信号来源;不要再给 GraphExecutor 另起一根 ad-hoc `AbortController`。
|
|
36
|
+
- `AgentRunnerService.run()` 一类 host runner 应同步返回 `{ handle, result }`:UI 可以立刻拿 handle 做 cancel/observe/cost,执行结果继续等 `result`。
|
|
37
|
+
- runner 生命周期必须显式写:启动前 `markRunning()`,正常结束 `markCompleted()`,异常结束 `markFailed()`,取消由 `handle.cancel({ reason })` 写 `cancelled`。
|
|
38
|
+
- `WaitUserNode` 触发的 `requires_user_interaction` 是正式 pause 事实事件。事件必须带 `metadata.run_context.runId`,host runner 要把 `runUntilYield().events` 中的这类事件发布/持久化,并调用 `markAwaitingUser()`;`DefaultRunSupervisor` 也会订阅该事件作为兜底联动。这样 `supervisor.peek(runId).status` 才会从 `running` 变成 `awaiting_user`。
|
|
39
|
+
- `pause/resume/runTree/handleFailure` 仍是冷暂停/树管理/故障策略占位,调用时应抛 `NotImplementedError`,不要给假实现。
|
|
40
|
+
|
|
41
|
+
## 3. RunHandle 完整 API(截至 0.5.0)
|
|
42
|
+
|
|
43
|
+
| Method | 用途 |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `runId` / `spec()` / `request()` | run 身份、对应 AgentSpec、invoke request 快照 |
|
|
46
|
+
| `signal` | runner 内部唯一 abort 信号;级联到 GraphExecutor 与所有 tool context |
|
|
47
|
+
| `cancel({ reason })` | 写 `RunRecord.status = 'cancelled'` + 触发 abort + 发 `run.cancel` audit envelope |
|
|
48
|
+
| `markRunning()` / `markCompleted()` / `markFailed(error)` | runner 必须显式写生命周期,否则 run 永久停在 `pending` |
|
|
49
|
+
| `markAwaitingUser()` | `WaitUserNode` 暂停后 host runner 调用,更新 RunRecord 到 `awaiting_user` |
|
|
50
|
+
| `observe(options?)` | 事件流:`includePersisted` 复用 EventStore replay,`signal` 控制订阅生命周期 |
|
|
51
|
+
| `cost()` | 读 `RunCostCollector.snapshot(runId)` |
|
|
52
|
+
| `traceContext()` | 返回 `{ runId, parentRunId?, turnId?, traceId? }`,给 telemetry / audit / child-run 派生用 |
|
|
53
|
+
|
|
54
|
+
## 4. 进程恢复(recoverOnBoot)
|
|
55
|
+
|
|
56
|
+
进程启动时建议调用 `recoverOnBoot()`,把上次进程遗留的 `pending/running/awaiting_user/paused` run 标记为 `RUN_ABANDONED`,避免管理面板里永远挂着"运行中"。
|
|
57
|
+
|
|
58
|
+
## 5. Cost 统计
|
|
59
|
+
|
|
60
|
+
`RunHandle.cost()` 只读你注入的 `RunCostCollector`。最小实现可以监听 `TelemetryPort.emit({ kind: 'llm_call', usage })`,按 `scope.runId ?? scope.turnId` 聚合 token 与 latency。
|
|
61
|
+
|
|
62
|
+
同步 child-run 场景建议把 `scope.runId` 设为 child run / subrun ID,并把 `scope.parentRunId` 设为父 run ID。这样父子 cost 可以分桶统计,父 run 的 `childrenTotal` 能覆盖同步子 agent 的 LLM cost。美元成本、quota ledger、跨进程 / detached 后台 run 的长期持久化账本属于后续阶段。
|
|
63
|
+
|
|
64
|
+
## 6. 最小验证
|
|
65
|
+
|
|
66
|
+
- 单测:显式 `runId` 注册后,`handle.runId` 和 `registryStore.load(runId)` 对齐。
|
|
67
|
+
- 单测:同一个 `runId` 注册两次抛 `RunAlreadyRegisteredError`。
|
|
68
|
+
- 单测:`parentSignal.abort('reason')` 后 `handle.signal.aborted === true`。
|
|
69
|
+
- 集成测:取消时 `RunRecord.errorIfAny.message` 能透传到你的 `stream_end.reason_message`。
|