@axiom-lattice/gateway 1.0.46 → 2.0.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/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +13 -0
- package/RESUME_STREAM_CONTENT_BASED.md +326 -0
- package/RESUME_STREAM_README.md +388 -0
- package/dist/index.js +93 -38
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +100 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/controllers/run.ts +60 -148
- package/src/routes/index.ts +7 -1
- package/src/schemas/index.ts +33 -0
- package/src/services/agent_service.ts +79 -14
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @axiom-lattice/gateway@
|
|
2
|
+
> @axiom-lattice/gateway@2.0.0 build /home/runner/work/agentic/agentic/packages/gateway
|
|
3
3
|
> tsup src/index.ts --format cjs,esm --dts --clean --sourcemap
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
[34mCLI[39m Cleaning output folder
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
15
|
-
[
|
|
16
|
-
[
|
|
17
|
-
[
|
|
12
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m23.98 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m51.37 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 87ms
|
|
15
|
+
[32mCJS[39m [1mdist/index.js [22m[32m26.07 KB[39m
|
|
16
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m51.45 KB[39m
|
|
17
|
+
[32mCJS[39m ⚡️ Build success in 88ms
|
|
18
18
|
[34mDTS[39m Build start
|
|
19
|
-
[32mDTS[39m ⚡️ Build success in
|
|
19
|
+
[32mDTS[39m ⚡️ Build success in 6652ms
|
|
20
20
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m1.97 KB[39m
|
|
21
21
|
[32mDTS[39m [1mdist/index.d.mts [22m[32m1.97 KB[39m
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Content-Based Resume Stream
|
|
2
|
+
|
|
3
|
+
## 概述 (Overview)
|
|
4
|
+
|
|
5
|
+
**重要更新**: `resume_stream` 现在基于**内容匹配**而不是长度匹配。
|
|
6
|
+
|
|
7
|
+
这是一个更实用的设计,因为在实际应用中,用户通常只能获取到已接收的**内容本身**,而不知道精确的字节长度。
|
|
8
|
+
|
|
9
|
+
## 为什么要改变?(Why the Change?)
|
|
10
|
+
|
|
11
|
+
### 之前的问题 (Previous Issues)
|
|
12
|
+
|
|
13
|
+
使用 `known_content_length` 的问题:
|
|
14
|
+
|
|
15
|
+
1. **内容处理**: 前端显示的内容可能经过格式化、转义、HTML渲染等处理
|
|
16
|
+
2. **字符编码**: 多字节字符(如中文、emoji)的长度计算不准确
|
|
17
|
+
3. **数据存储**: 内容存储到数据库或localStorage后,可能有格式变化
|
|
18
|
+
4. **不直观**: 用户需要手动计算和追踪字符长度
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// ❌ 之前:需要追踪长度
|
|
22
|
+
const knownLength = 150; // 怎么知道是150?需要手动计算
|
|
23
|
+
await resume_stream({
|
|
24
|
+
thread_id,
|
|
25
|
+
message_id,
|
|
26
|
+
known_content_length: knownLength
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 现在的优势 (Current Advantages)
|
|
31
|
+
|
|
32
|
+
使用 `known_content` 的优势:
|
|
33
|
+
|
|
34
|
+
1. **直观简单**: 直接传入已接收的内容字符串
|
|
35
|
+
2. **无需计算**: 不需要手动计算或追踪长度
|
|
36
|
+
3. **可靠匹配**: 基于内容本身匹配,更准确
|
|
37
|
+
4. **易于存储**: 内容可以直接从UI、state、localStorage获取
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// ✅ 现在:直接使用内容
|
|
41
|
+
const knownContent = getContentFromUI(); // 或从localStorage读取
|
|
42
|
+
await resume_stream({
|
|
43
|
+
thread_id,
|
|
44
|
+
message_id,
|
|
45
|
+
known_content: knownContent
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 匹配算法 (Matching Algorithm)
|
|
50
|
+
|
|
51
|
+
`getNewChunksSinceContent` 使用智能匹配算法:
|
|
52
|
+
|
|
53
|
+
### 1. 精确匹配 (Exact Match)
|
|
54
|
+
```typescript
|
|
55
|
+
// 累积内容完全等于已知内容
|
|
56
|
+
if (accumulatedContent === knownContent) {
|
|
57
|
+
// 返回后续所有chunks
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. 后缀匹配 (Suffix Match)
|
|
62
|
+
```typescript
|
|
63
|
+
// 已知内容是累积内容的后缀
|
|
64
|
+
if (accumulatedContent.endsWith(knownContent)) {
|
|
65
|
+
// 返回后续所有chunks
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. 前缀匹配 (Prefix Match)
|
|
70
|
+
```typescript
|
|
71
|
+
// 已知内容是累积内容的前缀
|
|
72
|
+
if (accumulatedContent.startsWith(knownContent)) {
|
|
73
|
+
// 返回剩余部分
|
|
74
|
+
const remaining = accumulatedContent.substring(knownContent.length);
|
|
75
|
+
// 智能重构chunks
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 4. 降级处理 (Fallback)
|
|
80
|
+
```typescript
|
|
81
|
+
// 如果没有匹配
|
|
82
|
+
// 返回所有chunks(最安全的降级策略)
|
|
83
|
+
return allChunks;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## 使用示例 (Usage Examples)
|
|
87
|
+
|
|
88
|
+
### 基本使用 (Basic Usage)
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// 1. 用户刷新前收到了部分内容
|
|
92
|
+
const receivedContent = "Hello world! This is a streaming response...";
|
|
93
|
+
|
|
94
|
+
// 2. 页面刷新后,从localStorage恢复内容
|
|
95
|
+
const savedContent = localStorage.getItem('chat_content');
|
|
96
|
+
|
|
97
|
+
// 3. 继续接收新内容
|
|
98
|
+
const stream = await resume_stream({
|
|
99
|
+
thread_id: 'thread-123',
|
|
100
|
+
message_id: 'msg-456',
|
|
101
|
+
known_content: savedContent || '',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
for await (const chunk of stream) {
|
|
105
|
+
displayNewContent(chunk.content);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### React 示例 (React Example)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
function ChatMessage({ threadId, messageId }) {
|
|
113
|
+
const [content, setContent] = useState('');
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
// 从localStorage恢复
|
|
117
|
+
const saved = localStorage.getItem(`message_${messageId}`);
|
|
118
|
+
if (saved) {
|
|
119
|
+
setContent(saved);
|
|
120
|
+
|
|
121
|
+
// 继续接收新内容
|
|
122
|
+
(async () => {
|
|
123
|
+
const stream = await resume_stream({
|
|
124
|
+
thread_id: threadId,
|
|
125
|
+
message_id: messageId,
|
|
126
|
+
known_content: saved,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
for await (const chunk of stream) {
|
|
130
|
+
setContent(prev => prev + chunk.content);
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
}
|
|
134
|
+
}, [threadId, messageId]);
|
|
135
|
+
|
|
136
|
+
// 保存内容
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
localStorage.setItem(`message_${messageId}`, content);
|
|
139
|
+
}, [content, messageId]);
|
|
140
|
+
|
|
141
|
+
return <div>{content}</div>;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 错误处理 (Error Handling)
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
async function resumeWithRetry(threadId, messageId, knownContent, maxRetries = 3) {
|
|
149
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
150
|
+
try {
|
|
151
|
+
const stream = await resume_stream({
|
|
152
|
+
thread_id: threadId,
|
|
153
|
+
message_id: messageId,
|
|
154
|
+
known_content: knownContent,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
for await (const chunk of stream) {
|
|
158
|
+
yield chunk;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return; // 成功
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Resume attempt ${i + 1} failed:`, error);
|
|
164
|
+
if (i === maxRetries - 1) throw error;
|
|
165
|
+
|
|
166
|
+
// 等待后重试
|
|
167
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## API 变更 (API Changes)
|
|
174
|
+
|
|
175
|
+
### Before (之前)
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
interface ResumeStreamOptions {
|
|
179
|
+
thread_id: string;
|
|
180
|
+
message_id: string;
|
|
181
|
+
known_content_length: number; // ❌ 需要长度
|
|
182
|
+
poll_interval?: number;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### After (现在)
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface ResumeStreamOptions {
|
|
190
|
+
thread_id: string;
|
|
191
|
+
message_id: string;
|
|
192
|
+
known_content: string; // ✅ 直接用内容
|
|
193
|
+
poll_interval?: number;
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### ChunkBuffer API
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// Before (之前)
|
|
201
|
+
getNewChunksSinceLength(
|
|
202
|
+
threadId: string,
|
|
203
|
+
messageId: string,
|
|
204
|
+
knownContentLength: number
|
|
205
|
+
): Promise<Chunk[]>
|
|
206
|
+
|
|
207
|
+
// After (现在)
|
|
208
|
+
getNewChunksSinceContent(
|
|
209
|
+
threadId: string,
|
|
210
|
+
messageId: string,
|
|
211
|
+
knownContent: string
|
|
212
|
+
): Promise<Chunk[]>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## 性能考虑 (Performance Considerations)
|
|
216
|
+
|
|
217
|
+
### 时间复杂度 (Time Complexity)
|
|
218
|
+
- **匹配**: O(n×m) 其中 n = chunks数量, m = 平均chunk长度
|
|
219
|
+
- **实践中**: 通常很快,因为chunks数量通常不多(< 100)
|
|
220
|
+
|
|
221
|
+
### 优化建议 (Optimization Tips)
|
|
222
|
+
|
|
223
|
+
1. **限制内容长度**: 只传递最后的N个字符用于匹配
|
|
224
|
+
```typescript
|
|
225
|
+
const lastNChars = 1000; // 只用最后1000字符匹配
|
|
226
|
+
const knownContent = fullContent.slice(-lastNChars);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
2. **使用哈希**: 对于超长内容,可以考虑使用内容哈希
|
|
230
|
+
```typescript
|
|
231
|
+
// 未来可能的优化
|
|
232
|
+
const contentHash = hashContent(knownContent);
|
|
233
|
+
await resume_stream({
|
|
234
|
+
thread_id,
|
|
235
|
+
message_id,
|
|
236
|
+
known_content_hash: contentHash
|
|
237
|
+
});
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## 注意事项 (Important Notes)
|
|
241
|
+
|
|
242
|
+
### ✅ 优点 (Advantages)
|
|
243
|
+
|
|
244
|
+
1. **用户友好**: 不需要追踪长度
|
|
245
|
+
2. **更可靠**: 基于内容本身,不受编码影响
|
|
246
|
+
3. **易于实现**: 前端代码更简单
|
|
247
|
+
4. **灵活匹配**: 多种匹配策略
|
|
248
|
+
|
|
249
|
+
### ⚠️ 限制 (Limitations)
|
|
250
|
+
|
|
251
|
+
1. **精确匹配**: 内容必须完全匹配(包括空格、换行)
|
|
252
|
+
2. **性能**: 对于超大内容可能需要优化
|
|
253
|
+
3. **编码**: 必须使用相同的编码格式
|
|
254
|
+
|
|
255
|
+
### 💡 最佳实践 (Best Practices)
|
|
256
|
+
|
|
257
|
+
1. **保存原始内容**: 不要对内容进行格式化后再保存
|
|
258
|
+
```typescript
|
|
259
|
+
// ✅ 好
|
|
260
|
+
const rawContent = chunks.join('');
|
|
261
|
+
localStorage.setItem('content', rawContent);
|
|
262
|
+
|
|
263
|
+
// ❌ 不好
|
|
264
|
+
const formatted = formatContent(chunks.join(''));
|
|
265
|
+
localStorage.setItem('content', formatted);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
2. **检查状态**: 恢复前先检查thread状态
|
|
269
|
+
```typescript
|
|
270
|
+
const status = await get_thread_status(threadId);
|
|
271
|
+
if (status.status === 'completed') {
|
|
272
|
+
// 已完成,获取完整内容
|
|
273
|
+
const fullContent = await get_accumulated_content(threadId);
|
|
274
|
+
} else {
|
|
275
|
+
// 继续streaming
|
|
276
|
+
await resume_stream({ ... });
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
3. **处理失败**: 如果匹配失败,fallback到完整内容
|
|
281
|
+
```typescript
|
|
282
|
+
const stream = await resume_stream({
|
|
283
|
+
thread_id: threadId,
|
|
284
|
+
message_id: messageId,
|
|
285
|
+
known_content: knownContent || '', // 空字符串会返回所有内容
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 迁移指南 (Migration Guide)
|
|
290
|
+
|
|
291
|
+
如果你正在使用旧版本的 `resume_stream`:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
// 旧代码 (Old Code)
|
|
295
|
+
const knownLength = currentContent.length;
|
|
296
|
+
await resume_stream({
|
|
297
|
+
thread_id,
|
|
298
|
+
message_id,
|
|
299
|
+
known_content_length: knownLength // ❌
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 新代码 (New Code)
|
|
303
|
+
await resume_stream({
|
|
304
|
+
thread_id,
|
|
305
|
+
message_id,
|
|
306
|
+
known_content: currentContent // ✅
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
简单改动:删除 `.length`,直接传内容!
|
|
311
|
+
|
|
312
|
+
## 总结 (Summary)
|
|
313
|
+
|
|
314
|
+
这次改动让 `resume_stream` 更加:
|
|
315
|
+
- ✅ **实用** (Practical): 符合实际使用场景
|
|
316
|
+
- ✅ **简单** (Simple): API更直观易用
|
|
317
|
+
- ✅ **可靠** (Reliable): 基于内容匹配更准确
|
|
318
|
+
- ✅ **灵活** (Flexible): 多种匹配策略
|
|
319
|
+
|
|
320
|
+
**核心理念**: 用户知道的是内容,而不是长度!
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|