@falai/agent 1.1.3 → 1.2.1
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 +9 -0
- package/dist/cjs/core/Agent.d.ts +17 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +47 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.d.ts +3 -0
- package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.js +4 -1
- package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts +65 -0
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -0
- package/dist/cjs/core/CompactionEngine.js +251 -0
- package/dist/cjs/core/CompactionEngine.js.map +1 -0
- package/dist/cjs/core/PromptComposer.d.ts +8 -1
- package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
- package/dist/cjs/core/PromptComposer.js +238 -126
- package/dist/cjs/core/PromptComposer.js.map +1 -1
- package/dist/cjs/core/PromptSectionCache.d.ts +57 -0
- package/dist/cjs/core/PromptSectionCache.d.ts.map +1 -0
- package/dist/cjs/core/PromptSectionCache.js +108 -0
- package/dist/cjs/core/PromptSectionCache.js.map +1 -0
- package/dist/cjs/core/ResponseEngine.d.ts +3 -2
- package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
- package/dist/cjs/core/ResponseEngine.js +8 -8
- package/dist/cjs/core/ResponseEngine.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +120 -70
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ResponsePipeline.d.ts +2 -1
- package/dist/cjs/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/cjs/core/ResponsePipeline.js +17 -19
- package/dist/cjs/core/ResponsePipeline.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts +10 -0
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +5 -4
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/core/SessionManager.d.ts.map +1 -1
- package/dist/cjs/core/SessionManager.js +20 -0
- package/dist/cjs/core/SessionManager.js.map +1 -1
- package/dist/cjs/core/StreamingToolExecutor.d.ts +142 -0
- package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -0
- package/dist/cjs/core/StreamingToolExecutor.js +455 -0
- package/dist/cjs/core/StreamingToolExecutor.js.map +1 -0
- package/dist/cjs/core/ToolManager.d.ts +18 -1
- package/dist/cjs/core/ToolManager.d.ts.map +1 -1
- package/dist/cjs/core/ToolManager.js +91 -0
- package/dist/cjs/core/ToolManager.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +8 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +7 -0
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +109 -19
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +32 -0
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +160 -53
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts +5 -0
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +65 -18
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts +5 -0
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +57 -18
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +44 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/agent.js.map +1 -1
- package/dist/cjs/types/ai.d.ts +2 -2
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +50 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -0
- package/dist/cjs/types/compaction.js +6 -0
- package/dist/cjs/types/compaction.js.map +1 -0
- package/dist/cjs/types/index.d.ts +4 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/tool.d.ts +84 -0
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/core/Agent.d.ts +17 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +47 -0
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/BatchPromptBuilder.d.ts +3 -0
- package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/core/BatchPromptBuilder.js +4 -1
- package/dist/core/BatchPromptBuilder.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts +65 -0
- package/dist/core/CompactionEngine.d.ts.map +1 -0
- package/dist/core/CompactionEngine.js +244 -0
- package/dist/core/CompactionEngine.js.map +1 -0
- package/dist/core/PromptComposer.d.ts +8 -1
- package/dist/core/PromptComposer.d.ts.map +1 -1
- package/dist/core/PromptComposer.js +238 -126
- package/dist/core/PromptComposer.js.map +1 -1
- package/dist/core/PromptSectionCache.d.ts +57 -0
- package/dist/core/PromptSectionCache.d.ts.map +1 -0
- package/dist/core/PromptSectionCache.js +104 -0
- package/dist/core/PromptSectionCache.js.map +1 -0
- package/dist/core/ResponseEngine.d.ts +3 -2
- package/dist/core/ResponseEngine.d.ts.map +1 -1
- package/dist/core/ResponseEngine.js +8 -8
- package/dist/core/ResponseEngine.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +121 -71
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ResponsePipeline.d.ts +2 -1
- package/dist/core/ResponsePipeline.d.ts.map +1 -1
- package/dist/core/ResponsePipeline.js +18 -20
- package/dist/core/ResponsePipeline.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts +10 -0
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +6 -5
- package/dist/core/RoutingEngine.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +17 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StreamingToolExecutor.d.ts +142 -0
- package/dist/core/StreamingToolExecutor.d.ts.map +1 -0
- package/dist/core/StreamingToolExecutor.js +448 -0
- package/dist/core/StreamingToolExecutor.js.map +1 -0
- package/dist/core/ToolManager.d.ts +18 -1
- package/dist/core/ToolManager.d.ts.map +1 -1
- package/dist/core/ToolManager.js +91 -0
- package/dist/core/ToolManager.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +7 -0
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +109 -19
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +32 -0
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +160 -53
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts +5 -0
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +65 -18
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts +5 -0
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +57 -18
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/types/agent.d.ts +44 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/agent.js.map +1 -1
- package/dist/types/ai.d.ts +2 -2
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +50 -0
- package/dist/types/compaction.d.ts.map +1 -0
- package/dist/types/compaction.js +5 -0
- package/dist/types/compaction.js.map +1 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/tool.d.ts +84 -0
- package/dist/types/tool.d.ts.map +1 -1
- package/docs/api/overview.md +140 -0
- package/docs/core/tools/enhanced-tool.md +186 -0
- package/docs/core/tools/streaming-execution.md +161 -0
- package/docs/guides/context-compaction.md +96 -0
- package/docs/guides/prompt-optimization.md +164 -0
- package/examples/advanced-patterns/context-compaction.ts +223 -0
- package/examples/advanced-patterns/streaming-responses.ts +85 -7
- package/examples/tools/enhanced-tool-metadata.ts +268 -0
- package/examples/tools/streaming-tool-execution.ts +283 -0
- package/package.json +1 -1
- package/src/core/Agent.ts +58 -2
- package/src/core/BatchPromptBuilder.ts +4 -1
- package/src/core/CompactionEngine.ts +318 -0
- package/src/core/PromptComposer.ts +259 -156
- package/src/core/PromptSectionCache.ts +136 -0
- package/src/core/ResponseEngine.ts +7 -11
- package/src/core/ResponseModal.ts +133 -83
- package/src/core/ResponsePipeline.ts +22 -22
- package/src/core/RoutingEngine.ts +16 -5
- package/src/core/SessionManager.ts +19 -0
- package/src/core/StreamingToolExecutor.ts +572 -0
- package/src/core/ToolManager.ts +151 -41
- package/src/index.ts +14 -0
- package/src/providers/AnthropicProvider.ts +121 -24
- package/src/providers/GeminiProvider.ts +174 -54
- package/src/providers/OpenAIProvider.ts +77 -25
- package/src/providers/OpenRouterProvider.ts +68 -25
- package/src/types/agent.ts +45 -0
- package/src/types/ai.ts +2 -2
- package/src/types/compaction.ts +52 -0
- package/src/types/index.ts +35 -14
- package/src/types/tool.ts +108 -0
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
GenerateMessageStreamChunk,
|
|
16
16
|
AgentStructuredResponse,
|
|
17
17
|
} from "../types";
|
|
18
|
+
import type { HistoryItem } from "../types/history";
|
|
18
19
|
import { withTimeoutAndRetry, logger } from "../utils";
|
|
19
20
|
|
|
20
21
|
const DEFAULT_RETRY_CONFIG = {
|
|
@@ -152,6 +153,67 @@ export class AnthropicProvider implements AiProvider {
|
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Build Anthropic-formatted messages from HistoryItem[] array.
|
|
158
|
+
* System messages are extracted separately (Anthropic uses a `system` param).
|
|
159
|
+
* Tool results are mapped to Anthropic's tool_result content blocks.
|
|
160
|
+
* Assistant tool_calls are mapped to tool_use content blocks.
|
|
161
|
+
*/
|
|
162
|
+
private buildAnthropicMessages(history: HistoryItem[]): {
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
messages: any[];
|
|
165
|
+
systemMessages: string[];
|
|
166
|
+
} {
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
const messages: any[] = [];
|
|
169
|
+
const systemMessages: string[] = [];
|
|
170
|
+
|
|
171
|
+
for (const item of history) {
|
|
172
|
+
switch (item.role) {
|
|
173
|
+
case "system":
|
|
174
|
+
systemMessages.push(item.content);
|
|
175
|
+
break;
|
|
176
|
+
case "user":
|
|
177
|
+
messages.push({ role: "user", content: item.content });
|
|
178
|
+
break;
|
|
179
|
+
case "assistant":
|
|
180
|
+
if (item.tool_calls && item.tool_calls.length > 0) {
|
|
181
|
+
const content: Array<Record<string, unknown>> = [];
|
|
182
|
+
if (item.content) {
|
|
183
|
+
content.push({ type: "text", text: item.content });
|
|
184
|
+
}
|
|
185
|
+
for (const tc of item.tool_calls) {
|
|
186
|
+
content.push({
|
|
187
|
+
type: "tool_use",
|
|
188
|
+
id: tc.id,
|
|
189
|
+
name: tc.name,
|
|
190
|
+
input: tc.arguments,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
messages.push({ role: "assistant", content });
|
|
194
|
+
} else {
|
|
195
|
+
messages.push({ role: "assistant", content: item.content || "" });
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case "tool":
|
|
199
|
+
// Anthropic tool results are sent as user messages with tool_result content blocks
|
|
200
|
+
messages.push({
|
|
201
|
+
role: "user",
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: "tool_result",
|
|
205
|
+
tool_use_id: item.tool_call_id,
|
|
206
|
+
content: typeof item.content === "string" ? item.content : JSON.stringify(item.content),
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { messages, systemMessages };
|
|
215
|
+
}
|
|
216
|
+
|
|
155
217
|
async generateMessage<
|
|
156
218
|
TContext = unknown,
|
|
157
219
|
TStructured = AgentStructuredResponse
|
|
@@ -199,8 +261,7 @@ export class AnthropicProvider implements AiProvider {
|
|
|
199
261
|
for (let i = 0; i < this.backupModels.length; i++) {
|
|
200
262
|
const backupModel = this.backupModels[i];
|
|
201
263
|
logger.debug(
|
|
202
|
-
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
203
|
-
this.backupModels.length
|
|
264
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${this.backupModels.length
|
|
204
265
|
}: ${backupModel}`
|
|
205
266
|
);
|
|
206
267
|
|
|
@@ -249,18 +310,36 @@ export class AnthropicProvider implements AiProvider {
|
|
|
249
310
|
// Anthropic requires max_tokens to be specified
|
|
250
311
|
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
251
312
|
|
|
313
|
+
// Build messages from history
|
|
314
|
+
const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
|
|
315
|
+
|
|
316
|
+
// Append the current prompt as the final user message
|
|
317
|
+
historyMessages.push({
|
|
318
|
+
role: "user",
|
|
319
|
+
content: input.prompt,
|
|
320
|
+
});
|
|
321
|
+
|
|
252
322
|
const params: MessageCreateParamsNonStreaming = {
|
|
253
323
|
model,
|
|
254
324
|
max_tokens: maxTokens,
|
|
255
|
-
messages:
|
|
256
|
-
{
|
|
257
|
-
role: "user",
|
|
258
|
-
content: input.prompt,
|
|
259
|
-
},
|
|
260
|
-
],
|
|
325
|
+
messages: historyMessages,
|
|
261
326
|
...this.config,
|
|
262
327
|
};
|
|
263
328
|
|
|
329
|
+
// Set system messages from history if present
|
|
330
|
+
if (systemMessages.length > 0) {
|
|
331
|
+
if (typeof this.config?.system === "string") {
|
|
332
|
+
params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
|
|
333
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
334
|
+
params.system = [
|
|
335
|
+
...this.config.system,
|
|
336
|
+
...systemMessages.map(s => ({ type: "text" as const, text: s })),
|
|
337
|
+
];
|
|
338
|
+
} else {
|
|
339
|
+
params.system = systemMessages.join("\n\n");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
264
343
|
// Add tools if provided
|
|
265
344
|
if (input.tools && input.tools.length > 0) {
|
|
266
345
|
params.tools = input.tools.map((tool) => ({
|
|
@@ -299,10 +378,6 @@ export class AnthropicProvider implements AiProvider {
|
|
|
299
378
|
);
|
|
300
379
|
const message = textContent?.type === "text" ? textContent.text : "";
|
|
301
380
|
|
|
302
|
-
if (!message) {
|
|
303
|
-
throw new Error("No response from Anthropic");
|
|
304
|
-
}
|
|
305
|
-
|
|
306
381
|
// Extract tool calls from response
|
|
307
382
|
const toolCalls: Array<{
|
|
308
383
|
toolName: string;
|
|
@@ -319,6 +394,11 @@ export class AnthropicProvider implements AiProvider {
|
|
|
319
394
|
}
|
|
320
395
|
}
|
|
321
396
|
|
|
397
|
+
// Only throw error if we have no text AND no function calls
|
|
398
|
+
if (!message && toolCalls.length === 0) {
|
|
399
|
+
throw new Error("No response from Anthropic");
|
|
400
|
+
}
|
|
401
|
+
|
|
322
402
|
// Parse JSON response if schema was provided
|
|
323
403
|
let structured: AgentStructuredResponse | undefined;
|
|
324
404
|
if (input.parameters?.jsonSchema) {
|
|
@@ -333,9 +413,9 @@ export class AnthropicProvider implements AiProvider {
|
|
|
333
413
|
// If tools were used, include them in structured response
|
|
334
414
|
if (toolCalls.length > 0) {
|
|
335
415
|
structured = {
|
|
336
|
-
|
|
416
|
+
...(structured || {}),
|
|
417
|
+
message: structured?.message || message,
|
|
337
418
|
toolCalls,
|
|
338
|
-
...structured,
|
|
339
419
|
} as AgentStructuredResponse;
|
|
340
420
|
}
|
|
341
421
|
|
|
@@ -390,8 +470,7 @@ export class AnthropicProvider implements AiProvider {
|
|
|
390
470
|
for (let i = 0; i < this.backupModels.length; i++) {
|
|
391
471
|
const backupModel = this.backupModels[i];
|
|
392
472
|
logger.debug(
|
|
393
|
-
`[ANTHROPIC] Trying backup model ${i + 1}/${
|
|
394
|
-
this.backupModels.length
|
|
473
|
+
`[ANTHROPIC] Trying backup model ${i + 1}/${this.backupModels.length
|
|
395
474
|
}: ${backupModel}`
|
|
396
475
|
);
|
|
397
476
|
|
|
@@ -439,19 +518,37 @@ export class AnthropicProvider implements AiProvider {
|
|
|
439
518
|
// Anthropic requires max_tokens to be specified
|
|
440
519
|
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
441
520
|
|
|
521
|
+
// Build messages from history
|
|
522
|
+
const { messages: historyMessages, systemMessages } = this.buildAnthropicMessages(input.history);
|
|
523
|
+
|
|
524
|
+
// Append the current prompt as the final user message
|
|
525
|
+
historyMessages.push({
|
|
526
|
+
role: "user" as const,
|
|
527
|
+
content: input.prompt,
|
|
528
|
+
});
|
|
529
|
+
|
|
442
530
|
const params = {
|
|
443
531
|
model,
|
|
444
532
|
max_tokens: maxTokens,
|
|
445
|
-
messages:
|
|
446
|
-
{
|
|
447
|
-
role: "user" as const,
|
|
448
|
-
content: input.prompt,
|
|
449
|
-
},
|
|
450
|
-
],
|
|
533
|
+
messages: historyMessages,
|
|
451
534
|
stream: true,
|
|
452
535
|
...this.config,
|
|
453
536
|
};
|
|
454
537
|
|
|
538
|
+
// Set system messages from history if present
|
|
539
|
+
if (systemMessages.length > 0) {
|
|
540
|
+
if (typeof this.config?.system === "string") {
|
|
541
|
+
params.system = `${this.config.system}\n\n${systemMessages.join("\n\n")}`;
|
|
542
|
+
} else if (Array.isArray(this.config?.system)) {
|
|
543
|
+
params.system = [
|
|
544
|
+
...this.config.system,
|
|
545
|
+
...systemMessages.map(s => ({ type: "text" as const, text: s })),
|
|
546
|
+
];
|
|
547
|
+
} else {
|
|
548
|
+
params.system = systemMessages.join("\n\n");
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
455
552
|
// Add tools if provided
|
|
456
553
|
if (input.tools && input.tools.length > 0) {
|
|
457
554
|
params.tools = input.tools.map((tool) => ({
|
|
@@ -536,9 +633,9 @@ export class AnthropicProvider implements AiProvider {
|
|
|
536
633
|
// If tools were used, include them in structured response
|
|
537
634
|
if (toolCalls.length > 0) {
|
|
538
635
|
structured = {
|
|
539
|
-
|
|
636
|
+
...(structured || {}),
|
|
637
|
+
message: structured?.message || accumulated,
|
|
540
638
|
toolCalls,
|
|
541
|
-
...structured,
|
|
542
639
|
} as AgentStructuredResponse;
|
|
543
640
|
}
|
|
544
641
|
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
AgentStructuredResponse,
|
|
20
20
|
StructuredSchema,
|
|
21
21
|
} from "../types";
|
|
22
|
+
import type { HistoryItem } from "../types/history";
|
|
22
23
|
import { withTimeoutAndRetry } from "../utils/retry";
|
|
23
24
|
import { tryParseJSONResponse } from "../utils/json";
|
|
24
25
|
import { logger } from "../utils/logger";
|
|
@@ -152,6 +153,122 @@ export class GeminiProvider implements AiProvider {
|
|
|
152
153
|
};
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Build Gemini-formatted contents from HistoryItem[] array.
|
|
158
|
+
* Gemini uses "user"/"model" roles (not "assistant").
|
|
159
|
+
* System messages are extracted separately for systemInstruction.
|
|
160
|
+
* Tool results map to functionResponse parts, tool calls to functionCall parts.
|
|
161
|
+
*/
|
|
162
|
+
private buildGeminiContents(history: HistoryItem[]): {
|
|
163
|
+
contents: Array<{ role: string; parts: Array<Record<string, unknown>> }>;
|
|
164
|
+
systemInstructions: string[];
|
|
165
|
+
} {
|
|
166
|
+
const contents: Array<{ role: string; parts: Array<Record<string, unknown>> }> = [];
|
|
167
|
+
const systemInstructions: string[] = [];
|
|
168
|
+
|
|
169
|
+
for (const item of history) {
|
|
170
|
+
switch (item.role) {
|
|
171
|
+
case "system":
|
|
172
|
+
systemInstructions.push(item.content);
|
|
173
|
+
break;
|
|
174
|
+
case "user":
|
|
175
|
+
contents.push({ role: "user", parts: [{ text: item.content }] });
|
|
176
|
+
break;
|
|
177
|
+
case "assistant":
|
|
178
|
+
if (item.tool_calls && item.tool_calls.length > 0) {
|
|
179
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
180
|
+
if (item.content) {
|
|
181
|
+
parts.push({ text: item.content });
|
|
182
|
+
}
|
|
183
|
+
for (const tc of item.tool_calls) {
|
|
184
|
+
parts.push({
|
|
185
|
+
functionCall: { name: tc.name, args: tc.arguments },
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
contents.push({ role: "model", parts });
|
|
189
|
+
} else {
|
|
190
|
+
contents.push({ role: "model", parts: [{ text: item.content || "" }] });
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "tool":
|
|
194
|
+
contents.push({
|
|
195
|
+
role: "user",
|
|
196
|
+
parts: [
|
|
197
|
+
{
|
|
198
|
+
functionResponse: {
|
|
199
|
+
name: item.name,
|
|
200
|
+
response: typeof item.content === "object" ? item.content : { result: item.content },
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { contents, systemInstructions };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Convert tool parameter schemas (JSON Schema) to Gemini's Schema format.
|
|
214
|
+
* Gemini's FunctionDeclaration.parameters expects its own Schema type,
|
|
215
|
+
* not raw JSON Schema. This method handles the conversion, including
|
|
216
|
+
* edge cases like empty objects that Gemini rejects.
|
|
217
|
+
*
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
private convertToolParameters(parameters: unknown): Schema | undefined {
|
|
221
|
+
if (!parameters || typeof parameters !== "object") {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return this.adaptSchemaForGemini(parameters as StructuredSchema);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.warn(`[GeminiProvider] Failed to convert tool parameters, passing as-is:`, error);
|
|
228
|
+
return parameters as Schema;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Safely extract text from a Gemini response or chunk.
|
|
234
|
+
* The `.text` getter can throw when the response contains only function calls
|
|
235
|
+
* (observed in some SDK versions). This method falls back to manually
|
|
236
|
+
* extracting text parts from candidates.
|
|
237
|
+
*
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; functionCall?: unknown }> } }> }): string {
|
|
241
|
+
try {
|
|
242
|
+
return responseOrChunk.text || "";
|
|
243
|
+
} catch {
|
|
244
|
+
// .text getter threw — extract text parts manually
|
|
245
|
+
const parts = responseOrChunk.candidates?.[0]?.content?.parts;
|
|
246
|
+
if (parts) {
|
|
247
|
+
return parts
|
|
248
|
+
.filter((p) => p.text != null)
|
|
249
|
+
.map((p) => p.text)
|
|
250
|
+
.join("");
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Build Gemini function declarations from framework tool definitions.
|
|
258
|
+
* Converts JSON Schema parameters to Gemini's Schema format.
|
|
259
|
+
*
|
|
260
|
+
* @private
|
|
261
|
+
*/
|
|
262
|
+
private buildFunctionDeclarations(
|
|
263
|
+
tools: Array<{ id: string; name?: string; description?: string; parameters?: unknown }>
|
|
264
|
+
): FunctionDeclaration[] {
|
|
265
|
+
return tools.map((tool) => ({
|
|
266
|
+
name: tool.name || tool.id,
|
|
267
|
+
description: tool.description || "",
|
|
268
|
+
parameters: this.convertToolParameters(tool.parameters),
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
155
272
|
/**
|
|
156
273
|
* Adapt common schema format to Gemini's specific requirements.
|
|
157
274
|
* Gemini has strict validation:
|
|
@@ -356,15 +473,8 @@ export class GeminiProvider implements AiProvider {
|
|
|
356
473
|
if (hasTools) {
|
|
357
474
|
const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
|
|
358
475
|
logger.debug(`[GeminiProvider] Configuring ${toolNames.length} tools for model ${model}:`, toolNames);
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
functionDeclarations: input.tools?.map((tool) => ({
|
|
362
|
-
name: tool.name || tool.id,
|
|
363
|
-
description: tool.description || "",
|
|
364
|
-
parameters: tool.parameters as FunctionDeclaration["parameters"], // JSON schema
|
|
365
|
-
})),
|
|
366
|
-
},
|
|
367
|
-
];
|
|
476
|
+
const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
|
|
477
|
+
configOverride.tools = [{ functionDeclarations }];
|
|
368
478
|
|
|
369
479
|
} else if (hasJsonSchema) {
|
|
370
480
|
// Only set JSON schema if no tools are present
|
|
@@ -377,10 +487,29 @@ export class GeminiProvider implements AiProvider {
|
|
|
377
487
|
|
|
378
488
|
let response: GenerateContentResponse;
|
|
379
489
|
try {
|
|
490
|
+
// Build contents from history
|
|
491
|
+
const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
|
|
492
|
+
|
|
493
|
+
// Append the current prompt as the final user content
|
|
494
|
+
historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
|
|
495
|
+
|
|
496
|
+
// Set system instruction from history if present
|
|
497
|
+
if (systemInstructions.length > 0) {
|
|
498
|
+
const existingSystem = configOverride.systemInstruction;
|
|
499
|
+
if (typeof existingSystem === "string") {
|
|
500
|
+
configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
|
|
501
|
+
} else {
|
|
502
|
+
configOverride.systemInstruction = systemInstructions.join("\n\n");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
380
506
|
response = await this.genAI.models.generateContent({
|
|
381
507
|
model,
|
|
382
|
-
contents:
|
|
383
|
-
config:
|
|
508
|
+
contents: historyContents,
|
|
509
|
+
config: {
|
|
510
|
+
...configOverride,
|
|
511
|
+
...(input.signal ? { abortSignal: input.signal } : {}),
|
|
512
|
+
},
|
|
384
513
|
});
|
|
385
514
|
} catch (error: unknown) {
|
|
386
515
|
logger.error(`[GeminiProvider] API call failed:`, error);
|
|
@@ -399,7 +528,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
399
528
|
if (part.functionCall) {
|
|
400
529
|
toolCalls.push({
|
|
401
530
|
toolName: part.functionCall.name || "",
|
|
402
|
-
arguments: part.functionCall.args as Record<string, unknown
|
|
531
|
+
arguments: (part.functionCall.args as Record<string, unknown>) || {},
|
|
403
532
|
});
|
|
404
533
|
}
|
|
405
534
|
}
|
|
@@ -408,30 +537,13 @@ export class GeminiProvider implements AiProvider {
|
|
|
408
537
|
// Debug logging for response structure
|
|
409
538
|
if (!response.text && toolCalls.length === 0) {
|
|
410
539
|
logger.debug(`[GeminiProvider] Debug - Response structure:`, {
|
|
411
|
-
hasText: !!response.text,
|
|
412
540
|
candidatesCount: response.candidates?.length || 0,
|
|
413
|
-
firstCandidateContent: response.candidates?.[0]?.content,
|
|
414
541
|
firstCandidateParts: response.candidates?.[0]?.content?.parts?.length || 0,
|
|
415
542
|
});
|
|
416
543
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
message = response.text || "";
|
|
421
|
-
} catch (textError) {
|
|
422
|
-
// Sometimes response.text throws when there are function calls
|
|
423
|
-
logger.debug(`[GeminiProvider] Could not get response.text (likely due to function calls):`, textError);
|
|
424
|
-
|
|
425
|
-
// Try to extract text parts manually
|
|
426
|
-
if (response.candidates && response.candidates[0]?.content?.parts) {
|
|
427
|
-
const textParts = response.candidates[0].content.parts
|
|
428
|
-
.filter(part => part.text)
|
|
429
|
-
.map(part => part.text)
|
|
430
|
-
.join('');
|
|
431
|
-
message = textParts;
|
|
432
|
-
logger.debug(`[GeminiProvider] Extracted text from parts:`, message);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
544
|
+
|
|
545
|
+
// Safely extract text — .text getter can throw when response has only function calls
|
|
546
|
+
const message = this.safeExtractText(response);
|
|
435
547
|
|
|
436
548
|
// Only throw error if we have no text AND no function calls
|
|
437
549
|
if (!message && toolCalls.length === 0) {
|
|
@@ -443,15 +555,8 @@ export class GeminiProvider implements AiProvider {
|
|
|
443
555
|
// Log when we have function calls but no text (this is normal)
|
|
444
556
|
if (toolCalls.length > 0 && !message) {
|
|
445
557
|
logger.debug(`[GeminiProvider] Function calls detected without text message:`, toolCalls.map(tc => tc.toolName));
|
|
446
|
-
} else if (toolCalls.length > 0 && message) {
|
|
447
|
-
logger.debug(`[GeminiProvider] Response has both text and function calls:`, {
|
|
448
|
-
messageLength: message.length,
|
|
449
|
-
toolCalls: toolCalls.map(tc => tc.toolName),
|
|
450
|
-
});
|
|
451
558
|
}
|
|
452
559
|
|
|
453
|
-
|
|
454
|
-
|
|
455
560
|
// Parse JSON response if schema was provided
|
|
456
561
|
let structured: AgentStructuredResponse | undefined;
|
|
457
562
|
if (input.parameters?.jsonSchema) {
|
|
@@ -574,15 +679,8 @@ export class GeminiProvider implements AiProvider {
|
|
|
574
679
|
if (hasTools) {
|
|
575
680
|
const toolNames = input.tools?.map((tool) => tool.name || tool.id) || [];
|
|
576
681
|
logger.debug(`[GeminiProvider] Configuring ${toolNames.length} tools for streaming:`, toolNames);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
functionDeclarations: input.tools?.map((tool) => ({
|
|
580
|
-
name: tool.name || tool.id,
|
|
581
|
-
description: tool.description || "",
|
|
582
|
-
parameters: tool.parameters as FunctionDeclaration["parameters"],
|
|
583
|
-
})),
|
|
584
|
-
},
|
|
585
|
-
];
|
|
682
|
+
const functionDeclarations = this.buildFunctionDeclarations(input.tools!);
|
|
683
|
+
configOverride.tools = [{ functionDeclarations }];
|
|
586
684
|
|
|
587
685
|
} else if (hasJsonSchema) {
|
|
588
686
|
// Only set JSON schema if no tools are present
|
|
@@ -595,10 +693,29 @@ export class GeminiProvider implements AiProvider {
|
|
|
595
693
|
|
|
596
694
|
let stream;
|
|
597
695
|
try {
|
|
696
|
+
// Build contents from history
|
|
697
|
+
const { contents: historyContents, systemInstructions } = this.buildGeminiContents(input.history);
|
|
698
|
+
|
|
699
|
+
// Append the current prompt as the final user content
|
|
700
|
+
historyContents.push({ role: "user", parts: [{ text: input.prompt }] });
|
|
701
|
+
|
|
702
|
+
// Set system instruction from history if present
|
|
703
|
+
if (systemInstructions.length > 0) {
|
|
704
|
+
const existingSystem = configOverride.systemInstruction;
|
|
705
|
+
if (typeof existingSystem === "string") {
|
|
706
|
+
configOverride.systemInstruction = `${existingSystem}\n\n${systemInstructions.join("\n\n")}`;
|
|
707
|
+
} else {
|
|
708
|
+
configOverride.systemInstruction = systemInstructions.join("\n\n");
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
598
712
|
stream = await this.genAI.models.generateContentStream({
|
|
599
713
|
model,
|
|
600
|
-
contents:
|
|
601
|
-
config:
|
|
714
|
+
contents: historyContents,
|
|
715
|
+
config: {
|
|
716
|
+
...configOverride,
|
|
717
|
+
...(input.signal ? { abortSignal: input.signal } : {}),
|
|
718
|
+
},
|
|
602
719
|
});
|
|
603
720
|
} catch (error: unknown) {
|
|
604
721
|
logger.error(`[GeminiProvider] Streaming API call failed:`, error);
|
|
@@ -615,7 +732,10 @@ export class GeminiProvider implements AiProvider {
|
|
|
615
732
|
}> = [];
|
|
616
733
|
|
|
617
734
|
for await (const chunk of stream) {
|
|
618
|
-
|
|
735
|
+
if (input.signal?.aborted) break;
|
|
736
|
+
|
|
737
|
+
// Safely extract text — chunk.text can throw when chunk has only function calls
|
|
738
|
+
const delta = this.safeExtractText(chunk);
|
|
619
739
|
|
|
620
740
|
// Extract tool calls from chunk
|
|
621
741
|
if (chunk.candidates && chunk.candidates[0]?.content?.parts) {
|
|
@@ -623,7 +743,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
623
743
|
if (part.functionCall) {
|
|
624
744
|
toolCalls.push({
|
|
625
745
|
toolName: part.functionCall.name || "",
|
|
626
|
-
arguments: part.functionCall.args as Record<string, unknown
|
|
746
|
+
arguments: (part.functionCall.args as Record<string, unknown>) || {},
|
|
627
747
|
});
|
|
628
748
|
}
|
|
629
749
|
}
|
|
@@ -657,12 +777,12 @@ export class GeminiProvider implements AiProvider {
|
|
|
657
777
|
}
|
|
658
778
|
}
|
|
659
779
|
|
|
660
|
-
//
|
|
780
|
+
// Include tool calls in structured response (even without JSON schema)
|
|
661
781
|
if (toolCalls.length > 0) {
|
|
662
782
|
structured = {
|
|
663
783
|
message: structured?.message || accumulated,
|
|
664
|
-
toolCalls,
|
|
665
784
|
...structured,
|
|
785
|
+
toolCalls,
|
|
666
786
|
} as AgentStructuredResponse;
|
|
667
787
|
}
|
|
668
788
|
|