godot-daedalus_backend 1.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/README.md +101 -0
- package/bin/godot-daedalus-backend.js +4 -0
- package/bin/godot-daedalus-mcp.js +4 -0
- package/bin/godot-daedalus-terminal-mcp.js +4 -0
- package/bin/run-tsx-entry.js +26 -0
- package/package.json +54 -0
- package/scripts/deepseek-tokenizer-server.py +54 -0
- package/src/app-paths.ts +36 -0
- package/src/main.ts +21 -0
- package/src/mcp/content-length-protocol.ts +68 -0
- package/src/mcp/custom-mcp-config-store.ts +397 -0
- package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
- package/src/mcp/godot-editor-bridge.ts +307 -0
- package/src/mcp/godot-mcp-server.ts +3484 -0
- package/src/mcp/godot-paths.ts +151 -0
- package/src/mcp/godot-project-settings.ts +233 -0
- package/src/mcp/godot-tool-registration.ts +46 -0
- package/src/mcp/mcp-config.ts +48 -0
- package/src/mcp/mcp-host.ts +393 -0
- package/src/mcp/mcp-session.ts +81 -0
- package/src/mcp/terminal-mcp-server.ts +576 -0
- package/src/mcp/tscn-tools.ts +302 -0
- package/src/mcp/types.ts +12 -0
- package/src/ping-client.ts +24 -0
- package/src/prompts/registry.ts +97 -0
- package/src/prompts/templates/backend-helper.md +25 -0
- package/src/prompts/templates/gdscript-reviewer.md +19 -0
- package/src/prompts/templates/godot-assistant.md +225 -0
- package/src/prompts/templates/scene-architect.md +15 -0
- package/src/prompts/templates/session-compressor.md +33 -0
- package/src/protocol/schema.ts +486 -0
- package/src/protocol/types.ts +77 -0
- package/src/providers/deepseek-agent.ts +1014 -0
- package/src/providers/deepseek-client.ts +114 -0
- package/src/providers/deepseek-dsml-tools.ts +90 -0
- package/src/providers/deepseek-loose-tools.ts +450 -0
- package/src/providers/provider-config-store.ts +164 -0
- package/src/server/client-session.ts +93 -0
- package/src/server/request-dispatcher.ts +74 -0
- package/src/server/response-helpers.ts +33 -0
- package/src/server/send-json.ts +8 -0
- package/src/server/websocket-server.ts +3997 -0
- package/src/session/session-compressor.ts +68 -0
- package/src/session/session-store.ts +669 -0
- package/src/skills/registry.ts +180 -0
- package/src/skills/templates/backend-helper.md +12 -0
- package/src/skills/templates/file-creator.md +14 -0
- package/src/skills/templates/gdscript-review.md +12 -0
- package/src/skills/templates/godot-project-init.md +29 -0
- package/src/skills/templates/scene-builder.md +12 -0
- package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
- package/src/tokens/model-profiles.ts +38 -0
- package/src/tokens/token-counter-factory.ts +52 -0
- package/src/tokens/token-counter.ts +22 -0
- package/src/tools/approval-gateway.ts +111 -0
- package/src/tools/llm-tools.ts +1415 -0
- package/src/tools/tool-dispatcher.ts +147 -0
- package/src/tools/tool-event-describer.ts +387 -0
- package/src/tools/tool-idempotency.ts +373 -0
- package/src/tools/tool-policy-table.ts +61 -0
- package/src/tools/tool-policy.ts +73 -0
- package/src/workflow/llm-planner.ts +407 -0
- package/src/workflow/planner.ts +201 -0
- package/src/workflow/runner.ts +141 -0
- package/src/workflow/types.ts +69 -0
- package/src/workspace/registry.ts +104 -0
- package/src/workspace/types.ts +7 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type {
|
|
3
|
+
ChatCompletionMessageParam,
|
|
4
|
+
ChatCompletionMessageToolCall,
|
|
5
|
+
ChatCompletionMessageFunctionToolCall,
|
|
6
|
+
ChatCompletionTool,
|
|
7
|
+
ChatCompletionToolMessageParam,
|
|
8
|
+
ChatCompletionCreateParamsNonStreaming,
|
|
9
|
+
ChatCompletionCreateParamsStreaming,
|
|
10
|
+
ChatCompletionChunk
|
|
11
|
+
} from "openai/resources/chat/completions";
|
|
12
|
+
import type { AiChatParams, ChatMessage } from "../protocol/types.js";
|
|
13
|
+
import {
|
|
14
|
+
createDeepSeekClient,
|
|
15
|
+
createMessages,
|
|
16
|
+
applyChatOptions,
|
|
17
|
+
type DeepSeekChatOptions
|
|
18
|
+
} from "../providers/deepseek-client.js";
|
|
19
|
+
import type { McpHost } from "../mcp/mcp-host.js";
|
|
20
|
+
import { getToolDefinitions, getToolDefinitionsForNames, DEFAULT_TOOL_STEPS, resolveToolBudget, MAX_TOTAL_TOOL_RESULT_CHARS } from "../tools/llm-tools.js";
|
|
21
|
+
import { dispatchToolCalls, ToolApprovalRequiredError, type OnToolEvent } from "../tools/tool-dispatcher.js";
|
|
22
|
+
import { ApprovalGateway } from "../tools/approval-gateway.js";
|
|
23
|
+
import { containsDsmlToolCalls, parseDsmlToolCalls, stripDsmlToolCalls } from "./deepseek-dsml-tools.js";
|
|
24
|
+
import { containsLooseToolCalls, isKnownLooseToolTagName, isPotentialLooseToolTagName, normalizeKnownToolName, parseLooseToolCalls, stripLooseToolCalls } from "./deepseek-loose-tools.js";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
27
|
+
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
28
|
+
const FINALIZE_AFTER_TOOL_LIMIT_PROMPT: string =
|
|
29
|
+
"工具调用阶段已经达到后端限制。请停止请求更多工具,基于目前已经获得的工具结果直接回答用户。"
|
|
30
|
+
+ "如果信息不完整,请明确说明哪些部分是根据已有信息总结的,哪些部分还需要进一步检查。";
|
|
31
|
+
const RETRY_WITHOUT_TOOL_SYNTAX_PROMPT: string =
|
|
32
|
+
"上一条候选回复包含内部工具调用标签,但当前阶段不允许调用工具。"
|
|
33
|
+
+ "请不要输出 XML、DSML、函数调用标签或任何工具请求。"
|
|
34
|
+
+ "直接基于对话和已有工具结果给用户最终答复;如果缺少信息,请简短说明限制。";
|
|
35
|
+
|
|
36
|
+
export type DeepSeekAgentResult =
|
|
37
|
+
| { status: "completed"; text: string }
|
|
38
|
+
| {
|
|
39
|
+
status: "approval_required";
|
|
40
|
+
approvalId: string;
|
|
41
|
+
toolName: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
continuation: DeepSeekAgentContinuation;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type DeepSeekAgentContinuation = {
|
|
47
|
+
messages: ChatCompletionMessageParam[];
|
|
48
|
+
nextStep: number;
|
|
49
|
+
totalToolResultChars: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ApprovedToolResult = {
|
|
53
|
+
toolCallId: string;
|
|
54
|
+
content: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type StreamedAssistantMessage = {
|
|
58
|
+
contentText: string;
|
|
59
|
+
reasoningContent: string;
|
|
60
|
+
toolCalls: ChatCompletionMessageToolCall[];
|
|
61
|
+
emittedContentText: string;
|
|
62
|
+
suppressedToolSyntax: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type ToolCallAccumulator = {
|
|
66
|
+
index: number;
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
argumentsText: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function estimateTextChars(text: string): number {
|
|
73
|
+
return text.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractTextContent(content: ChatCompletionMessageParam["content"]): string {
|
|
77
|
+
if (typeof content === "string") {
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(content)) {
|
|
82
|
+
return content
|
|
83
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
84
|
+
.map((part): string => part.text)
|
|
85
|
+
.join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isFunctionToolCall(toolCall: ChatCompletionMessageToolCall): toolCall is ChatCompletionMessageFunctionToolCall {
|
|
92
|
+
return toolCall.type === "function";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getAllowedToolNames(tools: ChatCompletionTool[]): ReadonlySet<string> {
|
|
96
|
+
const allowedToolNames: Set<string> = new Set();
|
|
97
|
+
|
|
98
|
+
for (const tool of tools) {
|
|
99
|
+
if (tool.type === "function") {
|
|
100
|
+
allowedToolNames.add(tool.function.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return allowedToolNames;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeToolCallForAllowedTools(
|
|
108
|
+
toolCall: ChatCompletionMessageToolCall,
|
|
109
|
+
allowedToolNames: ReadonlySet<string>
|
|
110
|
+
): ChatCompletionMessageToolCall | null {
|
|
111
|
+
if (!isFunctionToolCall(toolCall)) {
|
|
112
|
+
return toolCall;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalizedName: string = normalizeKnownToolName(toolCall.function.name) ?? toolCall.function.name;
|
|
116
|
+
if (!allowedToolNames.has(normalizedName)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (normalizedName === toolCall.function.name) {
|
|
121
|
+
return toolCall;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
...toolCall,
|
|
126
|
+
function: {
|
|
127
|
+
...toolCall.function,
|
|
128
|
+
name: normalizedName
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function filterToolCallsForAllowedTools(
|
|
134
|
+
toolCalls: ChatCompletionMessageToolCall[],
|
|
135
|
+
allowedToolNames: ReadonlySet<string>
|
|
136
|
+
): ChatCompletionMessageToolCall[] {
|
|
137
|
+
const filteredToolCalls: ChatCompletionMessageToolCall[] = [];
|
|
138
|
+
|
|
139
|
+
for (const toolCall of toolCalls) {
|
|
140
|
+
const normalizedToolCall: ChatCompletionMessageToolCall | null = normalizeToolCallForAllowedTools(toolCall, allowedToolNames);
|
|
141
|
+
if (normalizedToolCall !== null) {
|
|
142
|
+
filteredToolCalls.push(normalizedToolCall);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return filteredToolCalls;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function containsKnownToolSyntax(text: string | null | undefined): boolean {
|
|
150
|
+
return containsDsmlToolCalls(text) || containsLooseToolCalls(text);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stripKnownToolSyntax(text: string): string {
|
|
154
|
+
return stripLooseToolCalls(stripDsmlToolCalls(text));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseTextToolCalls(
|
|
158
|
+
text: string,
|
|
159
|
+
step: number,
|
|
160
|
+
allowedToolNames: ReadonlySet<string>
|
|
161
|
+
): ChatCompletionMessageToolCall[] {
|
|
162
|
+
const dsmlToolCalls: ChatCompletionMessageToolCall[] = containsDsmlToolCalls(text)
|
|
163
|
+
? filterToolCallsForAllowedTools(parseDsmlToolCalls(text, `dsml-step-${step}`), allowedToolNames)
|
|
164
|
+
: [];
|
|
165
|
+
const looseToolCalls: ChatCompletionMessageToolCall[] = containsLooseToolCalls(text, allowedToolNames)
|
|
166
|
+
? parseLooseToolCalls(text, `loose-step-${step}`, allowedToolNames)
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
return [...dsmlToolCalls, ...looseToolCalls];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function extractToolNames(toolCalls: ChatCompletionMessageToolCall[]): string[] {
|
|
173
|
+
const toolNames: Set<string> = new Set();
|
|
174
|
+
|
|
175
|
+
for (const toolCall of toolCalls) {
|
|
176
|
+
if (isFunctionToolCall(toolCall)) {
|
|
177
|
+
toolNames.add(normalizeKnownToolName(toolCall.function.name) ?? toolCall.function.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [...toolNames];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createToolCallPreludeDelta(
|
|
185
|
+
contentText: string | null,
|
|
186
|
+
emittedContentText: string
|
|
187
|
+
): string {
|
|
188
|
+
if (emittedContentText.trim().length > 0) {
|
|
189
|
+
return "";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const naturalPrelude: string = stripKnownToolSyntax(contentText ?? "").trim();
|
|
193
|
+
if (naturalPrelude.length > 0) {
|
|
194
|
+
return `\n\n${naturalPrelude}\n\n`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function escapeRegExp(text: string): string {
|
|
201
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
type LooseOpeningTag = {
|
|
205
|
+
tagName: string;
|
|
206
|
+
selfClosing: boolean;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function parseLooseOpeningTag(openingTag: string): LooseOpeningTag | null {
|
|
210
|
+
const match: RegExpMatchArray | null = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)(?:\s+[^<>]*?)?(\/?)\s*>$/u.exec(openingTag);
|
|
211
|
+
const tagName: string | undefined = match?.[1];
|
|
212
|
+
if (tagName === undefined) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
tagName,
|
|
218
|
+
selfClosing: (match?.[2] ?? "") === "/"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isDsmlToolCallsOpeningTag(openingTag: string): boolean {
|
|
223
|
+
return /^<\s*[||]+\s*DSML\s*[||]+\s*tool_calls\s*>$/iu.test(openingTag);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isPotentialToolOpeningFragment(text: string): boolean {
|
|
227
|
+
if (/^<\s*[||]/u.test(text)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const match: RegExpMatchArray | null = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/u.exec(text);
|
|
232
|
+
return match?.[1] !== undefined && isPotentialLooseToolTagName(match[1]);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function findLooseClosingTagEnd(text: string, tagName: string): number {
|
|
236
|
+
const closingPattern: RegExp = new RegExp(`<\\/\\s*${escapeRegExp(tagName)}\\s*>`, "iu");
|
|
237
|
+
const match: RegExpExecArray | null = closingPattern.exec(text);
|
|
238
|
+
return match === null ? -1 : match.index + match[0].length;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function findDsmlClosingTagEnd(text: string): number {
|
|
242
|
+
const closingPattern: RegExp = /<\/\s*[||]+\s*DSML\s*[||]+\s*tool_calls\s*>/iu;
|
|
243
|
+
const match: RegExpExecArray | null = closingPattern.exec(text);
|
|
244
|
+
return match === null ? -1 : match.index + match[0].length;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function getUnemittedSuffix(finalText: string, emittedText: string): string {
|
|
248
|
+
if (emittedText.length === 0) {
|
|
249
|
+
return finalText;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (finalText.startsWith(emittedText)) {
|
|
253
|
+
return finalText.slice(emittedText.length);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const trimmedEmittedText: string = emittedText.trimEnd();
|
|
257
|
+
if (trimmedEmittedText.length > 0 && finalText.startsWith(trimmedEmittedText)) {
|
|
258
|
+
return finalText.slice(trimmedEmittedText.length);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return `\n\n${finalText}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
class ToolSyntaxStreamFilter {
|
|
265
|
+
private pendingText: string = "";
|
|
266
|
+
private strippingTagName: string | null = null;
|
|
267
|
+
private strippingDsmlToolCalls: boolean = false;
|
|
268
|
+
private emittedText: string = "";
|
|
269
|
+
private suppressedSyntax: boolean = false;
|
|
270
|
+
|
|
271
|
+
push(text: string): string {
|
|
272
|
+
this.pendingText += text;
|
|
273
|
+
const visibleText: string = this.drain(false);
|
|
274
|
+
this.emittedText += visibleText;
|
|
275
|
+
return visibleText;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
flush(): string {
|
|
279
|
+
const visibleText: string = this.drain(true);
|
|
280
|
+
this.emittedText += visibleText;
|
|
281
|
+
return visibleText;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getEmittedText(): string {
|
|
285
|
+
return this.emittedText;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
hasSuppressedSyntax(): boolean {
|
|
289
|
+
return this.suppressedSyntax;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private drain(flush: boolean): string {
|
|
293
|
+
let visibleText: string = "";
|
|
294
|
+
|
|
295
|
+
while (this.pendingText.length > 0) {
|
|
296
|
+
if (this.strippingTagName !== null) {
|
|
297
|
+
const closingEnd: number = findLooseClosingTagEnd(this.pendingText, this.strippingTagName);
|
|
298
|
+
if (closingEnd < 0) {
|
|
299
|
+
if (flush) {
|
|
300
|
+
this.pendingText = "";
|
|
301
|
+
this.strippingTagName = null;
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.pendingText = this.pendingText.slice(closingEnd);
|
|
307
|
+
this.strippingTagName = null;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.strippingDsmlToolCalls) {
|
|
312
|
+
const closingEnd: number = findDsmlClosingTagEnd(this.pendingText);
|
|
313
|
+
if (closingEnd < 0) {
|
|
314
|
+
if (flush) {
|
|
315
|
+
this.pendingText = "";
|
|
316
|
+
this.strippingDsmlToolCalls = false;
|
|
317
|
+
}
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.pendingText = this.pendingText.slice(closingEnd);
|
|
322
|
+
this.strippingDsmlToolCalls = false;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const tagStart: number = this.pendingText.indexOf("<");
|
|
327
|
+
if (tagStart < 0) {
|
|
328
|
+
visibleText += this.pendingText;
|
|
329
|
+
this.pendingText = "";
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (tagStart > 0) {
|
|
334
|
+
visibleText += this.pendingText.slice(0, tagStart);
|
|
335
|
+
this.pendingText = this.pendingText.slice(tagStart);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const tagEnd: number = this.pendingText.indexOf(">");
|
|
340
|
+
if (tagEnd < 0) {
|
|
341
|
+
if (flush) {
|
|
342
|
+
if (isPotentialToolOpeningFragment(this.pendingText)) {
|
|
343
|
+
this.suppressedSyntax = true;
|
|
344
|
+
} else {
|
|
345
|
+
visibleText += this.pendingText;
|
|
346
|
+
}
|
|
347
|
+
this.pendingText = "";
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const openingTag: string = this.pendingText.slice(0, tagEnd + 1);
|
|
353
|
+
const looseOpeningTag: LooseOpeningTag | null = parseLooseOpeningTag(openingTag);
|
|
354
|
+
if (looseOpeningTag !== null && isKnownLooseToolTagName(looseOpeningTag.tagName)) {
|
|
355
|
+
this.suppressedSyntax = true;
|
|
356
|
+
this.pendingText = this.pendingText.slice(tagEnd + 1);
|
|
357
|
+
this.strippingTagName = looseOpeningTag.selfClosing ? null : looseOpeningTag.tagName;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isDsmlToolCallsOpeningTag(openingTag)) {
|
|
362
|
+
this.suppressedSyntax = true;
|
|
363
|
+
this.pendingText = this.pendingText.slice(tagEnd + 1);
|
|
364
|
+
this.strippingDsmlToolCalls = true;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
visibleText += openingTag;
|
|
369
|
+
this.pendingText = this.pendingText.slice(tagEnd + 1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return visibleText;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getReasoningContent(message: unknown): string {
|
|
377
|
+
if (message === null || typeof message !== "object") {
|
|
378
|
+
return "";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const reasoningValue: unknown = (message as { reasoning_content?: unknown }).reasoning_content;
|
|
382
|
+
return typeof reasoningValue === "string" ? reasoningValue : "";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function emitReasoningContent(message: unknown, onEvent?: OnToolEvent): void {
|
|
386
|
+
const reasoningContent: string = getReasoningContent(message);
|
|
387
|
+
if (reasoningContent.length === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
onEvent?.({ type: "ai.thinking.delta", text: reasoningContent });
|
|
392
|
+
onEvent?.({ type: "ai.thinking.done" });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getContentDelta(delta: unknown): string {
|
|
396
|
+
if (delta === null || typeof delta !== "object") {
|
|
397
|
+
return "";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const contentValue: unknown = (delta as { content?: unknown }).content;
|
|
401
|
+
return typeof contentValue === "string" ? contentValue : "";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getToolCallDeltaList(delta: unknown): unknown[] {
|
|
405
|
+
if (delta === null || typeof delta !== "object") {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const toolCallsValue: unknown = (delta as { tool_calls?: unknown }).tool_calls;
|
|
410
|
+
return Array.isArray(toolCallsValue) ? toolCallsValue : [];
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function applyToolCallDelta(accumulators: Map<number, ToolCallAccumulator>, value: unknown, step: number): void {
|
|
414
|
+
if (value === null || typeof value !== "object") {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const delta = value as {
|
|
419
|
+
index?: unknown;
|
|
420
|
+
id?: unknown;
|
|
421
|
+
function?: {
|
|
422
|
+
name?: unknown;
|
|
423
|
+
arguments?: unknown;
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
const index: number = typeof delta.index === "number" ? delta.index : accumulators.size;
|
|
427
|
+
const existing: ToolCallAccumulator | undefined = accumulators.get(index);
|
|
428
|
+
const accumulator: ToolCallAccumulator = existing ?? {
|
|
429
|
+
index,
|
|
430
|
+
id: `stream-tool-${step}-${index}`,
|
|
431
|
+
name: "",
|
|
432
|
+
argumentsText: ""
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
if (typeof delta.id === "string" && delta.id.length > 0) {
|
|
436
|
+
accumulator.id = delta.id;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (typeof delta.function?.name === "string" && delta.function.name.length > 0) {
|
|
440
|
+
accumulator.name = delta.function.name;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (typeof delta.function?.arguments === "string" && delta.function.arguments.length > 0) {
|
|
444
|
+
accumulator.argumentsText += delta.function.arguments;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
accumulators.set(index, accumulator);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function createToolCallsFromAccumulators(accumulators: Map<number, ToolCallAccumulator>): ChatCompletionMessageToolCall[] {
|
|
451
|
+
return Array.from(accumulators.values())
|
|
452
|
+
.sort((a: ToolCallAccumulator, b: ToolCallAccumulator): number => a.index - b.index)
|
|
453
|
+
.filter((accumulator: ToolCallAccumulator): boolean => accumulator.name.length > 0)
|
|
454
|
+
.map((accumulator: ToolCallAccumulator): ChatCompletionMessageToolCall => ({
|
|
455
|
+
id: accumulator.id,
|
|
456
|
+
type: "function",
|
|
457
|
+
function: {
|
|
458
|
+
name: accumulator.name,
|
|
459
|
+
arguments: accumulator.argumentsText
|
|
460
|
+
}
|
|
461
|
+
}) as ChatCompletionMessageToolCall);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function readStreamingAssistantMessage(
|
|
465
|
+
client: OpenAI,
|
|
466
|
+
params: AiChatParams,
|
|
467
|
+
options: DeepSeekChatOptions,
|
|
468
|
+
messages: ChatCompletionMessageParam[],
|
|
469
|
+
tools: ChatCompletionTool[],
|
|
470
|
+
step: number,
|
|
471
|
+
onEvent?: OnToolEvent,
|
|
472
|
+
emitContentDeltas: boolean = true,
|
|
473
|
+
abortSignal?: AbortSignal | undefined
|
|
474
|
+
): Promise<StreamedAssistantMessage> {
|
|
475
|
+
const requestBody: ChatCompletionCreateParamsStreaming = {
|
|
476
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
477
|
+
messages,
|
|
478
|
+
tools,
|
|
479
|
+
stream: true
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
applyChatOptions(requestBody, params);
|
|
483
|
+
|
|
484
|
+
const stream = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
485
|
+
const toolCallAccumulators: Map<number, ToolCallAccumulator> = new Map();
|
|
486
|
+
const contentFilter: ToolSyntaxStreamFilter = new ToolSyntaxStreamFilter();
|
|
487
|
+
let contentText = "";
|
|
488
|
+
let reasoningContent = "";
|
|
489
|
+
let emittedReasoning = false;
|
|
490
|
+
|
|
491
|
+
for await (const chunk of stream) {
|
|
492
|
+
const delta: unknown = (chunk as ChatCompletionChunk).choices[0]?.delta;
|
|
493
|
+
if (delta === undefined || delta === null) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const reasoningDelta: string = getReasoningContent(delta);
|
|
498
|
+
if (reasoningDelta.length > 0) {
|
|
499
|
+
reasoningContent += reasoningDelta;
|
|
500
|
+
emittedReasoning = true;
|
|
501
|
+
onEvent?.({ type: "ai.thinking.delta", text: reasoningDelta });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const contentDelta: string = getContentDelta(delta);
|
|
505
|
+
if (contentDelta.length > 0) {
|
|
506
|
+
contentText += contentDelta;
|
|
507
|
+
if (emitContentDeltas) {
|
|
508
|
+
const visibleDelta: string = contentFilter.push(contentDelta);
|
|
509
|
+
if (visibleDelta.length > 0) {
|
|
510
|
+
onEvent?.({ type: "ai.delta", text: visibleDelta });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const toolCallDelta of getToolCallDeltaList(delta)) {
|
|
516
|
+
applyToolCallDelta(toolCallAccumulators, toolCallDelta, step);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (emittedReasoning) {
|
|
521
|
+
onEvent?.({ type: "ai.thinking.done" });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (emitContentDeltas) {
|
|
525
|
+
const visibleTail: string = contentFilter.flush();
|
|
526
|
+
if (visibleTail.length > 0) {
|
|
527
|
+
onEvent?.({ type: "ai.delta", text: visibleTail });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
contentText,
|
|
533
|
+
reasoningContent,
|
|
534
|
+
toolCalls: createToolCallsFromAccumulators(toolCallAccumulators),
|
|
535
|
+
emittedContentText: emitContentDeltas ? contentFilter.getEmittedText() : "",
|
|
536
|
+
suppressedToolSyntax: emitContentDeltas && contentFilter.hasSuppressedSyntax()
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function createAssistantToolMessage(
|
|
541
|
+
contentText: string | null,
|
|
542
|
+
toolCalls: ChatCompletionMessageToolCall[],
|
|
543
|
+
reasoningContent: string
|
|
544
|
+
): ChatCompletionMessageParam {
|
|
545
|
+
const message: Record<string, unknown> = {
|
|
546
|
+
role: "assistant",
|
|
547
|
+
content: contentText === null ? contentText : stripKnownToolSyntax(contentText),
|
|
548
|
+
tool_calls: toolCalls
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
if (reasoningContent.length > 0) {
|
|
552
|
+
message.reasoning_content = reasoningContent;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return message as unknown as ChatCompletionMessageParam;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function createToolSyntaxLeakFallback(text: string, reason: string): string {
|
|
559
|
+
const strippedText: string = stripKnownToolSyntax(text);
|
|
560
|
+
const parsedToolCalls: ChatCompletionMessageToolCall[] = [
|
|
561
|
+
...parseDsmlToolCalls(text, "blocked-dsml"),
|
|
562
|
+
...parseLooseToolCalls(text, "blocked-loose")
|
|
563
|
+
];
|
|
564
|
+
const toolNames: string[] = extractToolNames(parsedToolCalls);
|
|
565
|
+
const toolText: string = toolNames.length > 0 ? `\n\n模型还尝试调用工具:${toolNames.map((name: string): string => `\`${name}\``).join(", ")}。` : "";
|
|
566
|
+
const prefix: string = strippedText.length > 0 ? `${strippedText}\n\n` : "";
|
|
567
|
+
|
|
568
|
+
return [
|
|
569
|
+
prefix.trimEnd(),
|
|
570
|
+
"工具调用没有继续执行,因为当前回复已经进入收束阶段。",
|
|
571
|
+
`原因:${reason}`,
|
|
572
|
+
"我已隐藏模型输出中的工具调用文本,避免把内部工具协议直接显示给你。",
|
|
573
|
+
toolText.trim()
|
|
574
|
+
].filter((part: string): boolean => part.length > 0).join("\n");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function createFinalAnswer(
|
|
578
|
+
client: OpenAI,
|
|
579
|
+
params: AiChatParams,
|
|
580
|
+
options: DeepSeekChatOptions,
|
|
581
|
+
messages: ChatCompletionMessageParam[],
|
|
582
|
+
reason: string,
|
|
583
|
+
abortSignal?: AbortSignal | undefined
|
|
584
|
+
): Promise<string> {
|
|
585
|
+
const finalMessages: ChatCompletionMessageParam[] = [
|
|
586
|
+
...messages,
|
|
587
|
+
{
|
|
588
|
+
role: "system",
|
|
589
|
+
content: `${FINALIZE_AFTER_TOOL_LIMIT_PROMPT}\n\n收束原因:${reason}`
|
|
590
|
+
}
|
|
591
|
+
];
|
|
592
|
+
const requestBody: ChatCompletionCreateParamsNonStreaming = {
|
|
593
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
594
|
+
messages: finalMessages
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
applyChatOptions(requestBody, params);
|
|
598
|
+
|
|
599
|
+
const completion = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
600
|
+
const text: string | null | undefined = completion.choices[0]?.message.content;
|
|
601
|
+
|
|
602
|
+
if (!text) {
|
|
603
|
+
throw new Error("LLM returned empty final response after tool limit");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (containsKnownToolSyntax(text)) {
|
|
607
|
+
return createToolSyntaxLeakFallback(text, reason);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return text;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function createToollessRetryAnswer(
|
|
614
|
+
client: OpenAI,
|
|
615
|
+
params: AiChatParams,
|
|
616
|
+
options: DeepSeekChatOptions,
|
|
617
|
+
messages: ChatCompletionMessageParam[],
|
|
618
|
+
reason: string,
|
|
619
|
+
abortSignal?: AbortSignal | undefined
|
|
620
|
+
): Promise<string> {
|
|
621
|
+
const retryMessages: ChatCompletionMessageParam[] = [
|
|
622
|
+
...messages,
|
|
623
|
+
{
|
|
624
|
+
role: "system",
|
|
625
|
+
content: `${RETRY_WITHOUT_TOOL_SYNTAX_PROMPT}\n\n拦截原因:${reason}`
|
|
626
|
+
}
|
|
627
|
+
];
|
|
628
|
+
const requestBody: ChatCompletionCreateParamsNonStreaming = {
|
|
629
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
630
|
+
messages: retryMessages
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
applyChatOptions(requestBody, params);
|
|
634
|
+
|
|
635
|
+
const completion = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
636
|
+
const text: string | null | undefined = completion.choices[0]?.message.content;
|
|
637
|
+
|
|
638
|
+
if (!text) {
|
|
639
|
+
throw new Error("LLM returned empty response after tool syntax retry");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (containsKnownToolSyntax(text)) {
|
|
643
|
+
return createToolSyntaxLeakFallback(text, "无工具阶段重试后仍输出工具标签");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return stripKnownToolSyntax(text);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function runAgentLoop(
|
|
650
|
+
client: OpenAI,
|
|
651
|
+
params: AiChatParams,
|
|
652
|
+
options: DeepSeekChatOptions,
|
|
653
|
+
messages: ChatCompletionMessageParam[],
|
|
654
|
+
mcpHost: McpHost,
|
|
655
|
+
gateway: ApprovalGateway,
|
|
656
|
+
tools: ChatCompletionTool[],
|
|
657
|
+
startStep: number,
|
|
658
|
+
maxSteps: number,
|
|
659
|
+
initialToolResultChars: number,
|
|
660
|
+
streamAssistant: boolean,
|
|
661
|
+
onEvent?: OnToolEvent,
|
|
662
|
+
abortSignal?: AbortSignal | undefined
|
|
663
|
+
): Promise<DeepSeekAgentResult> {
|
|
664
|
+
let totalToolResultChars: number = initialToolResultChars;
|
|
665
|
+
const allowedToolNames: ReadonlySet<string> = getAllowedToolNames(tools);
|
|
666
|
+
|
|
667
|
+
for (let step: number = startStep; step < maxSteps; step += 1) {
|
|
668
|
+
if (abortSignal?.aborted) {
|
|
669
|
+
throw new Error("Request cancelled");
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let toolCalls: ChatCompletionMessageToolCall[] | undefined;
|
|
673
|
+
let contentText: string | null;
|
|
674
|
+
let reasoningContent: string = "";
|
|
675
|
+
let emittedContentText: string = "";
|
|
676
|
+
let suppressedStreamToolSyntax: boolean = false;
|
|
677
|
+
if (streamAssistant) {
|
|
678
|
+
const streamedMessage: StreamedAssistantMessage = await readStreamingAssistantMessage(
|
|
679
|
+
client,
|
|
680
|
+
params,
|
|
681
|
+
options,
|
|
682
|
+
messages,
|
|
683
|
+
tools,
|
|
684
|
+
step,
|
|
685
|
+
onEvent,
|
|
686
|
+
true,
|
|
687
|
+
abortSignal
|
|
688
|
+
);
|
|
689
|
+
toolCalls = streamedMessage.toolCalls;
|
|
690
|
+
contentText = streamedMessage.contentText.length > 0 ? streamedMessage.contentText : null;
|
|
691
|
+
reasoningContent = streamedMessage.reasoningContent;
|
|
692
|
+
emittedContentText = streamedMessage.emittedContentText;
|
|
693
|
+
suppressedStreamToolSyntax = streamedMessage.suppressedToolSyntax;
|
|
694
|
+
} else {
|
|
695
|
+
const requestBody: ChatCompletionCreateParamsNonStreaming = {
|
|
696
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
697
|
+
messages,
|
|
698
|
+
tools
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
applyChatOptions(requestBody, params);
|
|
702
|
+
|
|
703
|
+
const completion = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
704
|
+
const choice = completion.choices[0];
|
|
705
|
+
|
|
706
|
+
if (!choice) {
|
|
707
|
+
throw new Error("LLM returned empty choices");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const message = choice.message;
|
|
711
|
+
reasoningContent = getReasoningContent(message);
|
|
712
|
+
emitReasoningContent(message, onEvent);
|
|
713
|
+
toolCalls = message.tool_calls;
|
|
714
|
+
contentText = message.content;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (toolCalls !== undefined && toolCalls.length > 0) {
|
|
718
|
+
toolCalls = filterToolCallsForAllowedTools(toolCalls, allowedToolNames);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (tools.length > 0 && (!toolCalls || toolCalls.length === 0) && contentText !== null) {
|
|
722
|
+
const parsedToolCalls: ChatCompletionMessageToolCall[] = parseTextToolCalls(contentText, step, allowedToolNames);
|
|
723
|
+
if (parsedToolCalls.length > 0) {
|
|
724
|
+
toolCalls = parsedToolCalls;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
729
|
+
const text: string | null = contentText;
|
|
730
|
+
|
|
731
|
+
if (!text) {
|
|
732
|
+
throw new Error("LLM returned empty response");
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const hasKnownToolSyntax: boolean = containsKnownToolSyntax(text) || suppressedStreamToolSyntax;
|
|
736
|
+
const finalText: string = hasKnownToolSyntax && tools.length === 0
|
|
737
|
+
? await createToollessRetryAnswer(client, params, options, messages, "当前阶段没有可执行的工具调用", abortSignal)
|
|
738
|
+
: hasKnownToolSyntax
|
|
739
|
+
? createToolSyntaxLeakFallback(text, "当前阶段没有可执行的工具调用")
|
|
740
|
+
: stripKnownToolSyntax(text);
|
|
741
|
+
if (streamAssistant && hasKnownToolSyntax) {
|
|
742
|
+
const suffixText: string = getUnemittedSuffix(finalText, emittedContentText);
|
|
743
|
+
if (suffixText.length > 0) {
|
|
744
|
+
onEvent?.({ type: "ai.delta", text: suffixText });
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return { status: "completed", text: finalText };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (streamAssistant) {
|
|
752
|
+
const preludeDelta: string = createToolCallPreludeDelta(contentText, emittedContentText);
|
|
753
|
+
if (preludeDelta.length > 0) {
|
|
754
|
+
onEvent?.({ type: "ai.delta", text: preludeDelta });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const assistantMessage: ChatCompletionMessageParam = createAssistantToolMessage(contentText, toolCalls, reasoningContent);
|
|
759
|
+
|
|
760
|
+
messages.push(assistantMessage);
|
|
761
|
+
|
|
762
|
+
let toolResults;
|
|
763
|
+
try {
|
|
764
|
+
if (abortSignal?.aborted) {
|
|
765
|
+
throw new Error("Request cancelled");
|
|
766
|
+
}
|
|
767
|
+
toolResults = await dispatchToolCalls(mcpHost, toolCalls, step, gateway, onEvent);
|
|
768
|
+
} catch (error: unknown) {
|
|
769
|
+
if (error instanceof ToolApprovalRequiredError) {
|
|
770
|
+
const pendingToolCall: ChatCompletionMessageToolCall | undefined = toolCalls.find(
|
|
771
|
+
(toolCall: ChatCompletionMessageToolCall): boolean => toolCall.id === error.pendingApproval.toolCallId
|
|
772
|
+
);
|
|
773
|
+
const continuationMessages: ChatCompletionMessageParam[] = [...messages];
|
|
774
|
+
|
|
775
|
+
if (pendingToolCall !== undefined) {
|
|
776
|
+
continuationMessages[continuationMessages.length - 1] = createAssistantToolMessage(contentText, [pendingToolCall], reasoningContent);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return {
|
|
780
|
+
status: "approval_required",
|
|
781
|
+
approvalId: error.pendingApproval.approvalId,
|
|
782
|
+
toolName: error.pendingApproval.llmToolName,
|
|
783
|
+
reason: error.pendingApproval.reason,
|
|
784
|
+
continuation: {
|
|
785
|
+
messages: continuationMessages,
|
|
786
|
+
nextStep: step + 1,
|
|
787
|
+
totalToolResultChars
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
throw error;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
for (const result of toolResults) {
|
|
796
|
+
const contentText: string = extractTextContent(result.content);
|
|
797
|
+
totalToolResultChars += estimateTextChars(contentText);
|
|
798
|
+
messages.push(result);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (totalToolResultChars >= MAX_TOTAL_TOOL_RESULT_CHARS) {
|
|
802
|
+
const finalText: string = await createFinalAnswer(
|
|
803
|
+
client,
|
|
804
|
+
params,
|
|
805
|
+
options,
|
|
806
|
+
messages,
|
|
807
|
+
`工具结果总量达到 ${totalToolResultChars} 字符,上限为 ${MAX_TOTAL_TOOL_RESULT_CHARS} 字符`,
|
|
808
|
+
abortSignal
|
|
809
|
+
);
|
|
810
|
+
if (streamAssistant) {
|
|
811
|
+
onEvent?.({ type: "ai.delta", text: finalText });
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
status: "completed",
|
|
816
|
+
text: finalText
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const finalText: string = await createFinalAnswer(
|
|
822
|
+
client,
|
|
823
|
+
params,
|
|
824
|
+
options,
|
|
825
|
+
messages,
|
|
826
|
+
`工具调用达到最大步数 ${maxSteps},当前工具结果总量为 ${totalToolResultChars} 字符`,
|
|
827
|
+
abortSignal
|
|
828
|
+
);
|
|
829
|
+
if (streamAssistant) {
|
|
830
|
+
onEvent?.({ type: "ai.delta", text: finalText });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
status: "completed",
|
|
835
|
+
text: finalText
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export async function runDeepSeekAgent(
|
|
840
|
+
params: AiChatParams,
|
|
841
|
+
options: DeepSeekChatOptions,
|
|
842
|
+
history: ChatMessage[],
|
|
843
|
+
systemPrompt: string,
|
|
844
|
+
mcpHost: McpHost,
|
|
845
|
+
gateway: ApprovalGateway,
|
|
846
|
+
allowedToolNames?: readonly string[] | undefined,
|
|
847
|
+
onEvent?: OnToolEvent,
|
|
848
|
+
abortSignal?: AbortSignal | undefined
|
|
849
|
+
): Promise<DeepSeekAgentResult> {
|
|
850
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
851
|
+
const tools = allowedToolNames !== undefined
|
|
852
|
+
? getToolDefinitionsForNames(allowedToolNames)
|
|
853
|
+
: getToolDefinitions();
|
|
854
|
+
|
|
855
|
+
const maxSteps: number = resolveToolBudget(
|
|
856
|
+
(params.options as Record<string, unknown> | undefined)?.["toolBudget"] as string | undefined,
|
|
857
|
+
params.skillId
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
const messages: ChatCompletionMessageParam[] = createMessages(params, history, systemPrompt);
|
|
861
|
+
|
|
862
|
+
return runAgentLoop(client, params, options, messages, mcpHost, gateway, tools, 0, maxSteps, 0, false, onEvent, abortSignal);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export async function runDeepSeekAgentStreaming(
|
|
866
|
+
params: AiChatParams,
|
|
867
|
+
options: DeepSeekChatOptions,
|
|
868
|
+
history: ChatMessage[],
|
|
869
|
+
systemPrompt: string,
|
|
870
|
+
mcpHost: McpHost,
|
|
871
|
+
gateway: ApprovalGateway,
|
|
872
|
+
allowedToolNames?: readonly string[] | undefined,
|
|
873
|
+
onEvent?: OnToolEvent,
|
|
874
|
+
abortSignal?: AbortSignal | undefined
|
|
875
|
+
): Promise<DeepSeekAgentResult> {
|
|
876
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
877
|
+
const tools = allowedToolNames !== undefined
|
|
878
|
+
? getToolDefinitionsForNames(allowedToolNames)
|
|
879
|
+
: getToolDefinitions();
|
|
880
|
+
|
|
881
|
+
const maxSteps: number = resolveToolBudget(
|
|
882
|
+
(params.options as Record<string, unknown> | undefined)?.["toolBudget"] as string | undefined,
|
|
883
|
+
params.skillId
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const messages: ChatCompletionMessageParam[] = createMessages(params, history, systemPrompt);
|
|
887
|
+
|
|
888
|
+
return runAgentLoop(client, params, options, messages, mcpHost, gateway, tools, 0, maxSteps, 0, true, onEvent, abortSignal);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
export async function continueDeepSeekAgent(
|
|
892
|
+
params: AiChatParams,
|
|
893
|
+
options: DeepSeekChatOptions,
|
|
894
|
+
continuation: DeepSeekAgentContinuation,
|
|
895
|
+
approvedToolResult: ApprovedToolResult,
|
|
896
|
+
mcpHost: McpHost,
|
|
897
|
+
gateway: ApprovalGateway,
|
|
898
|
+
allowedToolNames?: readonly string[] | undefined,
|
|
899
|
+
onEvent?: OnToolEvent,
|
|
900
|
+
abortSignal?: AbortSignal | undefined
|
|
901
|
+
): Promise<DeepSeekAgentResult> {
|
|
902
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
903
|
+
const tools = allowedToolNames !== undefined
|
|
904
|
+
? getToolDefinitionsForNames(allowedToolNames)
|
|
905
|
+
: getToolDefinitions();
|
|
906
|
+
const messages: ChatCompletionMessageParam[] = [...continuation.messages];
|
|
907
|
+
const toolMessage: ChatCompletionToolMessageParam = {
|
|
908
|
+
role: "tool",
|
|
909
|
+
tool_call_id: approvedToolResult.toolCallId,
|
|
910
|
+
content: approvedToolResult.content
|
|
911
|
+
};
|
|
912
|
+
const totalToolResultChars: number = continuation.totalToolResultChars + estimateTextChars(approvedToolResult.content);
|
|
913
|
+
|
|
914
|
+
messages.push(toolMessage);
|
|
915
|
+
|
|
916
|
+
if (totalToolResultChars >= MAX_TOTAL_TOOL_RESULT_CHARS) {
|
|
917
|
+
return {
|
|
918
|
+
status: "completed",
|
|
919
|
+
text: await createFinalAnswer(
|
|
920
|
+
client,
|
|
921
|
+
params,
|
|
922
|
+
options,
|
|
923
|
+
messages,
|
|
924
|
+
`工具结果总量达到 ${totalToolResultChars} 字符,上限为 ${MAX_TOTAL_TOOL_RESULT_CHARS} 字符`,
|
|
925
|
+
abortSignal
|
|
926
|
+
)
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const maxSteps: number = resolveToolBudget(
|
|
931
|
+
(params.options as Record<string, unknown> | undefined)?.["toolBudget"] as string | undefined,
|
|
932
|
+
params.skillId
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
return runAgentLoop(
|
|
936
|
+
client,
|
|
937
|
+
params,
|
|
938
|
+
options,
|
|
939
|
+
messages,
|
|
940
|
+
mcpHost,
|
|
941
|
+
gateway,
|
|
942
|
+
tools,
|
|
943
|
+
continuation.nextStep,
|
|
944
|
+
maxSteps,
|
|
945
|
+
totalToolResultChars,
|
|
946
|
+
false,
|
|
947
|
+
onEvent,
|
|
948
|
+
abortSignal
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
export async function continueDeepSeekAgentStreaming(
|
|
953
|
+
params: AiChatParams,
|
|
954
|
+
options: DeepSeekChatOptions,
|
|
955
|
+
continuation: DeepSeekAgentContinuation,
|
|
956
|
+
approvedToolResult: ApprovedToolResult,
|
|
957
|
+
mcpHost: McpHost,
|
|
958
|
+
gateway: ApprovalGateway,
|
|
959
|
+
allowedToolNames?: readonly string[] | undefined,
|
|
960
|
+
onEvent?: OnToolEvent,
|
|
961
|
+
abortSignal?: AbortSignal | undefined
|
|
962
|
+
): Promise<DeepSeekAgentResult> {
|
|
963
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
964
|
+
const tools = allowedToolNames !== undefined
|
|
965
|
+
? getToolDefinitionsForNames(allowedToolNames)
|
|
966
|
+
: getToolDefinitions();
|
|
967
|
+
const messages: ChatCompletionMessageParam[] = [...continuation.messages];
|
|
968
|
+
const toolMessage: ChatCompletionToolMessageParam = {
|
|
969
|
+
role: "tool",
|
|
970
|
+
tool_call_id: approvedToolResult.toolCallId,
|
|
971
|
+
content: approvedToolResult.content
|
|
972
|
+
};
|
|
973
|
+
const totalToolResultChars: number = continuation.totalToolResultChars + estimateTextChars(approvedToolResult.content);
|
|
974
|
+
|
|
975
|
+
messages.push(toolMessage);
|
|
976
|
+
|
|
977
|
+
if (totalToolResultChars >= MAX_TOTAL_TOOL_RESULT_CHARS) {
|
|
978
|
+
const finalText: string = await createFinalAnswer(
|
|
979
|
+
client,
|
|
980
|
+
params,
|
|
981
|
+
options,
|
|
982
|
+
messages,
|
|
983
|
+
`工具结果总量达到 ${totalToolResultChars} 字符,上限为 ${MAX_TOTAL_TOOL_RESULT_CHARS} 字符`,
|
|
984
|
+
abortSignal
|
|
985
|
+
);
|
|
986
|
+
onEvent?.({ type: "ai.delta", text: finalText });
|
|
987
|
+
|
|
988
|
+
return {
|
|
989
|
+
status: "completed",
|
|
990
|
+
text: finalText
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
const maxSteps: number = resolveToolBudget(
|
|
995
|
+
(params.options as Record<string, unknown> | undefined)?.["toolBudget"] as string | undefined,
|
|
996
|
+
params.skillId
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
return runAgentLoop(
|
|
1000
|
+
client,
|
|
1001
|
+
params,
|
|
1002
|
+
options,
|
|
1003
|
+
messages,
|
|
1004
|
+
mcpHost,
|
|
1005
|
+
gateway,
|
|
1006
|
+
tools,
|
|
1007
|
+
continuation.nextStep,
|
|
1008
|
+
maxSteps,
|
|
1009
|
+
totalToolResultChars,
|
|
1010
|
+
true,
|
|
1011
|
+
onEvent,
|
|
1012
|
+
abortSignal
|
|
1013
|
+
);
|
|
1014
|
+
}
|