c0de-agent 1.13.1 → 1.14.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/dist/cli/commands/serve.js +5 -1
- package/dist/cli/deps.d.ts +2 -4
- package/dist/cli/deps.js +22 -36
- package/dist/cli/modes/print.js +27 -17
- package/dist/core/agent.js +59 -36
- package/dist/core/agents/parallel.js +4 -2
- package/dist/core/config-path.d.ts +3 -1
- package/dist/core/config-path.js +33 -3
- package/dist/core/config.js +81 -0
- package/dist/core/context.js +6 -3
- package/dist/core/loop/persist.js +4 -1
- package/dist/core/loop/subagent.d.ts +19 -0
- package/dist/core/loop/subagent.js +73 -7
- package/dist/core/loop.d.ts +4 -1
- package/dist/core/loop.js +22 -6
- package/dist/{server/registry-config.d.ts → core/provider-registry.d.ts} +10 -2
- package/dist/core/provider-registry.js +66 -0
- package/dist/core/redact.d.ts +2 -1
- package/dist/core/redact.js +4 -2
- package/dist/core/secret.d.ts +5 -0
- package/dist/core/secret.js +14 -0
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +61 -0
- package/dist/core/slash.js +31 -18
- package/dist/core/title.js +4 -2
- package/dist/core/todo-tags.d.ts +4 -3
- package/dist/core/todo-tags.js +20 -6
- package/dist/core/workflow.d.ts +4 -13
- package/dist/core/workflow.js +6 -6
- package/dist/core/workflows/context.js +31 -59
- package/dist/core/workflows/runtime.js +10 -8
- package/dist/dap/index.d.ts +1 -0
- package/dist/dap/index.js +1 -0
- package/dist/dap/protocol.d.ts +8 -3
- package/dist/dap/protocol.js +54 -18
- package/dist/dap/session.js +71 -12
- package/dist/dap/spawn.d.ts +11 -0
- package/dist/dap/spawn.js +48 -0
- package/dist/dap/transport.js +18 -0
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +8 -1
- package/dist/kanban/store.js +61 -13
- package/dist/llm/protocols/openai-compat.d.ts +7 -0
- package/dist/llm/protocols/openai-compat.js +19 -2
- package/dist/llm/protocols/utils/tool-stream.d.ts +12 -3
- package/dist/llm/protocols/utils/tool-stream.js +47 -23
- package/dist/llm/provider.js +18 -3
- package/dist/llm/retry.d.ts +1 -0
- package/dist/llm/retry.js +20 -7
- package/dist/llm/routing.js +14 -2
- package/dist/llm/schema/messages.d.ts +9 -2
- package/dist/llm/token.d.ts +9 -0
- package/dist/llm/token.js +10 -6
- package/dist/llm/transport.js +7 -2
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +70 -0
- package/dist/mcp/index.d.ts +46 -0
- package/dist/mcp/index.js +106 -0
- package/dist/mcp/tool-adapter.d.ts +14 -0
- package/dist/mcp/tool-adapter.js +65 -0
- package/dist/mcp/transport.d.ts +35 -0
- package/dist/mcp/transport.js +243 -0
- package/dist/mcp/types.d.ts +55 -0
- package/dist/mcp/types.js +3 -0
- package/dist/plugins/init.d.ts +2 -0
- package/dist/plugins/init.js +10 -1
- package/dist/project/project.js +50 -3
- package/dist/project/resolve.d.ts +7 -0
- package/dist/project/resolve.js +52 -5
- package/dist/server/permission/interactive.js +9 -4
- package/dist/server/permission/store.js +8 -2
- package/dist/server/routes/auth.js +28 -13
- package/dist/server/routes/chat.js +92 -26
- package/dist/server/routes/config.js +38 -10
- package/dist/server/routes/files.js +75 -16
- package/dist/server/routes/filesystem.js +9 -2
- package/dist/server/routes/kanban.js +146 -9
- package/dist/server/routes/permissions.js +32 -0
- package/dist/server/routes/project.js +15 -6
- package/dist/server/routes/provider.js +10 -5
- package/dist/server/routes/session.js +72 -26
- package/dist/server/routes/terminal.js +10 -2
- package/dist/server/routes/todo.js +18 -0
- package/dist/server/routes/tool.js +13 -2
- package/dist/server/routes/update.js +5 -1
- package/dist/server/routes/workflows.js +17 -6
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +31 -17
- package/dist/server/terminal/pty-manager.d.ts +3 -0
- package/dist/server/terminal/pty-manager.js +8 -3
- package/dist/server/types.d.ts +2 -0
- package/dist/server/util/json-body.d.ts +31 -0
- package/dist/server/util/json-body.js +38 -0
- package/dist/session/archive.js +4 -4
- package/dist/session/branch.d.ts +10 -1
- package/dist/session/branch.js +54 -9
- package/dist/session/compaction.d.ts +5 -0
- package/dist/session/compaction.js +15 -10
- package/dist/session/context.d.ts +7 -3
- package/dist/session/context.js +35 -9
- package/dist/session/import.js +53 -6
- package/dist/session/index.d.ts +2 -2
- package/dist/session/index.js +2 -2
- package/dist/session/message.d.ts +12 -3
- package/dist/session/message.js +44 -4
- package/dist/session/session.d.ts +11 -5
- package/dist/session/session.js +99 -88
- package/dist/session/shake.js +35 -25
- package/dist/session/squash.js +3 -4
- package/dist/session/token.d.ts +7 -8
- package/dist/session/token.js +19 -16
- package/dist/session/usage.js +4 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/round.d.ts +12 -0
- package/dist/shared/round.js +18 -0
- package/dist/shared/timer.d.ts +23 -0
- package/dist/shared/timer.js +27 -0
- package/dist/shared/types/tool.d.ts +4 -0
- package/dist/shared/utils/markdown-code.d.ts +15 -0
- package/dist/shared/utils/markdown-code.js +44 -0
- package/dist/shared/utils/path.d.ts +4 -1
- package/dist/shared/utils/path.js +5 -2
- package/dist/shared/utils/string.d.ts +5 -0
- package/dist/shared/utils/string.js +52 -0
- package/dist/tools/builtin/bash.js +79 -10
- package/dist/tools/builtin/edit.js +67 -8
- package/dist/tools/builtin/glob.js +150 -14
- package/dist/tools/builtin/grep.js +53 -11
- package/dist/tools/builtin/kanban.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +4 -1
- package/dist/tools/builtin/read.js +15 -2
- package/dist/tools/builtin/resolvers.d.ts +9 -2
- package/dist/tools/builtin/resolvers.js +20 -6
- package/dist/tools/builtin/task.js +107 -25
- package/dist/tools/builtin/todo.js +84 -9
- package/dist/tools/builtin/write.js +7 -1
- package/dist/tools/builtin/yield.js +9 -1
- package/dist/tools/executor.js +53 -1
- package/dist/tools/hashline/patch.js +114 -38
- package/dist/tools/truncate.js +45 -16
- package/dist/tools/validate.js +66 -20
- package/dist/tools/websearch/index.js +8 -4
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/scheduler.js +9 -2
- package/dist/update/snapshot.d.ts +14 -4
- package/dist/update/snapshot.js +22 -22
- package/dist/update/updater.d.ts +16 -2
- package/dist/update/updater.js +39 -8
- package/dist/update/version.d.ts +3 -1
- package/dist/update/version.js +63 -9
- package/package.json +1 -1
- package/dist/server/registry-config.js +0 -44
package/dist/kanban/store.js
CHANGED
|
@@ -40,6 +40,16 @@ class KanbanInvalidPriorityError extends Error {
|
|
|
40
40
|
this.value = value;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
/** 非有限 position(如 JSON 1e999 → Infinity)。
|
|
44
|
+
* 写入 position 列后 max(position) 恒 Infinity,后续追加卡片全部同值并列、
|
|
45
|
+
* 排序静默失效且无法自愈——入口显式拒绝。 */
|
|
46
|
+
class KanbanInvalidPositionError extends Error {
|
|
47
|
+
value;
|
|
48
|
+
constructor(value) {
|
|
49
|
+
super(`position 必须为有限数字,收到 ${value}`);
|
|
50
|
+
this.value = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
43
53
|
/** 卡片不存在(update/move 目标 id 无效)。 */
|
|
44
54
|
class KanbanCardNotFoundError extends Error {
|
|
45
55
|
cardId;
|
|
@@ -120,6 +130,32 @@ function createKanbanStore(handle, projectId) {
|
|
|
120
130
|
.limit(1);
|
|
121
131
|
return (row?.columns ?? []);
|
|
122
132
|
}
|
|
133
|
+
/** 本项目的活动板 id(不懒建):变更操作的作用域基准。无板 = 本项目无卡片。 */
|
|
134
|
+
async function currentBoardId() {
|
|
135
|
+
const [row] = await db
|
|
136
|
+
.select({ id: kanbanBoards.id })
|
|
137
|
+
.from(kanbanBoards)
|
|
138
|
+
.where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
|
|
139
|
+
.limit(1);
|
|
140
|
+
return row?.id ?? null;
|
|
141
|
+
}
|
|
142
|
+
/** 取本板卡片;不属于本板(其他项目的卡片 id / 不存在)抛 KanbanCardNotFoundError。
|
|
143
|
+
* store 以 projectId 构造,但 update/move/delete 按卡片 id 寻址——不校验作用域时
|
|
144
|
+
* `PATCH /api/kanban/<项目B>/cards/<项目A 的卡片 id>` 会静默改写 A 的看板
|
|
145
|
+
* (REST 路径里的 projectId 形同虚设),删除更会直接物理删除他板卡片。 */
|
|
146
|
+
async function cardInThisBoard(id) {
|
|
147
|
+
const boardId = await currentBoardId();
|
|
148
|
+
if (!boardId)
|
|
149
|
+
throw new KanbanCardNotFoundError(id);
|
|
150
|
+
const [row] = await db
|
|
151
|
+
.select()
|
|
152
|
+
.from(kanbanCards)
|
|
153
|
+
.where(and(eq(kanbanCards.id, id), eq(kanbanCards.boardId, boardId)))
|
|
154
|
+
.limit(1);
|
|
155
|
+
if (!row)
|
|
156
|
+
throw new KanbanCardNotFoundError(id);
|
|
157
|
+
return row;
|
|
158
|
+
}
|
|
123
159
|
/** 校验列存在;不存在抛 KanbanColumnNotFoundError(消息含可用列清单)。 */
|
|
124
160
|
function assertColumn(columns, columnId, op) {
|
|
125
161
|
if (!columns.some((c) => c.id === columnId)) {
|
|
@@ -191,6 +227,8 @@ function createKanbanStore(handle, projectId) {
|
|
|
191
227
|
},
|
|
192
228
|
async updateCard(id, patch) {
|
|
193
229
|
assertPriority(patch.priority);
|
|
230
|
+
// 作用域校验:卡片必须属于本项目的板(他板卡片 id → 404 语义)。
|
|
231
|
+
const existing = await cardInThisBoard(id);
|
|
194
232
|
const [row] = await db
|
|
195
233
|
.update(kanbanCards)
|
|
196
234
|
.set({
|
|
@@ -200,20 +238,18 @@ function createKanbanStore(handle, projectId) {
|
|
|
200
238
|
...(patch.labels !== undefined && { labels: patch.labels }),
|
|
201
239
|
updatedAt: new Date(),
|
|
202
240
|
})
|
|
203
|
-
.where(eq(kanbanCards.id, id))
|
|
241
|
+
.where(and(eq(kanbanCards.id, id), eq(kanbanCards.boardId, existing.boardId)))
|
|
204
242
|
.returning();
|
|
205
243
|
if (!row)
|
|
206
244
|
throw new KanbanCardNotFoundError(id);
|
|
207
245
|
return rowToCard(row);
|
|
208
246
|
},
|
|
209
247
|
async moveCard(id, columnId, position) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (!card)
|
|
216
|
-
throw new KanbanCardNotFoundError(id);
|
|
248
|
+
if (position !== undefined && !Number.isFinite(position)) {
|
|
249
|
+
throw new KanbanInvalidPositionError(position);
|
|
250
|
+
}
|
|
251
|
+
// 作用域校验(同时取回 boardId):他板卡片 id 不得经本项目路径移动。
|
|
252
|
+
const card = await cardInThisBoard(id);
|
|
217
253
|
// 悬空列校验:目标列必须存在于当前列配置,否则卡片静默不可见。
|
|
218
254
|
const columns = await boardColumns(card.boardId);
|
|
219
255
|
assertColumn(columns, columnId, 'move');
|
|
@@ -222,13 +258,25 @@ function createKanbanStore(handle, projectId) {
|
|
|
222
258
|
const [row] = await db
|
|
223
259
|
.update(kanbanCards)
|
|
224
260
|
.set({ columnId, position: newPos, updatedAt: new Date() })
|
|
225
|
-
.where(eq(kanbanCards.id, id))
|
|
261
|
+
.where(and(eq(kanbanCards.id, id), eq(kanbanCards.boardId, card.boardId)))
|
|
226
262
|
.returning();
|
|
227
|
-
|
|
228
|
-
|
|
263
|
+
if (!row)
|
|
264
|
+
throw new KanbanCardNotFoundError(id);
|
|
265
|
+
return rowToCard(row);
|
|
229
266
|
},
|
|
230
267
|
async deleteCard(id) {
|
|
231
|
-
|
|
268
|
+
// 作用域校验 + 删除影响行数校验:他板卡片 id 与不存在的 id 都必须报错——
|
|
269
|
+
// 此前无条件 `delete where id` 恒返回成功,REST 对任何 id 都回 ok:true,
|
|
270
|
+
// 删除失败(拼错 id / 卡片已被 agent 删除)静默无反馈。
|
|
271
|
+
const boardId = await currentBoardId();
|
|
272
|
+
if (!boardId)
|
|
273
|
+
throw new KanbanCardNotFoundError(id);
|
|
274
|
+
const rows = await db
|
|
275
|
+
.delete(kanbanCards)
|
|
276
|
+
.where(and(eq(kanbanCards.id, id), eq(kanbanCards.boardId, boardId)))
|
|
277
|
+
.returning({ id: kanbanCards.id });
|
|
278
|
+
if (rows.length === 0)
|
|
279
|
+
throw new KanbanCardNotFoundError(id);
|
|
232
280
|
},
|
|
233
281
|
async updateBoard(patch) {
|
|
234
282
|
const boardId = await getOrCreateBoardId();
|
|
@@ -315,7 +363,7 @@ function createKanbanStore(handle, projectId) {
|
|
|
315
363
|
},
|
|
316
364
|
};
|
|
317
365
|
}
|
|
318
|
-
export { createKanbanStore, KanbanCardNotFoundError, KanbanColumnInUseError, KanbanColumnNotFoundError, KanbanInvalidPriorityError, };
|
|
366
|
+
export { createKanbanStore, KanbanCardNotFoundError, KanbanColumnInUseError, KanbanColumnNotFoundError, KanbanInvalidPositionError, KanbanInvalidPriorityError, };
|
|
319
367
|
/** 把活动看板软删除进回收站(记录原项目名与工作目录)。项目行删除后 FK 将 projectId 置 null。 */
|
|
320
368
|
export async function softDeleteKanbanBoard(handle, projectId, projectName, worktree) {
|
|
321
369
|
const rows = await handle.db
|
|
@@ -89,6 +89,13 @@ type StepState = {
|
|
|
89
89
|
tools: ToolStreamState;
|
|
90
90
|
/** Finish reason seen but not yet finalized (waiting for trailing usage chunk). */
|
|
91
91
|
pendingFinish: FinishReason | null;
|
|
92
|
+
/** 已终结标记:finalize 后所有后续 chunk 一律忽略。
|
|
93
|
+
* 此前终结后未设标记——finish_reason+usage 同 chunk 完成终结后,再收到
|
|
94
|
+
* usage-only 尾 chunk(部分 provider 在 combined chunk 之后仍补发 usage 帧)
|
|
95
|
+
* 会再次走终结分支:lifecycle 已复位,重发 step-start/step-finish/finish
|
|
96
|
+
* (下游拿到重复 done + 重复 usage);携带非空 content 的尾 chunk 更会
|
|
97
|
+
* 产出「完成后幻影文本」,追加进已终结的轮次。 */
|
|
98
|
+
finalized: boolean;
|
|
92
99
|
};
|
|
93
100
|
declare const initialStepState: () => StepState;
|
|
94
101
|
/**
|
|
@@ -6,6 +6,14 @@ const joinSystem = (request) => request.system.map((p) => p.text).join('\n\n');
|
|
|
6
6
|
const toOpenAIContent = (part) => {
|
|
7
7
|
if (part.type === 'text')
|
|
8
8
|
return { type: 'text', text: part.text };
|
|
9
|
+
if (part.type === 'image') {
|
|
10
|
+
// OpenAI 多模态契约:dataURL 形式的 image_url。mediaType/data 来自用户
|
|
11
|
+
// 附件(data 为无前缀 base64)——此处拼出完整 dataURL 送达模型。
|
|
12
|
+
return {
|
|
13
|
+
type: 'image_url',
|
|
14
|
+
image_url: { url: `data:${part.mediaType};base64,${part.data}` },
|
|
15
|
+
};
|
|
16
|
+
}
|
|
9
17
|
return { type: 'text', text: '' };
|
|
10
18
|
};
|
|
11
19
|
const messageToOpenAI = (msg) => {
|
|
@@ -28,7 +36,7 @@ const messageToOpenAI = (msg) => {
|
|
|
28
36
|
const textParts = msg.content.filter((p) => p.type === 'text');
|
|
29
37
|
const toolCalls = msg.content.filter((p) => p.type === 'tool-call');
|
|
30
38
|
const content = msg.role === 'user'
|
|
31
|
-
?
|
|
39
|
+
? msg.content.filter((p) => p.type === 'text' || p.type === 'image').map(toOpenAIContent)
|
|
32
40
|
: textParts.map((p) => (p.type === 'text' ? p.text : '')).join('');
|
|
33
41
|
const message = {
|
|
34
42
|
role: msg.role,
|
|
@@ -104,6 +112,7 @@ const initialStepState = () => ({
|
|
|
104
112
|
lifecycle: lifecycleInitial(),
|
|
105
113
|
tools: emptyTools(),
|
|
106
114
|
pendingFinish: null,
|
|
115
|
+
finalized: false,
|
|
107
116
|
});
|
|
108
117
|
/** Map an OpenAI usage object to the internal Usage fields. */
|
|
109
118
|
const mapUsage = (raw) => ({
|
|
@@ -124,11 +133,17 @@ const finalize = (lifecycle, events, reason, usage) => lifecycleFinish(lifecycle
|
|
|
124
133
|
* trailing usage chunk triggers the finish. `done` is only true once finalized.
|
|
125
134
|
*/
|
|
126
135
|
const step = (state, chunk) => {
|
|
136
|
+
// 终结后的 chunk 一律忽略:终结是不可逆状态,后续帧(重复 usage、
|
|
137
|
+
// 迟到 delta)不得重发 finish 事件或把文本追加进已终结的轮次。
|
|
138
|
+
if (state.finalized) {
|
|
139
|
+
return { state, events: [], done: true };
|
|
140
|
+
}
|
|
127
141
|
const events = [];
|
|
128
142
|
let lifecycle = state.lifecycle;
|
|
129
143
|
let tools = state.tools;
|
|
130
144
|
let pendingFinish = state.pendingFinish;
|
|
131
145
|
let done = false;
|
|
146
|
+
let finalized = false;
|
|
132
147
|
const choice = chunk.choices?.[0];
|
|
133
148
|
if (choice !== undefined && choice.delta !== undefined) {
|
|
134
149
|
const delta = choice.delta;
|
|
@@ -165,6 +180,7 @@ const step = (state, chunk) => {
|
|
|
165
180
|
if (chunk.usage !== undefined) {
|
|
166
181
|
lifecycle = finalize(lifecycle, events, pendingFinish, mapUsage(chunk.usage));
|
|
167
182
|
done = true;
|
|
183
|
+
finalized = true;
|
|
168
184
|
pendingFinish = null;
|
|
169
185
|
}
|
|
170
186
|
}
|
|
@@ -174,9 +190,10 @@ const step = (state, chunk) => {
|
|
|
174
190
|
const reason = pendingFinish ?? 'stop';
|
|
175
191
|
lifecycle = finalize(lifecycle, events, reason, mapUsage(chunk.usage));
|
|
176
192
|
done = true;
|
|
193
|
+
finalized = true;
|
|
177
194
|
pendingFinish = null;
|
|
178
195
|
}
|
|
179
|
-
return { state: { lifecycle, tools, pendingFinish }, events, done };
|
|
196
|
+
return { state: { lifecycle, tools, pendingFinish, finalized }, events, done };
|
|
180
197
|
};
|
|
181
198
|
/** Finalize a stream that ended without a trailing usage chunk (no-op if already finished). */
|
|
182
199
|
const finalizeStream = (state) => {
|
|
@@ -20,9 +20,18 @@ type AppendOutcome = {
|
|
|
20
20
|
};
|
|
21
21
|
declare const empty: () => ToolStreamState;
|
|
22
22
|
/**
|
|
23
|
-
* Append a tool-call delta.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Append a tool-call delta.
|
|
24
|
+
*
|
|
25
|
+
* Identity(id/name)可能在后续 delta 才到达:部分兼容 provider 的首个
|
|
26
|
+
* tool_calls 片段只带 index 与 arguments,id/name 缺席。此前对无 id/name 的
|
|
27
|
+
* delta 整体丢弃——领先的 arguments 片段静默蒸发,工具入参被截断/损坏
|
|
28
|
+
* (JSON 解析失败或语义漂移的「执行了错误参数」)。正确语义:
|
|
29
|
+
* 1. 身份未齐时创建占位条目,片段只累积、不发事件(无 id 无法发事件);
|
|
30
|
+
* 2. 身份到达时补发 tool-input-start + 已缓冲的 tool-input-delta,再追加
|
|
31
|
+
* 当前片段——事件顺序与参数内容完整;
|
|
32
|
+
* 3. 身份始终缺失的条目在 finishAll 静默跳过(无法映射为工具调用,
|
|
33
|
+
* 与旧的「整体丢弃」等价——空 id 的 tool call 会在下一轮触发
|
|
34
|
+
* invalid tool_call_id)。
|
|
26
35
|
*/
|
|
27
36
|
declare const appendOrStart: (state: ToolStreamState, delta: DeltaInput) => AppendOutcome;
|
|
28
37
|
type FinishedTool = {
|
|
@@ -1,41 +1,61 @@
|
|
|
1
1
|
import { isLLMError, llmError, reasonMessage } from '../../schema/errors.js';
|
|
2
2
|
const empty = () => ({});
|
|
3
3
|
/**
|
|
4
|
-
* Append a tool-call delta.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Append a tool-call delta.
|
|
5
|
+
*
|
|
6
|
+
* Identity(id/name)可能在后续 delta 才到达:部分兼容 provider 的首个
|
|
7
|
+
* tool_calls 片段只带 index 与 arguments,id/name 缺席。此前对无 id/name 的
|
|
8
|
+
* delta 整体丢弃——领先的 arguments 片段静默蒸发,工具入参被截断/损坏
|
|
9
|
+
* (JSON 解析失败或语义漂移的「执行了错误参数」)。正确语义:
|
|
10
|
+
* 1. 身份未齐时创建占位条目,片段只累积、不发事件(无 id 无法发事件);
|
|
11
|
+
* 2. 身份到达时补发 tool-input-start + 已缓冲的 tool-input-delta,再追加
|
|
12
|
+
* 当前片段——事件顺序与参数内容完整;
|
|
13
|
+
* 3. 身份始终缺失的条目在 finishAll 静默跳过(无法映射为工具调用,
|
|
14
|
+
* 与旧的「整体丢弃」等价——空 id 的 tool call 会在下一轮触发
|
|
15
|
+
* invalid tool_call_id)。
|
|
7
16
|
*/
|
|
8
17
|
const appendOrStart = (state, delta) => {
|
|
9
18
|
const current = state[delta.index];
|
|
10
19
|
const events = [];
|
|
11
20
|
let next;
|
|
12
21
|
if (current === undefined) {
|
|
13
|
-
|
|
14
|
-
const name = delta.name;
|
|
15
|
-
// 无效 id/name(缺失或空字符串):丢弃该 delta 而非抛错。部分兼容
|
|
16
|
-
// provider 把 tool call 的 arguments 流式片段拆成多个独立 delta,每片
|
|
17
|
-
// id/name 为空。接受空值会产生空 id 的 tool call,导致下一轮
|
|
18
|
-
// invalid tool_call_id;抛错则让整个流崩溃。跳过最稳妥。
|
|
19
|
-
if (!id || !name) {
|
|
20
|
-
return { state, events };
|
|
21
|
-
}
|
|
22
|
-
next = { id, name, input: '', started: false };
|
|
23
|
-
const start = { type: 'tool-input-start', id, name };
|
|
24
|
-
events.push(start);
|
|
25
|
-
next.started = true;
|
|
22
|
+
next = { id: delta.id ?? '', name: delta.name ?? '', input: '', started: false };
|
|
26
23
|
}
|
|
27
24
|
else {
|
|
28
25
|
next = { ...current };
|
|
29
26
|
}
|
|
27
|
+
// 回填迟到的身份字段(已就位的字段不受后续 delta 的空值影响)
|
|
28
|
+
if (next.id === '' && delta.id)
|
|
29
|
+
next.id = delta.id;
|
|
30
|
+
if (next.name === '' && delta.name)
|
|
31
|
+
next.name = delta.name;
|
|
32
|
+
const identityReady = next.id !== '' && next.name !== '';
|
|
33
|
+
if (identityReady && !next.started) {
|
|
34
|
+
const start = { type: 'tool-input-start', id: next.id, name: next.name };
|
|
35
|
+
events.push(start);
|
|
36
|
+
next.started = true;
|
|
37
|
+
// 身份到达前已缓冲的片段按到达顺序补发
|
|
38
|
+
if (next.input.length > 0) {
|
|
39
|
+
const buffered = {
|
|
40
|
+
type: 'tool-input-delta',
|
|
41
|
+
id: next.id,
|
|
42
|
+
name: next.name,
|
|
43
|
+
text: next.input,
|
|
44
|
+
};
|
|
45
|
+
events.push(buffered);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
30
48
|
if (delta.argumentsDelta !== undefined && delta.argumentsDelta.length > 0) {
|
|
31
49
|
next.input += delta.argumentsDelta;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
50
|
+
if (next.started) {
|
|
51
|
+
const d = {
|
|
52
|
+
type: 'tool-input-delta',
|
|
53
|
+
id: next.id,
|
|
54
|
+
name: next.name,
|
|
55
|
+
text: delta.argumentsDelta,
|
|
56
|
+
};
|
|
57
|
+
events.push(d);
|
|
58
|
+
}
|
|
39
59
|
}
|
|
40
60
|
return { state: { ...state, [delta.index]: next }, events };
|
|
41
61
|
};
|
|
@@ -64,6 +84,10 @@ const finishAll = (state) => {
|
|
|
64
84
|
const tool = state[Number(key)];
|
|
65
85
|
if (tool === undefined)
|
|
66
86
|
continue;
|
|
87
|
+
// 身份始终缺失的占位条目(appendOrStart 缓冲后 id/name 从未到达)无法
|
|
88
|
+
// 映射为工具调用——静默跳过,与旧的「整体丢弃」口径一致。
|
|
89
|
+
if (!tool.started)
|
|
90
|
+
continue;
|
|
67
91
|
const end = { type: 'tool-input-end', id: tool.id, name: tool.name };
|
|
68
92
|
events.push(end);
|
|
69
93
|
// 解析失败不抛错:模型可能输出不完整 JSON(流被截断/提前结束)。
|
package/dist/llm/provider.js
CHANGED
|
@@ -41,7 +41,10 @@ const toInternalMessage = (msg) => {
|
|
|
41
41
|
const parts = msg.content.map((p) => {
|
|
42
42
|
if (p.type === 'text')
|
|
43
43
|
return { type: 'text', text: p.text };
|
|
44
|
-
|
|
44
|
+
// 图片数据必须送达 provider:此前降级为 `[image: <mediaType>]` 文本占位符,
|
|
45
|
+
// base64 静默丢弃——模型收到的只是一行文字,多模态能力在最后一公里失效
|
|
46
|
+
// (前端附件/持久化/token 估算全部就绪,唯独此处丢数据)。
|
|
47
|
+
return { type: 'image', mediaType: p.mediaType, data: p.data };
|
|
45
48
|
});
|
|
46
49
|
// assistant messages may carry tool_calls
|
|
47
50
|
if (msg.role === 'assistant' && msg.toolCalls !== undefined) {
|
|
@@ -164,10 +167,22 @@ const chatStream = async function* (ctx, request, options) {
|
|
|
164
167
|
const targets = [chain.primary, ...chain.fallbacks];
|
|
165
168
|
let started = false;
|
|
166
169
|
let lastError;
|
|
167
|
-
for (
|
|
170
|
+
for (let i = 0; i < targets.length; i++) {
|
|
171
|
+
const target = targets[i];
|
|
168
172
|
if (target === undefined)
|
|
169
173
|
continue;
|
|
170
|
-
|
|
174
|
+
try {
|
|
175
|
+
resolveRoute(ctx.registry, target.provider, target.model);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
// 主路由未注册 = 配置错误 → 快速失败;回退目标未注册(provider 被跳过注册:
|
|
179
|
+
// apiKey 跨机不可解/缺 baseURL/畸形条目)只跳过该目标——此前整条链被
|
|
180
|
+
// NoRoute 击穿,可用回退永不尝试,原始错误也被顶替。
|
|
181
|
+
if (i === 0)
|
|
182
|
+
throw error;
|
|
183
|
+
lastError = error;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
171
186
|
try {
|
|
172
187
|
for await (const sc of streamTarget(ctx, request, target, chain)) {
|
|
173
188
|
started = true;
|
package/dist/llm/retry.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LLMErrorReason, RetryPolicy } from './schema/errors.js';
|
|
2
2
|
declare const RETRY_INITIAL_DELAY = 2000;
|
|
3
3
|
declare const RETRY_MAX_DELAY_NO_HEADERS = 30000;
|
|
4
|
+
/** 32 位 timer 安全上限(> 此值 setTimeout 钳到 1ms);与 shared MAX_TIMER_MS 单一来源。 */
|
|
4
5
|
declare const RETRY_MAX_DELAY = 2147483647;
|
|
5
6
|
/**
|
|
6
7
|
* Compute the delay before the next retry attempt (ms).
|
package/dist/llm/retry.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { MAX_TIMER_MS } from '../shared/timer.js';
|
|
1
2
|
import { isLLMError, reasonRetryAfterMs, retryPolicy } from './schema/errors.js';
|
|
2
3
|
const RETRY_INITIAL_DELAY = 2_000;
|
|
3
4
|
const RETRY_BACKOFF_FACTOR = 2;
|
|
4
5
|
const RETRY_MAX_DELAY_NO_HEADERS = 30_000;
|
|
5
|
-
|
|
6
|
+
/** 32 位 timer 安全上限(> 此值 setTimeout 钳到 1ms);与 shared MAX_TIMER_MS 单一来源。 */
|
|
7
|
+
const RETRY_MAX_DELAY = MAX_TIMER_MS;
|
|
6
8
|
/** Extract response headers from an LLMError's http context (if any). */
|
|
7
9
|
const errorHeaders = (error) => {
|
|
8
10
|
if (!isLLMError(error))
|
|
@@ -15,6 +17,14 @@ const errorHeaders = (error) => {
|
|
|
15
17
|
};
|
|
16
18
|
/** Cap a delay to the 32-bit safe ceiling. */
|
|
17
19
|
const capDelay = (ms) => Math.min(ms, RETRY_MAX_DELAY);
|
|
20
|
+
/** 严格解析「正有限毫秒数」:Infinity('1e999')/NaN/负数/零一律拒绝。
|
|
21
|
+
* parseFloat('1e999') = Infinity 能穿过 Number.isNaN 检查——capDelay(Infinity)
|
|
22
|
+
* 后单次重试延迟 2^31ms ≈ 24.8 天,且 RateLimit 的 policy maxDelay=Infinity
|
|
23
|
+
* 不拦截,agent run 实际挂死。 */
|
|
24
|
+
const parsePositiveFinite = (s) => {
|
|
25
|
+
const n = Number.parseFloat(s);
|
|
26
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
27
|
+
};
|
|
18
28
|
/**
|
|
19
29
|
* Compute the delay before the next retry attempt (ms).
|
|
20
30
|
* Honors retry-after / retry-after-ms headers when present, else exponential backoff
|
|
@@ -22,25 +32,28 @@ const capDelay = (ms) => Math.min(ms, RETRY_MAX_DELAY);
|
|
|
22
32
|
*/
|
|
23
33
|
const delay = (attempt, error, initialDelayMs = RETRY_INITIAL_DELAY) => {
|
|
24
34
|
const headers = errorHeaders(error);
|
|
35
|
+
// 无可用重试指示时统一 30s 上限退避。此前仅无 headers 路径 cap 30s——headers
|
|
36
|
+
// 存在但缺/坏 retry-after(如 500 错误携带其他头)时退避无上限,attempt=10
|
|
37
|
+
// 即膨胀到 2^9*2000 ≈ 17 分钟。
|
|
38
|
+
const backoff = () => capDelay(Math.min(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1), RETRY_MAX_DELAY_NO_HEADERS));
|
|
25
39
|
if (headers) {
|
|
26
40
|
const retryAfterMs = headers['retry-after-ms'];
|
|
27
41
|
if (retryAfterMs !== undefined) {
|
|
28
|
-
const parsedMs =
|
|
29
|
-
if (
|
|
42
|
+
const parsedMs = parsePositiveFinite(retryAfterMs);
|
|
43
|
+
if (parsedMs !== null)
|
|
30
44
|
return capDelay(parsedMs);
|
|
31
45
|
}
|
|
32
46
|
const retryAfter = headers['retry-after'];
|
|
33
47
|
if (retryAfter !== undefined) {
|
|
34
|
-
const parsedSeconds =
|
|
35
|
-
if (
|
|
48
|
+
const parsedSeconds = parsePositiveFinite(retryAfter);
|
|
49
|
+
if (parsedSeconds !== null)
|
|
36
50
|
return capDelay(Math.ceil(parsedSeconds * 1000));
|
|
37
51
|
const parsed = Date.parse(retryAfter) - Date.now();
|
|
38
52
|
if (!Number.isNaN(parsed) && parsed > 0)
|
|
39
53
|
return capDelay(Math.ceil(parsed));
|
|
40
54
|
}
|
|
41
|
-
return capDelay(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1));
|
|
42
55
|
}
|
|
43
|
-
return
|
|
56
|
+
return backoff();
|
|
44
57
|
};
|
|
45
58
|
/**
|
|
46
59
|
* Decide whether a thrown error is retryable. Returns undefined when not retryable
|
package/dist/llm/routing.js
CHANGED
|
@@ -14,8 +14,20 @@ const runWithFallback = async (registry, chain, run) => {
|
|
|
14
14
|
const target = targets[i];
|
|
15
15
|
if (target === undefined)
|
|
16
16
|
continue;
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
// Validate the route exists before attempting.
|
|
19
|
+
resolveRoute(registry, target.provider, target.model);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// 主路由未注册 = 配置错误 → 快速失败(不静默改用回退 provider)。
|
|
23
|
+
if (i === 0)
|
|
24
|
+
throw error;
|
|
25
|
+
// 回退目标未注册(provider 因 apiKey 跨机不可解/缺 baseURL/畸形条目被跳过
|
|
26
|
+
// 注册,但 config.providers 里仍在、buildFallbackChain 照样选中)只应跳过
|
|
27
|
+
// 该目标——此前整条链被 NoRoute 击穿:可用回退永不尝试,原始错误也被顶替。
|
|
28
|
+
lastError = error;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
19
31
|
try {
|
|
20
32
|
const result = await withRetry(() => run(target.provider, target.model), {
|
|
21
33
|
maxRetries: chain.maxRetries,
|
|
@@ -45,7 +45,14 @@ type ToolResultPart = {
|
|
|
45
45
|
cache?: CacheHint;
|
|
46
46
|
providerMetadata?: ProviderMetadata;
|
|
47
47
|
};
|
|
48
|
-
type
|
|
48
|
+
type ImagePart = {
|
|
49
|
+
type: 'image';
|
|
50
|
+
mediaType: string;
|
|
51
|
+
/** base64 编码(不含 `data:` 前缀)。 */
|
|
52
|
+
data: string;
|
|
53
|
+
providerMetadata?: ProviderMetadata;
|
|
54
|
+
};
|
|
55
|
+
type ContentPart = TextPart | ReasoningPart | ToolCallPart | ToolResultPart | ImagePart;
|
|
49
56
|
type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
50
57
|
type Message = {
|
|
51
58
|
id?: string;
|
|
@@ -93,5 +100,5 @@ type InternalRequest = {
|
|
|
93
100
|
};
|
|
94
101
|
/** Immutable patch of an InternalRequest. */
|
|
95
102
|
declare const requestUpdate: (req: InternalRequest, patch: Partial<InternalRequest>) => InternalRequest;
|
|
96
|
-
export type { ContentPart, InternalRequest, Message, MessageRole, ReasoningPart, ResponseFormat, SystemPart, TextPart, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart, ToolResultValue, };
|
|
103
|
+
export type { ContentPart, ImagePart, InternalRequest, Message, MessageRole, ReasoningPart, ResponseFormat, SystemPart, TextPart, ToolCallPart, ToolChoice, ToolDefinition, ToolResultPart, ToolResultValue, };
|
|
97
104
|
export { messageAssistant, messageSystem, messageUser, requestUpdate };
|
package/dist/llm/token.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJK-aware token estimate.
|
|
3
|
+
* Chinese/CJK characters ≈ 2 tokens each (denser encoding).
|
|
4
|
+
* Other characters ≈ 4 chars/token (standard heuristic).
|
|
5
|
+
*
|
|
6
|
+
* 单一权威实现:session/token 此前持有一套 CJK 感知实现、本模块持有一套
|
|
7
|
+
* 「chars/4」朴素实现,同一字符串在两处给出不同估算(中文低估 ~8 倍),
|
|
8
|
+
* 预算/压缩口径随调用点漂移——已收敛至此。
|
|
9
|
+
*/
|
|
1
10
|
declare const estimateTokens: (text: string) => number;
|
|
2
11
|
export { estimateTokens };
|
package/dist/llm/token.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* CJK-aware token estimate.
|
|
3
|
+
* Chinese/CJK characters ≈ 2 tokens each (denser encoding).
|
|
4
|
+
* Other characters ≈ 4 chars/token (standard heuristic).
|
|
5
|
+
*
|
|
6
|
+
* 单一权威实现:session/token 此前持有一套 CJK 感知实现、本模块持有一套
|
|
7
|
+
* 「chars/4」朴素实现,同一字符串在两处给出不同估算(中文低估 ~8 倍),
|
|
8
|
+
* 预算/压缩口径随调用点漂移——已收敛至此。
|
|
6
9
|
*/
|
|
7
|
-
const TOKEN_CHARS_RATIO = 4;
|
|
8
10
|
const estimateTokens = (text) => {
|
|
9
11
|
if (text.length === 0)
|
|
10
12
|
return 0;
|
|
11
|
-
|
|
13
|
+
const cjkCount = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g)?.length ?? 0;
|
|
14
|
+
const otherCount = text.length - cjkCount;
|
|
15
|
+
return Math.ceil(cjkCount * 2 + otherCount / 4);
|
|
12
16
|
};
|
|
13
17
|
export { estimateTokens };
|
package/dist/llm/transport.js
CHANGED
|
@@ -16,6 +16,9 @@ const sseFraming = async function* (stream) {
|
|
|
16
16
|
if (done)
|
|
17
17
|
break;
|
|
18
18
|
buffer += decoder.decode(value, { stream: true });
|
|
19
|
+
// SSE 规范允许 CRLF 行结束(部分网关/代理改写行结束符)——归一化为 LF
|
|
20
|
+
// 再分帧,否则 \r\n\r\n 分隔的事件永远匹配不到 \n\n 而滞留缓冲区。
|
|
21
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
19
22
|
let sepIndex = buffer.indexOf('\n\n');
|
|
20
23
|
while (sepIndex !== -1) {
|
|
21
24
|
const block = buffer.slice(0, sepIndex);
|
|
@@ -154,14 +157,16 @@ const classifyHttpError = (status, body, url, headers) => {
|
|
|
154
157
|
});
|
|
155
158
|
};
|
|
156
159
|
const parseRetryAfterMs = (retryAfterMs, retryAfter) => {
|
|
160
|
+
// 与 retry.ts delay() 同口径:仅正有限数('1e999' → Infinity 必须拒绝,
|
|
161
|
+
// 否则塞进 reason 后重试层延迟 2^31ms ≈ 24.8 天挂死 agent run)。
|
|
157
162
|
if (retryAfterMs !== null && retryAfterMs !== undefined) {
|
|
158
163
|
const parsed = Number.parseFloat(retryAfterMs);
|
|
159
|
-
if (
|
|
164
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
160
165
|
return Math.ceil(parsed);
|
|
161
166
|
}
|
|
162
167
|
if (retryAfter !== null && retryAfter !== undefined) {
|
|
163
168
|
const parsed = Number.parseFloat(retryAfter);
|
|
164
|
-
if (
|
|
169
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
165
170
|
return Math.ceil(parsed * 1000);
|
|
166
171
|
}
|
|
167
172
|
return undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MCPTransport } from './transport.js';
|
|
2
|
+
type MCPClient = {
|
|
3
|
+
request: (method: string, params?: unknown, timeoutMs?: number) => Promise<unknown>;
|
|
4
|
+
notify: (method: string, params?: unknown) => void;
|
|
5
|
+
close: () => void;
|
|
6
|
+
};
|
|
7
|
+
/** 默认单请求超时(initialize/tools/list 用);工具调用在调用点传更长的值。 */
|
|
8
|
+
declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
9
|
+
declare function createMCPClient(transport: MCPTransport): MCPClient;
|
|
10
|
+
export type { MCPClient };
|
|
11
|
+
export { createMCPClient, DEFAULT_REQUEST_TIMEOUT_MS };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// MCP JSON-RPC 客户端(spec §6.1):request/response 配对(seq id)、超时、
|
|
2
|
+
// notification 无响应、连接关闭时 reject 全部 pending。协议纯逻辑,transport 注入。
|
|
3
|
+
/** 默认单请求超时(initialize/tools/list 用);工具调用在调用点传更长的值。 */
|
|
4
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
5
|
+
function createMCPClient(transport) {
|
|
6
|
+
let nextId = 1;
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
const failAll = (message) => {
|
|
9
|
+
for (const p of pending.values()) {
|
|
10
|
+
if (p.timer)
|
|
11
|
+
clearTimeout(p.timer);
|
|
12
|
+
p.reject(new Error(message));
|
|
13
|
+
}
|
|
14
|
+
pending.clear();
|
|
15
|
+
};
|
|
16
|
+
transport.onMessage((msg) => {
|
|
17
|
+
if (!('id' in msg))
|
|
18
|
+
return; // 服务端通知:当前无订阅方,忽略
|
|
19
|
+
const p = pending.get(msg.id);
|
|
20
|
+
if (!p)
|
|
21
|
+
return; // 已超时/已响应/未知 id
|
|
22
|
+
pending.delete(msg.id);
|
|
23
|
+
if (p.timer)
|
|
24
|
+
clearTimeout(p.timer);
|
|
25
|
+
if (msg.error) {
|
|
26
|
+
p.reject(new Error(`MCP ${msg.error.message} (code ${msg.error.code}${msg.error.data ? `, data: ${JSON.stringify(msg.error.data)}` : ''})`));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
p.resolve(msg.result);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
transport.onClose(() => {
|
|
33
|
+
failAll('MCP transport closed');
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
request(method, params, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
37
|
+
const id = nextId;
|
|
38
|
+
nextId += 1;
|
|
39
|
+
const msg = {
|
|
40
|
+
jsonrpc: '2.0',
|
|
41
|
+
id,
|
|
42
|
+
method,
|
|
43
|
+
...(params !== undefined ? { params } : {}),
|
|
44
|
+
};
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const timer = timeoutMs > 0
|
|
47
|
+
? setTimeout(() => {
|
|
48
|
+
pending.delete(id);
|
|
49
|
+
reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
|
|
50
|
+
}, timeoutMs)
|
|
51
|
+
: undefined;
|
|
52
|
+
pending.set(id, { resolve, reject, timer });
|
|
53
|
+
transport.send(msg);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
notify(method, params) {
|
|
57
|
+
const msg = {
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
method,
|
|
60
|
+
...(params !== undefined ? { params } : {}),
|
|
61
|
+
};
|
|
62
|
+
transport.send(msg);
|
|
63
|
+
},
|
|
64
|
+
close() {
|
|
65
|
+
failAll('MCP client closed');
|
|
66
|
+
transport.close();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export { createMCPClient, DEFAULT_REQUEST_TIMEOUT_MS };
|