@falai/agent 2.2.0 → 2.2.2

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 (81) hide show
  1. package/dist/cjs/core/BranchEvaluator.d.ts +2 -2
  2. package/dist/cjs/core/BranchEvaluator.js +2 -2
  3. package/dist/cjs/core/BranchEvaluator.js.map +1 -1
  4. package/dist/cjs/core/Flow.d.ts +2 -1
  5. package/dist/cjs/core/Flow.d.ts.map +1 -1
  6. package/dist/cjs/core/Flow.js +2 -1
  7. package/dist/cjs/core/Flow.js.map +1 -1
  8. package/dist/cjs/core/FlowRouter.js +2 -2
  9. package/dist/cjs/core/FlowRouter.js.map +1 -1
  10. package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
  11. package/dist/cjs/core/PromptComposer.js +8 -20
  12. package/dist/cjs/core/PromptComposer.js.map +1 -1
  13. package/dist/cjs/core/Step.d.ts +2 -1
  14. package/dist/cjs/core/Step.d.ts.map +1 -1
  15. package/dist/cjs/core/Step.js +2 -1
  16. package/dist/cjs/core/Step.js.map +1 -1
  17. package/dist/cjs/index.d.ts +2 -0
  18. package/dist/cjs/index.d.ts.map +1 -1
  19. package/dist/cjs/index.js +4 -2
  20. package/dist/cjs/index.js.map +1 -1
  21. package/dist/cjs/providers/DeepSeekProvider.d.ts +61 -0
  22. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -0
  23. package/dist/cjs/providers/DeepSeekProvider.js +450 -0
  24. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -0
  25. package/dist/cjs/providers/index.d.ts +2 -0
  26. package/dist/cjs/providers/index.d.ts.map +1 -1
  27. package/dist/cjs/providers/index.js +3 -1
  28. package/dist/cjs/providers/index.js.map +1 -1
  29. package/dist/cjs/types/agent.d.ts +5 -3
  30. package/dist/cjs/types/agent.d.ts.map +1 -1
  31. package/dist/cjs/types/flow.d.ts +4 -4
  32. package/dist/core/BranchEvaluator.d.ts +2 -2
  33. package/dist/core/BranchEvaluator.js +2 -2
  34. package/dist/core/BranchEvaluator.js.map +1 -1
  35. package/dist/core/Flow.d.ts +2 -1
  36. package/dist/core/Flow.d.ts.map +1 -1
  37. package/dist/core/Flow.js +2 -1
  38. package/dist/core/Flow.js.map +1 -1
  39. package/dist/core/FlowRouter.js +2 -2
  40. package/dist/core/FlowRouter.js.map +1 -1
  41. package/dist/core/PromptComposer.d.ts.map +1 -1
  42. package/dist/core/PromptComposer.js +8 -20
  43. package/dist/core/PromptComposer.js.map +1 -1
  44. package/dist/core/Step.d.ts +2 -1
  45. package/dist/core/Step.d.ts.map +1 -1
  46. package/dist/core/Step.js +2 -1
  47. package/dist/core/Step.js.map +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/providers/DeepSeekProvider.d.ts +61 -0
  53. package/dist/providers/DeepSeekProvider.d.ts.map +1 -0
  54. package/dist/providers/DeepSeekProvider.js +443 -0
  55. package/dist/providers/DeepSeekProvider.js.map +1 -0
  56. package/dist/providers/index.d.ts +2 -0
  57. package/dist/providers/index.d.ts.map +1 -1
  58. package/dist/providers/index.js +1 -0
  59. package/dist/providers/index.js.map +1 -1
  60. package/dist/types/agent.d.ts +5 -3
  61. package/dist/types/agent.d.ts.map +1 -1
  62. package/dist/types/flow.d.ts +4 -4
  63. package/docs/guides/branching.md +3 -1
  64. package/docs/guides/conditions.md +9 -8
  65. package/docs/guides/instructions.md +7 -7
  66. package/docs/reference/branches.md +2 -2
  67. package/docs/reference/flow.md +1 -1
  68. package/docs/reference/instruction.md +7 -5
  69. package/docs/reference/providers.md +48 -2
  70. package/docs/reference/step.md +2 -2
  71. package/package.json +11 -11
  72. package/src/core/BranchEvaluator.ts +4 -4
  73. package/src/core/Flow.ts +2 -1
  74. package/src/core/FlowRouter.ts +2 -2
  75. package/src/core/PromptComposer.ts +9 -20
  76. package/src/core/Step.ts +2 -1
  77. package/src/index.ts +2 -0
  78. package/src/providers/DeepSeekProvider.ts +666 -0
  79. package/src/providers/index.ts +3 -0
  80. package/src/types/agent.ts +5 -3
  81. package/src/types/flow.ts +4 -4
@@ -0,0 +1,666 @@
1
+ /**
2
+ * DeepSeek provider implementation (OpenAI-compatible API)
3
+ * Supports deepseek-chat, deepseek-reasoner with optional thinking/reasoning mode.
4
+ * DeepSeek streams reasoning content via `delta.reasoning_content` when thinking is enabled.
5
+ */
6
+
7
+ import OpenAI from "openai";
8
+ import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
9
+ import { FunctionParameters } from "openai/resources/shared.mjs";
10
+
11
+ import type {
12
+ AiProvider,
13
+ GenerateMessageInput,
14
+ GenerateMessageOutput,
15
+ GenerateMessageStreamChunk,
16
+ AgentStructuredResponse,
17
+ StructuredSchema,
18
+ } from "../types";
19
+ import type { HistoryItem } from "../types/history";
20
+ import { withTimeoutAndRetry, logger } from "../utils";
21
+
22
+ const DEFAULT_RETRY_CONFIG = {
23
+ timeout: 60000,
24
+ retries: 3,
25
+ };
26
+
27
+ /**
28
+ * Configuration options for DeepSeek provider
29
+ * Uses types from openai package (DeepSeek is OpenAI-compatible)
30
+ */
31
+ export interface DeepSeekProviderOptions {
32
+ /** DeepSeek API key */
33
+ apiKey: string;
34
+ /** Model to use (required) - e.g., "deepseek-chat", "deepseek-reasoner" */
35
+ model: string;
36
+ /** Backup models to try if primary fails (default: []) */
37
+ backupModels?: string[];
38
+ /** Custom base URL (default: "https://api.deepseek.com") */
39
+ baseURL?: string;
40
+ /** Default parameters - uses ChatCompletionCreateParamsNonStreaming from openai package */
41
+ config?: Partial<
42
+ Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages">
43
+ >;
44
+ /** Retry configuration */
45
+ retryConfig?: {
46
+ timeout?: number;
47
+ retries?: number;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Type guard for errors with status/code properties
53
+ */
54
+ interface ErrorWithStatus {
55
+ status?: number;
56
+ code?: string;
57
+ message?: string;
58
+ type?: string;
59
+ }
60
+
61
+ /**
62
+ * Type guard to check if error is ErrorWithStatus
63
+ */
64
+ function isErrorWithStatus(error: unknown): error is ErrorWithStatus {
65
+ return (
66
+ typeof error === "object" &&
67
+ error !== null &&
68
+ ("status" in error || "code" in error || "message" in error)
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Safely extract error message
74
+ */
75
+ function getErrorMessage(error: unknown): string {
76
+ if (error instanceof Error) {
77
+ return error.message;
78
+ }
79
+ if (isErrorWithStatus(error) && error.message) {
80
+ return error.message;
81
+ }
82
+ return String(error);
83
+ }
84
+
85
+ /**
86
+ * Determines if an error should trigger backup model usage
87
+ */
88
+ const shouldUseBackupModel = (error: unknown): boolean => {
89
+ if (!isErrorWithStatus(error)) {
90
+ return false;
91
+ }
92
+
93
+ // Server errors
94
+ if (error.status === 500 || error.status === 503) {
95
+ return true;
96
+ }
97
+
98
+ // Rate limiting
99
+ if (error.status === 429) {
100
+ return true;
101
+ }
102
+
103
+ // Model overloaded or unavailable
104
+ if (error.code === "model_not_found" || error.code === "model_overloaded") {
105
+ return true;
106
+ }
107
+
108
+ const message = getErrorMessage(error);
109
+ if (
110
+ message.includes("overloaded") ||
111
+ message.includes("unavailable") ||
112
+ message.includes("internal error") ||
113
+ message.includes("Internal error")
114
+ ) {
115
+ return true;
116
+ }
117
+
118
+ return false;
119
+ };
120
+
121
+ /**
122
+ * DeepSeek provider implementation using the OpenAI-compatible API.
123
+ * Supports deepseek-chat and deepseek-reasoner with optional thinking mode.
124
+ *
125
+ * DeepSeek streams reasoning content via `delta.reasoning_content` when
126
+ * thinking mode is enabled. Tool calls follow the standard OpenAI format.
127
+ */
128
+ export class DeepSeekProvider implements AiProvider {
129
+ public readonly name = "deepseek";
130
+ private client: OpenAI;
131
+ private primaryModel: string;
132
+ private backupModels: string[];
133
+ private config?: Partial<
134
+ Omit<ChatCompletionCreateParamsNonStreaming, "model" | "messages">
135
+ >;
136
+ private retryConfig: { timeout: number; retries: number };
137
+
138
+ constructor(options: DeepSeekProviderOptions) {
139
+ const {
140
+ apiKey,
141
+ model,
142
+ backupModels = [],
143
+ baseURL = "https://api.deepseek.com",
144
+ config,
145
+ retryConfig,
146
+ } = options;
147
+
148
+ if (!apiKey) {
149
+ throw new Error("DeepSeek API key is required");
150
+ }
151
+
152
+ if (!model) {
153
+ throw new Error(
154
+ "Model is required. Example: 'deepseek-chat' or 'deepseek-reasoner'"
155
+ );
156
+ }
157
+
158
+ this.client = new OpenAI({
159
+ apiKey,
160
+ baseURL,
161
+ });
162
+ this.primaryModel = model;
163
+ this.backupModels = backupModels;
164
+ this.config = config;
165
+ this.retryConfig = {
166
+ timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
167
+ retries: retryConfig?.retries || DEFAULT_RETRY_CONFIG.retries,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Build OpenAI-formatted messages from HistoryItem[] array.
173
+ * DeepSeek uses OpenAI-compatible message format.
174
+ */
175
+ private buildMessages(history: HistoryItem[]): Array<unknown> {
176
+ const messages: Array<unknown> = [];
177
+
178
+ for (const item of history) {
179
+ switch (item.role) {
180
+ case "system":
181
+ messages.push({ role: "system", content: item.content });
182
+ break;
183
+ case "user":
184
+ messages.push({ role: "user", content: item.content });
185
+ break;
186
+ case "assistant":
187
+ if (item.tool_calls && item.tool_calls.length > 0) {
188
+ messages.push({
189
+ role: "assistant",
190
+ content: item.content || null,
191
+ tool_calls: item.tool_calls.map((tc) => ({
192
+ id: tc.id,
193
+ type: "function",
194
+ function: {
195
+ name: tc.name,
196
+ arguments: JSON.stringify(tc.arguments),
197
+ },
198
+ })),
199
+ });
200
+ } else {
201
+ messages.push({ role: "assistant", content: item.content || "" });
202
+ }
203
+ break;
204
+ case "tool":
205
+ messages.push({
206
+ role: "tool",
207
+ tool_call_id: item.tool_call_id,
208
+ content:
209
+ typeof item.content === "string"
210
+ ? item.content
211
+ : JSON.stringify(item.content),
212
+ });
213
+ break;
214
+ }
215
+ }
216
+
217
+ return messages;
218
+ }
219
+
220
+ /**
221
+ * Adapt common schema format to DeepSeek's format.
222
+ * DeepSeek is OpenAI-compatible and uses standard JSON Schema.
223
+ */
224
+ private adaptSchema(schema: StructuredSchema): Record<string, unknown> {
225
+ return schema as Record<string, unknown>;
226
+ }
227
+
228
+ async generateMessage<
229
+ TContext = unknown,
230
+ TStructured = AgentStructuredResponse,
231
+ >(
232
+ input: GenerateMessageInput<TContext>
233
+ ): Promise<GenerateMessageOutput<TStructured>> {
234
+ return this.generateWithBackup<TContext, TStructured>(input);
235
+ }
236
+
237
+ async *generateMessageStream<
238
+ TContext = unknown,
239
+ TStructured = AgentStructuredResponse,
240
+ >(
241
+ input: GenerateMessageInput<TContext>
242
+ ): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
243
+ yield* this.generateStreamWithBackup<TContext, TStructured>(input);
244
+ }
245
+
246
+ private async generateWithBackup<
247
+ TContext = unknown,
248
+ TStructured = AgentStructuredResponse,
249
+ >(
250
+ input: GenerateMessageInput<TContext>
251
+ ): Promise<GenerateMessageOutput<TStructured>> {
252
+ try {
253
+ return await this.generateWithModel<TContext, TStructured>(
254
+ this.primaryModel,
255
+ input
256
+ );
257
+ } catch (primaryError: unknown) {
258
+ const primaryErrMsg = getErrorMessage(primaryError);
259
+ logger.warn(
260
+ `[DEEPSEEK] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
261
+ );
262
+
263
+ if (!shouldUseBackupModel(primaryError)) {
264
+ throw primaryError;
265
+ }
266
+
267
+ logger.debug(`[DEEPSEEK] Trying backup models`);
268
+
269
+ let lastBackupError: unknown = primaryError;
270
+
271
+ for (let i = 0; i < this.backupModels.length; i++) {
272
+ const backupModel = this.backupModels[i];
273
+ logger.debug(
274
+ `[DEEPSEEK] Trying backup model ${i + 1}/${this.backupModels.length}: ${backupModel}`
275
+ );
276
+
277
+ try {
278
+ const result = await this.generateWithModel(backupModel, input);
279
+ logger.debug(`[DEEPSEEK] Backup model ${backupModel} succeeded`);
280
+ return result as GenerateMessageOutput<TStructured>;
281
+ } catch (backupError: unknown) {
282
+ const backupErrMsg = getErrorMessage(backupError);
283
+ logger.warn(
284
+ `[DEEPSEEK] Backup model ${backupModel} failed: ${backupErrMsg}`
285
+ );
286
+ lastBackupError = backupError;
287
+
288
+ if (
289
+ !shouldUseBackupModel(backupError) &&
290
+ i < this.backupModels.length - 1
291
+ ) {
292
+ logger.debug(
293
+ `[DEEPSEEK] Backup model error doesn't qualify for further attempts`
294
+ );
295
+ break;
296
+ }
297
+ }
298
+ }
299
+
300
+ const lastBackupErrMsg = getErrorMessage(lastBackupError);
301
+ logger.error(
302
+ `[DEEPSEEK] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
303
+ );
304
+ throw lastBackupError;
305
+ }
306
+ }
307
+
308
+ private async generateWithModel<
309
+ TContext = unknown,
310
+ TStructured = AgentStructuredResponse,
311
+ >(
312
+ model: string,
313
+ input: GenerateMessageInput<TContext>
314
+ ): Promise<GenerateMessageOutput<TStructured>> {
315
+ const operation = async (): Promise<GenerateMessageOutput> => {
316
+ const historyMessages = this.buildMessages(input.history);
317
+ historyMessages.push({ role: "user", content: input.prompt });
318
+
319
+ const params: ChatCompletionCreateParamsNonStreaming = {
320
+ model,
321
+ messages:
322
+ historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
323
+ ...this.config,
324
+ };
325
+
326
+ // Override with input parameters if provided
327
+ if (input.parameters?.maxOutputTokens !== undefined) {
328
+ params.max_tokens = input.parameters.maxOutputTokens;
329
+ }
330
+
331
+ // Add tools if provided
332
+ if (input.tools && input.tools.length > 0) {
333
+ params.tools = input.tools.map((tool) => ({
334
+ type: "function" as const,
335
+ function: {
336
+ name: tool.name || tool.id,
337
+ description: tool.description,
338
+ parameters: tool.parameters as FunctionParameters,
339
+ },
340
+ }));
341
+ params.tool_choice = "auto";
342
+ }
343
+
344
+ // Use structured output if JSON schema is provided
345
+ if (input.parameters?.jsonSchema) {
346
+ params.response_format = {
347
+ type: "json_schema" as const,
348
+ json_schema: {
349
+ name: input.parameters.schemaName || "structured_output",
350
+ schema: this.adaptSchema(input.parameters.jsonSchema),
351
+ },
352
+ } as ChatCompletionCreateParamsNonStreaming["response_format"];
353
+ }
354
+
355
+ const response = await this.client.chat.completions.create(params);
356
+
357
+ const message = response.choices[0]?.message?.content || "";
358
+
359
+ let toolCalls: Array<{
360
+ toolName: string;
361
+ arguments: Record<string, unknown>;
362
+ }> = [];
363
+ if (response.choices?.[0]?.message?.tool_calls) {
364
+ toolCalls = response.choices[0].message.tool_calls
365
+ .filter((toolCall) => toolCall.type === "function")
366
+ .map((toolCall) => {
367
+ let toolCallArguments: Record<string, unknown> = {};
368
+ try {
369
+ toolCallArguments = JSON.parse(
370
+ toolCall.function.arguments
371
+ ) as Record<string, unknown>;
372
+ } catch (error) {
373
+ logger.warn(
374
+ `[DEEPSEEK] Failed to parse tool call arguments: ${getErrorMessage(error)}`
375
+ );
376
+ toolCallArguments = {};
377
+ }
378
+ return {
379
+ toolName: toolCall.function.name,
380
+ arguments: toolCallArguments,
381
+ };
382
+ });
383
+ }
384
+
385
+ // Only throw error if we have no text AND no function calls
386
+ if (!message && toolCalls.length === 0) {
387
+ throw new Error("No response from DeepSeek");
388
+ }
389
+
390
+ // Parse structured output if schema was provided
391
+ let structured: AgentStructuredResponse | undefined;
392
+ if (input.parameters?.jsonSchema && message) {
393
+ try {
394
+ structured = JSON.parse(message) as AgentStructuredResponse;
395
+ } catch (error) {
396
+ logger.warn("[DEEPSEEK] Failed to parse JSON response:", error);
397
+ }
398
+ }
399
+
400
+ // If tools were used, include them in structured response
401
+ if (toolCalls.length > 0) {
402
+ structured = {
403
+ ...(structured || {}),
404
+ message: structured?.message || message,
405
+ toolCalls,
406
+ } as AgentStructuredResponse;
407
+ }
408
+
409
+ return {
410
+ message,
411
+ metadata: {
412
+ model: response.model,
413
+ finishReason: response.choices[0]?.finish_reason,
414
+ tokensUsed: response.usage?.total_tokens,
415
+ promptTokens: response.usage?.prompt_tokens,
416
+ completionTokens: response.usage?.completion_tokens,
417
+ },
418
+ structured,
419
+ };
420
+ };
421
+
422
+ return withTimeoutAndRetry(
423
+ operation,
424
+ this.retryConfig.timeout,
425
+ this.retryConfig.retries,
426
+ `DeepSeek ${model}`
427
+ ) as Promise<GenerateMessageOutput<TStructured>>;
428
+ }
429
+
430
+ private async *generateStreamWithBackup<
431
+ TContext = unknown,
432
+ TStructured = AgentStructuredResponse,
433
+ >(
434
+ input: GenerateMessageInput<TContext>
435
+ ): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
436
+ try {
437
+ yield* this.generateStreamWithModel<TContext, TStructured>(
438
+ this.primaryModel,
439
+ input
440
+ );
441
+ } catch (primaryError: unknown) {
442
+ const primaryErrMsg = getErrorMessage(primaryError);
443
+ logger.warn(
444
+ `[DEEPSEEK] Primary model ${this.primaryModel} failed: ${primaryErrMsg}`
445
+ );
446
+
447
+ if (!shouldUseBackupModel(primaryError)) {
448
+ throw primaryError;
449
+ }
450
+
451
+ logger.debug(`[DEEPSEEK] Trying backup models for streaming`);
452
+
453
+ let lastBackupError: unknown = primaryError;
454
+
455
+ for (let i = 0; i < this.backupModels.length; i++) {
456
+ const backupModel = this.backupModels[i];
457
+ logger.debug(
458
+ `[DEEPSEEK] Trying backup model ${i + 1}/${this.backupModels.length}: ${backupModel}`
459
+ );
460
+
461
+ try {
462
+ yield* this.generateStreamWithModel<TContext, TStructured>(
463
+ backupModel,
464
+ input
465
+ );
466
+ logger.debug(`[DEEPSEEK] Backup model ${backupModel} succeeded`);
467
+ return;
468
+ } catch (backupError: unknown) {
469
+ const backupErrMsg = getErrorMessage(backupError);
470
+ logger.warn(
471
+ `[DEEPSEEK] Backup model ${backupModel} failed: ${backupErrMsg}`
472
+ );
473
+ lastBackupError = backupError;
474
+
475
+ if (
476
+ !shouldUseBackupModel(backupError) &&
477
+ i < this.backupModels.length - 1
478
+ ) {
479
+ logger.debug(
480
+ `[DEEPSEEK] Backup model error doesn't qualify for further attempts`
481
+ );
482
+ break;
483
+ }
484
+ }
485
+ }
486
+
487
+ const lastBackupErrMsg = getErrorMessage(lastBackupError);
488
+ logger.error(
489
+ `[DEEPSEEK] All models failed. Primary: ${primaryErrMsg}, Last backup: ${lastBackupErrMsg}`
490
+ );
491
+ throw lastBackupError;
492
+ }
493
+ }
494
+
495
+ private async *generateStreamWithModel<
496
+ TContext = unknown,
497
+ TStructured = AgentStructuredResponse,
498
+ >(
499
+ model: string,
500
+ input: GenerateMessageInput<TContext>
501
+ ): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
502
+ const historyMessages = this.buildMessages(input.history);
503
+ historyMessages.push({ role: "user" as const, content: input.prompt });
504
+
505
+ const params = {
506
+ ...this.config,
507
+ model,
508
+ messages:
509
+ historyMessages as ChatCompletionCreateParamsNonStreaming["messages"],
510
+ stream: true as const,
511
+ stream_options: { include_usage: true },
512
+ };
513
+
514
+ // Override with input parameters if provided
515
+ if (input.parameters?.maxOutputTokens !== undefined) {
516
+ params.max_tokens = input.parameters.maxOutputTokens;
517
+ }
518
+
519
+ // Add tools if provided
520
+ if (input.tools && input.tools.length > 0) {
521
+ params.tools = input.tools.map((tool) => ({
522
+ type: "function" as const,
523
+ function: {
524
+ name: tool.name || tool.id,
525
+ description: tool.description,
526
+ parameters: tool.parameters as FunctionParameters,
527
+ },
528
+ }));
529
+ params.tool_choice = "auto";
530
+ }
531
+
532
+ // Request JSON schema output if schema is provided
533
+ if (input.parameters?.jsonSchema) {
534
+ params.response_format = {
535
+ type: "json_schema" as const,
536
+ json_schema: {
537
+ name: input.parameters.schemaName || "structured_output",
538
+ schema: this.adaptSchema(input.parameters.jsonSchema),
539
+ },
540
+ } as ChatCompletionCreateParamsNonStreaming["response_format"];
541
+ }
542
+
543
+ const stream = await this.client.chat.completions.create(params);
544
+
545
+ let accumulated = "";
546
+ let currentModel = model;
547
+ let finishReason: string | undefined;
548
+ let promptTokens: number | undefined;
549
+ let completionTokens: number | undefined;
550
+ let totalTokens: number | undefined;
551
+ const toolCalls: Array<{
552
+ toolName: string;
553
+ arguments: Record<string, unknown>;
554
+ }> = [];
555
+
556
+ for await (const chunk of stream) {
557
+ currentModel = chunk.model;
558
+ const choice = chunk.choices?.[0];
559
+
560
+ // DeepSeek may include usage in chunks with include_usage
561
+ if (chunk.usage) {
562
+ promptTokens = chunk.usage.prompt_tokens;
563
+ completionTokens = chunk.usage.completion_tokens;
564
+ totalTokens = chunk.usage.total_tokens;
565
+ }
566
+
567
+ if (!choice) continue;
568
+
569
+ const delta = choice.delta as Record<string, unknown>;
570
+
571
+ // Extract tool calls from delta
572
+ const deltaToolCalls = delta?.tool_calls as
573
+ | Array<{
574
+ index: number;
575
+ id?: string;
576
+ function?: { name?: string; arguments?: string };
577
+ }>
578
+ | undefined;
579
+
580
+ if (deltaToolCalls) {
581
+ for (const toolCall of deltaToolCalls) {
582
+ if (toolCall.function) {
583
+ let toolCallArguments: Record<string, unknown> = {};
584
+ try {
585
+ toolCallArguments = toolCall.function.arguments
586
+ ? (JSON.parse(toolCall.function.arguments) as Record<
587
+ string,
588
+ unknown
589
+ >)
590
+ : {};
591
+ } catch (error) {
592
+ logger.warn(
593
+ `[DEEPSEEK] Failed to parse tool call arguments in stream: ${getErrorMessage(error)}`
594
+ );
595
+ toolCallArguments = {};
596
+ }
597
+ toolCalls.push({
598
+ toolName: toolCall.function.name || "",
599
+ arguments: toolCallArguments,
600
+ });
601
+ }
602
+ }
603
+ }
604
+
605
+ // DeepSeek streams reasoning via `reasoning_content` on the delta
606
+ const reasoning =
607
+ (delta?.reasoning_content as string | undefined) ?? undefined;
608
+ if (reasoning) {
609
+ logger.debug(`[DEEPSEEK] Reasoning: ${reasoning}`);
610
+ }
611
+
612
+ const content = (delta?.content as string | undefined) ?? "";
613
+ if (content) {
614
+ accumulated += content;
615
+ yield {
616
+ delta: content,
617
+ accumulated,
618
+ done: false,
619
+ };
620
+ }
621
+
622
+ if (choice.finish_reason) {
623
+ finishReason = choice.finish_reason;
624
+ }
625
+ }
626
+
627
+ // Parse JSON response if schema was provided
628
+ let structured: TStructured | undefined;
629
+ if (input.parameters?.jsonSchema && accumulated) {
630
+ try {
631
+ structured = JSON.parse(accumulated) as TStructured;
632
+ } catch (error) {
633
+ logger.warn(
634
+ "[DEEPSEEK] Failed to parse JSON response in stream:",
635
+ error
636
+ );
637
+ }
638
+ }
639
+
640
+ // Include tool calls in structured response
641
+ if (toolCalls.length > 0) {
642
+ structured = {
643
+ ...(structured || {}),
644
+ message:
645
+ (structured as AgentStructuredResponse | undefined)?.message ||
646
+ accumulated,
647
+ toolCalls,
648
+ } as TStructured;
649
+ }
650
+
651
+ // Yield final chunk
652
+ yield {
653
+ delta: "",
654
+ accumulated,
655
+ done: true,
656
+ metadata: {
657
+ model: currentModel,
658
+ finishReason,
659
+ tokensUsed: totalTokens,
660
+ promptTokens,
661
+ completionTokens,
662
+ },
663
+ structured,
664
+ };
665
+ }
666
+ }
@@ -14,3 +14,6 @@ export type { OpenAIProviderOptions } from "./OpenAIProvider";
14
14
 
15
15
  export { OpenRouterProvider } from "./OpenRouterProvider";
16
16
  export type { OpenRouterProviderOptions } from "./OpenRouterProvider";
17
+
18
+ export { DeepSeekProvider } from "./DeepSeekProvider";
19
+ export type { DeepSeekProviderOptions } from "./DeepSeekProvider";
@@ -267,7 +267,7 @@ export interface Instruction<TContext = unknown, TData = unknown> {
267
267
  */
268
268
  kind?: 'must' | 'never' | 'should';
269
269
  /**
270
- * AI-evaluated activation condition. String or array of strings (AND semantics).
270
+ * AI-evaluated activation condition. String or array of strings (OR semantics).
271
271
  * Undefined = always active. Functions are NOT allowed here — use `if`.
272
272
  */
273
273
  when?: ConditionWhen;
@@ -307,7 +307,8 @@ export interface ScopedInstructions<TContext = unknown, TData = unknown> {
307
307
  }
308
308
 
309
309
  /**
310
- * Observability record for an instruction that was active and rendered into a turn's prompt.
310
+ * Observability record for an instruction that was rendered into a turn's prompt.
311
+ * Textual `when` conditions are included in the prompt for the AI to evaluate.
311
312
  * Deterministic — derived from rendering, not from LLM self-report.
312
313
  */
313
314
  export interface AppliedInstruction {
@@ -329,7 +330,8 @@ export interface AgentResponse<TData = Record<string, unknown>> {
329
330
  /** Why execution stopped (for multi-step execution) */
330
331
  stoppedReason?: StoppedReason;
331
332
  /**
332
- * Instructions whose conditions passed and were rendered into this turn's prompt.
333
+ * Instructions rendered into this turn's prompt after code-evaluated gates passed.
334
+ * Textual `when` conditions remain in the prompt for the AI to evaluate.
333
335
  * Deterministic — derived from rendering, not from LLM self-report.
334
336
  */
335
337
  appliedInstructions?: AppliedInstruction[];