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.
Files changed (67) hide show
  1. package/README.md +101 -0
  2. package/bin/godot-daedalus-backend.js +4 -0
  3. package/bin/godot-daedalus-mcp.js +4 -0
  4. package/bin/godot-daedalus-terminal-mcp.js +4 -0
  5. package/bin/run-tsx-entry.js +26 -0
  6. package/package.json +54 -0
  7. package/scripts/deepseek-tokenizer-server.py +54 -0
  8. package/src/app-paths.ts +36 -0
  9. package/src/main.ts +21 -0
  10. package/src/mcp/content-length-protocol.ts +68 -0
  11. package/src/mcp/custom-mcp-config-store.ts +397 -0
  12. package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
  13. package/src/mcp/godot-editor-bridge.ts +307 -0
  14. package/src/mcp/godot-mcp-server.ts +3484 -0
  15. package/src/mcp/godot-paths.ts +151 -0
  16. package/src/mcp/godot-project-settings.ts +233 -0
  17. package/src/mcp/godot-tool-registration.ts +46 -0
  18. package/src/mcp/mcp-config.ts +48 -0
  19. package/src/mcp/mcp-host.ts +393 -0
  20. package/src/mcp/mcp-session.ts +81 -0
  21. package/src/mcp/terminal-mcp-server.ts +576 -0
  22. package/src/mcp/tscn-tools.ts +302 -0
  23. package/src/mcp/types.ts +12 -0
  24. package/src/ping-client.ts +24 -0
  25. package/src/prompts/registry.ts +97 -0
  26. package/src/prompts/templates/backend-helper.md +25 -0
  27. package/src/prompts/templates/gdscript-reviewer.md +19 -0
  28. package/src/prompts/templates/godot-assistant.md +225 -0
  29. package/src/prompts/templates/scene-architect.md +15 -0
  30. package/src/prompts/templates/session-compressor.md +33 -0
  31. package/src/protocol/schema.ts +486 -0
  32. package/src/protocol/types.ts +77 -0
  33. package/src/providers/deepseek-agent.ts +1014 -0
  34. package/src/providers/deepseek-client.ts +114 -0
  35. package/src/providers/deepseek-dsml-tools.ts +90 -0
  36. package/src/providers/deepseek-loose-tools.ts +450 -0
  37. package/src/providers/provider-config-store.ts +164 -0
  38. package/src/server/client-session.ts +93 -0
  39. package/src/server/request-dispatcher.ts +74 -0
  40. package/src/server/response-helpers.ts +33 -0
  41. package/src/server/send-json.ts +8 -0
  42. package/src/server/websocket-server.ts +3997 -0
  43. package/src/session/session-compressor.ts +68 -0
  44. package/src/session/session-store.ts +669 -0
  45. package/src/skills/registry.ts +180 -0
  46. package/src/skills/templates/backend-helper.md +12 -0
  47. package/src/skills/templates/file-creator.md +14 -0
  48. package/src/skills/templates/gdscript-review.md +12 -0
  49. package/src/skills/templates/godot-project-init.md +29 -0
  50. package/src/skills/templates/scene-builder.md +12 -0
  51. package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
  52. package/src/tokens/model-profiles.ts +38 -0
  53. package/src/tokens/token-counter-factory.ts +52 -0
  54. package/src/tokens/token-counter.ts +22 -0
  55. package/src/tools/approval-gateway.ts +111 -0
  56. package/src/tools/llm-tools.ts +1415 -0
  57. package/src/tools/tool-dispatcher.ts +147 -0
  58. package/src/tools/tool-event-describer.ts +387 -0
  59. package/src/tools/tool-idempotency.ts +373 -0
  60. package/src/tools/tool-policy-table.ts +61 -0
  61. package/src/tools/tool-policy.ts +73 -0
  62. package/src/workflow/llm-planner.ts +407 -0
  63. package/src/workflow/planner.ts +201 -0
  64. package/src/workflow/runner.ts +141 -0
  65. package/src/workflow/types.ts +69 -0
  66. package/src/workspace/registry.ts +104 -0
  67. 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
+ }