@nebutra/agents 1.1.1 → 1.1.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 (69) hide show
  1. package/LICENSE +21 -676
  2. package/README.md +7 -3
  3. package/dist/{agent-CahnMASx.d.ts → agent-DDGWuUpe.d.ts} +1 -1
  4. package/{src/env.ts → dist/chunk-BMSL4E4A.js} +23 -47
  5. package/dist/{chunk-5JZJ5KMC.js → chunk-BZKIMAGK.js} +16 -7
  6. package/dist/{chunk-NPQECBXL.js → chunk-GRSMUTUS.js} +60 -7
  7. package/dist/{chunk-B7XWL35G.js → chunk-QYZDHC5A.js} +5 -1
  8. package/dist/{chunk-RLWM437Q.js → chunk-RNFUEMUB.js} +10 -3
  9. package/dist/chunk-RWQL4HXC.js +171 -0
  10. package/dist/{chunk-NVPE5EDI.js → chunk-V6VC2O6Q.js} +4 -3
  11. package/dist/chunk-VZPQOXWW.js +47 -0
  12. package/dist/{chunk-5LX742GP.js → chunk-XLBS3XUI.js} +4 -50
  13. package/dist/env.d.ts +42 -0
  14. package/dist/env.js +12 -0
  15. package/dist/fallback.d.ts +100 -0
  16. package/dist/fallback.js +18 -0
  17. package/dist/generation/index.d.ts +122 -0
  18. package/dist/generation/index.js +19 -0
  19. package/dist/index.d.ts +21 -328
  20. package/dist/index.js +57 -245
  21. package/dist/observability.d.ts +46 -0
  22. package/dist/observability.js +13 -0
  23. package/dist/providers/langchain.d.ts +2 -2
  24. package/dist/providers/vercel-ai.d.ts +2 -2
  25. package/dist/providers/vercel-ai.js +15 -7
  26. package/dist/sdk/config.d.ts +3 -0
  27. package/dist/sdk/config.js +1 -1
  28. package/dist/sdk/index.d.ts +36 -3
  29. package/dist/sdk/index.js +20 -7
  30. package/dist/sdk/models.d.ts +18 -6
  31. package/dist/sdk/models.js +1 -1
  32. package/dist/sdk/provider.d.ts +1 -0
  33. package/dist/sdk/provider.js +3 -3
  34. package/dist/tools.d.ts +1 -1
  35. package/dist/{types-NtgB3pch.d.ts → types-BytC-HfQ.d.ts} +1 -5
  36. package/package.json +71 -19
  37. package/.turbo/turbo-build.log +0 -40
  38. package/.turbo/turbo-test.log +0 -19
  39. package/.turbo/turbo-typecheck.log +0 -4
  40. package/AGENTS.md +0 -63
  41. package/CHANGELOG.md +0 -36
  42. package/src/__tests__/cost-observability.test.ts +0 -172
  43. package/src/__tests__/fallback-wiring.test.ts +0 -313
  44. package/src/__tests__/generation.test.ts +0 -111
  45. package/src/__tests__/public-api.test.ts +0 -114
  46. package/src/__tests__/runtime-gateway.test.ts +0 -108
  47. package/src/agent.ts +0 -117
  48. package/src/context.ts +0 -99
  49. package/src/fallback.ts +0 -358
  50. package/src/gateway.ts +0 -234
  51. package/src/generation/index.ts +0 -157
  52. package/src/generation/mock-provider.ts +0 -123
  53. package/src/generation/types.ts +0 -87
  54. package/src/index.ts +0 -104
  55. package/src/memory.ts +0 -126
  56. package/src/observability.ts +0 -102
  57. package/src/orchestrator.ts +0 -147
  58. package/src/providers/langchain.ts +0 -28
  59. package/src/providers/vercel-ai.ts +0 -114
  60. package/src/router.ts +0 -158
  61. package/src/sdk/config.ts +0 -73
  62. package/src/sdk/index.ts +0 -214
  63. package/src/sdk/models.ts +0 -57
  64. package/src/sdk/provider.ts +0 -80
  65. package/src/tenant.ts +0 -52
  66. package/src/tools.ts +0 -65
  67. package/src/types.ts +0 -114
  68. package/tsconfig.json +0 -12
  69. package/tsup.config.ts +0 -21
package/src/index.ts DELETED
@@ -1,104 +0,0 @@
1
- // ─── Core ─────────────────────────────────────────────────────────────────────
2
- export { BaseAgent } from "./agent";
3
- // ─── User context (personalization) ───────────────────────────────────────────
4
- export {
5
- buildPersonalizedSystemPrompt,
6
- renderUserContextBlock,
7
- type UserContext,
8
- } from "./context";
9
- // ─── Env / Observability / Fallback ─────────────────────────────────────────
10
- export {
11
- type AgentsEnv,
12
- AgentsEnvSchema,
13
- type FallbackProviderName,
14
- getAgentsEnv,
15
- isLangfuseConfigured,
16
- } from "./env";
17
- export {
18
- buildSystemWithCache,
19
- type CreateFallbackModelOptions,
20
- type EmbeddingFallbackOptions,
21
- type FallbackResult,
22
- filterAvailableProviders,
23
- isRetryableError,
24
- runEmbedWithFallback,
25
- runWithFallback,
26
- withAnthropicCacheControl,
27
- } from "./fallback";
28
- // ─── Generation (image / video modality) ─────────────────────────────────────
29
- // New modality on the same env-key-gated provider layer as the LLM fallback
30
- // chain. `mock` is always available so CI / flag-gated demos need no secret.
31
- export {
32
- _resetGenerationRegistry,
33
- type GenerationCallOptions,
34
- type GenerationContext,
35
- type GenerationModality,
36
- type GenerationProvider,
37
- type GenerationResult,
38
- generateImage,
39
- generateVideo,
40
- type ImageGenerationRequest,
41
- listGenerationProviders,
42
- mockGenerationProvider,
43
- registerGenerationProvider,
44
- type VideoGenerationRequest,
45
- } from "./generation/index";
46
- // ─── Memory ───────────────────────────────────────────────────────────────────
47
- export { clearMemory, getMemory, saveMemory } from "./memory";
48
- export {
49
- buildTelemetryConfig,
50
- flushTelemetry,
51
- initLangfuse,
52
- type TelemetryMetadata,
53
- } from "./observability";
54
- export { AgentOrchestrator } from "./orchestrator";
55
- export { AgentRouter } from "./router";
56
- // ─── Vercel AI SDK helpers (absorbed from @nebutra/ai-sdk) ───────────────────
57
- // Top-level generation, streaming and embedding helpers that wrap the Vercel
58
- // AI SDK (`ai` package) with a single configure()-driven provider resolver.
59
- export {
60
- configure,
61
- createEmbeddingModel,
62
- createModel,
63
- type EmbedOptions,
64
- embed,
65
- embedMany,
66
- type GenerateOptions,
67
- type GenerateTextResult,
68
- generateText,
69
- getConfig,
70
- type ModelMessage,
71
- type ModelPreset,
72
- models,
73
- type NebutraAIConfig,
74
- NebutraAIConfigSchema,
75
- type ProviderType,
76
- type ResolvedNebutraAIConfig,
77
- resolveModel,
78
- type StreamTextResult,
79
- streamText,
80
- } from "./sdk/index";
81
- // ─── Tenant ───────────────────────────────────────────────────────────────────
82
- export { checkAgentQuota, createAgentContext } from "./tenant";
83
- // ─── Tools ────────────────────────────────────────────────────────────────────
84
- export {
85
- BUILT_IN_TOOLS,
86
- databaseQueryTool,
87
- knowledgeBaseTool,
88
- webSearchTool,
89
- } from "./tools";
90
- // ─── Types ────────────────────────────────────────────────────────────────────
91
- export type {
92
- AgentConfig,
93
- AgentContext,
94
- AgentMessage,
95
- AgentResponse,
96
- AgentTool,
97
- AgentUsageEvent,
98
- MemoryConfig,
99
- OrchestratorConfig,
100
- PipelineStep,
101
- RouterConfig,
102
- TokenUsage,
103
- ToolCallResult,
104
- } from "./types";
package/src/memory.ts DELETED
@@ -1,126 +0,0 @@
1
- /**
2
- * Agent memory — Redis-backed per-tenant conversation persistence.
3
- *
4
- * Key format: `agent:memory:{tenantId}:{conversationId}`
5
- * TTL: 7 days (configurable via AGENT_MEMORY_TTL_SECONDS env var).
6
- *
7
- * Graceful degradation: if Redis is unavailable, functions return
8
- * empty arrays / silently skip writes so agents still work in
9
- * in-memory-only mode.
10
- */
11
-
12
- import { logger } from "@nebutra/logger";
13
- import type { AgentMessage } from "./types";
14
-
15
- const DEFAULT_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days
16
-
17
- function getTtl(): number {
18
- const envTtl = process.env.AGENT_MEMORY_TTL_SECONDS;
19
- if (envTtl) {
20
- const parsed = Number.parseInt(envTtl, 10);
21
- if (!Number.isNaN(parsed) && parsed > 0) {
22
- return parsed;
23
- }
24
- }
25
- return DEFAULT_TTL_SECONDS;
26
- }
27
-
28
- function memoryKey(tenantId: string, conversationId: string): string {
29
- return `agent:memory:${tenantId}:${conversationId}`;
30
- }
31
-
32
- /**
33
- * Lazily resolve Redis. Returns null when Redis is not configured
34
- * so callers can gracefully degrade.
35
- */
36
- async function tryGetRedis() {
37
- try {
38
- const { getRedis } = await import("@nebutra/cache");
39
- return getRedis();
40
- } catch {
41
- return null;
42
- }
43
- }
44
-
45
- /**
46
- * Load conversation history from Redis.
47
- * Returns an empty array when Redis is unavailable.
48
- */
49
- export async function getMemory(tenantId: string, conversationId: string): Promise<AgentMessage[]> {
50
- const redis = await tryGetRedis();
51
- if (!redis) return [];
52
-
53
- try {
54
- const raw = await redis.get<string>(memoryKey(tenantId, conversationId));
55
- if (!raw) return [];
56
-
57
- const parsed: unknown = typeof raw === "string" ? JSON.parse(raw) : raw;
58
- if (!Array.isArray(parsed)) return [];
59
-
60
- return parsed.map((m: Record<string, unknown>): AgentMessage => {
61
- const toolCalls = m.toolCalls;
62
- if (Array.isArray(toolCalls) && toolCalls.length > 0) {
63
- return {
64
- role: m.role as AgentMessage["role"],
65
- content: String(m.content ?? ""),
66
- toolCalls: toolCalls as unknown as NonNullable<AgentMessage["toolCalls"]>,
67
- timestamp: new Date(String(m.timestamp)),
68
- };
69
- }
70
- return {
71
- role: m.role as AgentMessage["role"],
72
- content: String(m.content ?? ""),
73
- timestamp: new Date(String(m.timestamp)),
74
- };
75
- });
76
- } catch (error) {
77
- logger.warn("Failed to load agent memory, falling back to empty", {
78
- tenantId,
79
- conversationId,
80
- error,
81
- });
82
- return [];
83
- }
84
- }
85
-
86
- /**
87
- * Persist messages to Redis with TTL.
88
- * Silently skips when Redis is unavailable.
89
- */
90
- export async function saveMemory(
91
- tenantId: string,
92
- conversationId: string,
93
- messages: readonly AgentMessage[],
94
- ): Promise<void> {
95
- const redis = await tryGetRedis();
96
- if (!redis) return;
97
-
98
- try {
99
- const key = memoryKey(tenantId, conversationId);
100
- await redis.set(key, JSON.stringify(messages), { ex: getTtl() });
101
- } catch (error) {
102
- logger.warn("Failed to save agent memory", {
103
- tenantId,
104
- conversationId,
105
- error,
106
- });
107
- }
108
- }
109
-
110
- /**
111
- * Clear conversation memory for a tenant/conversation pair.
112
- */
113
- export async function clearMemory(tenantId: string, conversationId: string): Promise<void> {
114
- const redis = await tryGetRedis();
115
- if (!redis) return;
116
-
117
- try {
118
- await redis.del(memoryKey(tenantId, conversationId));
119
- } catch (error) {
120
- logger.warn("Failed to clear agent memory", {
121
- tenantId,
122
- conversationId,
123
- error,
124
- });
125
- }
126
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * LLM observability via Langfuse.
3
- *
4
- * - No-op when env vars are missing — the package works with zero config.
5
- * - Exposes `experimental_telemetry` settings ready to plug into Vercel AI SDK.
6
- * - Use `LangfuseExporter` from `langfuse-vercel` in your OTEL NodeSDK setup
7
- * for full trace export (see README).
8
- */
9
-
10
- import { logger } from "@nebutra/logger";
11
- import { getAgentsEnv, isLangfuseConfigured } from "./env";
12
-
13
- // `Langfuse` client is dynamically imported so the package starts up
14
- // without telemetry deps when they are not used.
15
- type LangfuseClient = {
16
- trace: (input: unknown) => unknown;
17
- flushAsync: () => Promise<void>;
18
- shutdownAsync: () => Promise<void>;
19
- };
20
-
21
- let _client: LangfuseClient | null | undefined;
22
-
23
- /**
24
- * Returns a configured `Langfuse` client, or `null` when env is missing.
25
- *
26
- * Telemetry is OPTIONAL. If LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY are
27
- * not set, this returns null (no error). Callers must handle the null case.
28
- */
29
- export async function initLangfuse(): Promise<LangfuseClient | null> {
30
- if (_client !== undefined) return _client;
31
-
32
- if (!isLangfuseConfigured()) {
33
- _client = null;
34
- return null;
35
- }
36
-
37
- try {
38
- const env = getAgentsEnv();
39
- const { Langfuse } = await import("langfuse");
40
- _client = new Langfuse({
41
- publicKey: env.LANGFUSE_PUBLIC_KEY!,
42
- secretKey: env.LANGFUSE_SECRET_KEY!,
43
- baseUrl: env.LANGFUSE_HOST,
44
- }) as unknown as LangfuseClient;
45
- logger.info("Langfuse telemetry enabled", { host: env.LANGFUSE_HOST });
46
- return _client;
47
- } catch (error) {
48
- logger.warn("Failed to initialise Langfuse — telemetry disabled", { error });
49
- _client = null;
50
- return null;
51
- }
52
- }
53
-
54
- /** Test helper — clears cached client so subsequent init() re-reads env. */
55
- export function _resetLangfuseCache(): void {
56
- _client = undefined;
57
- }
58
-
59
- export interface TelemetryMetadata {
60
- tenantId?: string | undefined;
61
- userId?: string | undefined;
62
- sessionId?: string | undefined;
63
- agentId?: string | undefined;
64
- [key: string]: unknown;
65
- }
66
-
67
- /**
68
- * Build the `experimental_telemetry` option for AI SDK calls.
69
- * Returns `{ isEnabled: false }` (a safe no-op) when Langfuse is not configured,
70
- * which avoids any OTEL span creation cost.
71
- */
72
- export function buildTelemetryConfig(args: { functionId: string; metadata?: TelemetryMetadata }): {
73
- isEnabled: boolean;
74
- functionId?: string;
75
- metadata?: Record<string, unknown>;
76
- } {
77
- if (!isLangfuseConfigured()) {
78
- return { isEnabled: false };
79
- }
80
-
81
- return {
82
- isEnabled: true,
83
- functionId: args.functionId,
84
- metadata: {
85
- ...(args.metadata ?? {}),
86
- // Langfuse picks up these conventional keys from metadata
87
- ...(args.metadata?.tenantId ? { langfuseUserId: args.metadata.tenantId } : {}),
88
- ...(args.metadata?.sessionId ? { langfuseSessionId: args.metadata.sessionId } : {}),
89
- },
90
- };
91
- }
92
-
93
- /** Flush pending telemetry before process exit. Safe to call when disabled. */
94
- export async function flushTelemetry(): Promise<void> {
95
- const client = await initLangfuse();
96
- if (!client) return;
97
- try {
98
- await client.flushAsync();
99
- } catch (error) {
100
- logger.warn("Langfuse flush failed", { error });
101
- }
102
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * AgentOrchestrator — multi-agent coordination engine.
3
- *
4
- * Supports three execution modes:
5
- * - chat(): route a single message to the best agent
6
- * - pipeline(): chain agents sequentially (output → next input)
7
- * - broadcast(): fan-out to all agents and collect results
8
- */
9
-
10
- import { logger } from "@nebutra/logger";
11
- import { BaseAgent } from "./agent";
12
- import { AgentRouter } from "./router";
13
- import { checkAgentQuota } from "./tenant";
14
- import type {
15
- AgentContext,
16
- AgentMessage,
17
- AgentResponse,
18
- OrchestratorConfig,
19
- PipelineStep,
20
- } from "./types";
21
-
22
- export class AgentOrchestrator {
23
- private readonly agents: Map<string, BaseAgent>;
24
- private readonly router: AgentRouter;
25
- private readonly defaultAgentId: string | undefined;
26
-
27
- constructor(config: OrchestratorConfig) {
28
- this.agents = new Map();
29
- this.defaultAgentId = config.defaultAgentId;
30
-
31
- // Register agents — callers provide AgentConfig[], we wrap in BaseAgent
32
- // In practice, callers will register concrete subclasses (VercelAIAgent, etc.)
33
- for (const agentConfig of config.agents) {
34
- this.agents.set(agentConfig.id, new BaseAgent(agentConfig));
35
- }
36
-
37
- // Set up router
38
- this.router = new AgentRouter(config.router ?? { strategy: "keyword" });
39
- }
40
-
41
- /**
42
- * Register a pre-built agent instance (e.g. VercelAIAgent).
43
- * Overwrites any agent with the same ID.
44
- */
45
- registerAgent(agent: BaseAgent): void {
46
- this.agents.set(agent.config.id, agent);
47
- }
48
-
49
- /**
50
- * Get a registered agent by ID.
51
- */
52
- getAgent(agentId: string): BaseAgent | undefined {
53
- return this.agents.get(agentId);
54
- }
55
-
56
- /**
57
- * Route a message to the best agent and execute.
58
- */
59
- async chat(message: string, context: AgentContext): Promise<AgentResponse> {
60
- await this.assertQuota(context.tenantId);
61
-
62
- const agentConfigs = [...this.agents.values()].map((a) => a.config);
63
- const agentId = await this.router.route(message, agentConfigs, context, this.defaultAgentId);
64
-
65
- const agent = this.agents.get(agentId);
66
- if (!agent) {
67
- throw new Error(`Agent "${agentId}" not found in orchestrator`);
68
- }
69
-
70
- const messages: AgentMessage[] = [{ role: "user", content: message, timestamp: new Date() }];
71
-
72
- return agent.run(messages, context);
73
- }
74
-
75
- /**
76
- * Execute a multi-agent pipeline where each step's output feeds the next.
77
- * @experimental — API may change. Use `chat()` for production workloads.
78
- */
79
- async pipeline(
80
- steps: readonly PipelineStep[],
81
- input: string,
82
- context: AgentContext,
83
- ): Promise<AgentResponse> {
84
- await this.assertQuota(context.tenantId);
85
-
86
- let currentInput = input;
87
- let lastResponse: AgentResponse | undefined;
88
-
89
- for (const step of steps) {
90
- const agent = this.agents.get(step.agentId);
91
- if (!agent) {
92
- throw new Error(`Pipeline step references unknown agent "${step.agentId}"`);
93
- }
94
-
95
- const transformedInput = step.transformInput
96
- ? step.transformInput(currentInput)
97
- : currentInput;
98
-
99
- const messages: AgentMessage[] = [
100
- { role: "user", content: transformedInput, timestamp: new Date() },
101
- ];
102
-
103
- lastResponse = await agent.run(messages, context);
104
-
105
- // Extract the last assistant message as input for the next step
106
- const assistantMessages = lastResponse.messages.filter((m) => m.role === "assistant");
107
- const lastAssistant = assistantMessages[assistantMessages.length - 1];
108
- currentInput = lastAssistant?.content ?? "";
109
-
110
- logger.info("Pipeline step completed", {
111
- agentId: step.agentId,
112
- tenantId: context.tenantId,
113
- });
114
- }
115
-
116
- if (!lastResponse) {
117
- throw new Error("Pipeline produced no response (empty steps?)");
118
- }
119
-
120
- return lastResponse;
121
- }
122
-
123
- /**
124
- * Broadcast a message to ALL registered agents in parallel.
125
- * Returns an array of responses (one per agent).
126
- * @experimental — API may change. Use `chat()` for production workloads.
127
- */
128
- async broadcast(message: string, context: AgentContext): Promise<readonly AgentResponse[]> {
129
- await this.assertQuota(context.tenantId);
130
-
131
- const messages: AgentMessage[] = [{ role: "user", content: message, timestamp: new Date() }];
132
-
133
- const promises = [...this.agents.values()].map((agent) => agent.run(messages, context));
134
-
135
- return Promise.all(promises);
136
- }
137
-
138
- /**
139
- * Check tenant quota before execution.
140
- */
141
- private async assertQuota(tenantId: string): Promise<void> {
142
- const { allowed } = await checkAgentQuota(tenantId);
143
- if (!allowed) {
144
- throw new Error(`Tenant "${tenantId}" has exceeded agent execution quota`);
145
- }
146
- }
147
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * LangChain.js agent adapter — stub.
3
- *
4
- * This module intentionally throws at construction time to guide
5
- * developers through the required dependency installation.
6
- *
7
- * Once the packages are installed, implement the `execute()` method
8
- * using LangChain's AgentExecutor or the newer LangGraph approach.
9
- */
10
-
11
- import { BaseAgent } from "../agent";
12
- import type { AgentConfig } from "../types";
13
-
14
- export class LangChainAgent extends BaseAgent {
15
- constructor(config: AgentConfig) {
16
- super(config);
17
- throw new Error(
18
- [
19
- "LangChain agent requires additional packages. Install:",
20
- "",
21
- " pnpm add langchain @langchain/core @langchain/openai",
22
- "",
23
- "Then implement the execute() method in this file using",
24
- "LangChain's AgentExecutor or LangGraph.",
25
- ].join("\n"),
26
- );
27
- }
28
- }
@@ -1,114 +0,0 @@
1
- /**
2
- * Vercel AI SDK agent adapter.
3
- *
4
- * Uses `streamText` from the `ai` package with tool-loop support.
5
- * The `ai` peer dependency is dynamically imported so the package
6
- * doesn't fail at require-time when the SDK is absent.
7
- */
8
-
9
- import { BaseAgent } from "../agent";
10
- import { runWithFallback, withAnthropicCacheControl } from "../fallback";
11
- import { buildTelemetryConfig } from "../observability";
12
- import type { AgentContext, AgentMessage, AgentResponse } from "../types";
13
-
14
- export class VercelAIAgent extends BaseAgent {
15
- protected override async execute(
16
- messages: readonly AgentMessage[],
17
- context: AgentContext,
18
- ): Promise<AgentResponse> {
19
- // Dynamic import — avoids hard dependency on `ai`
20
- const { streamText, stepCountIs, dynamicTool } = await import("ai");
21
-
22
- type StreamTextParams = Parameters<typeof streamText>[0];
23
-
24
- // Build AI SDK tool definitions from our AgentTool interface.
25
- // We use dynamicTool() because our AgentTool uses a loose JSON Schema
26
- // record type rather than a typed Zod schema.
27
- const toolSet: StreamTextParams["tools"] = this.config.tools
28
- ? Object.fromEntries(
29
- this.config.tools.map((t) => [
30
- t.name,
31
- dynamicTool({
32
- description: t.description,
33
- inputSchema: t.inputSchema as Parameters<typeof dynamicTool>[0]["inputSchema"],
34
- execute: async (args) => t.execute(args, context),
35
- }),
36
- ]),
37
- )
38
- : undefined;
39
-
40
- // Build streamText options, conditionally including tools to satisfy
41
- // exactOptionalPropertyTypes (tools must not be `undefined`).
42
- //
43
- // Cost optimization: stable content (system prompt + tool defs) is placed
44
- // FIRST and dynamic content (user messages) LAST. This ordering is required
45
- // for both Anthropic explicit prompt caching (90% discount via `cacheControl`)
46
- // and OpenAI automatic caching (≥1024 token stable prefix). Reordering or
47
- // mutating the system prompt invalidates the cache on every call.
48
- const telemetry = buildTelemetryConfig({
49
- functionId: `agent.${this.config.id}`,
50
- metadata: {
51
- tenantId: context.tenantId,
52
- userId: context.userId,
53
- sessionId: context.conversationId,
54
- agentId: this.config.id,
55
- },
56
- });
57
-
58
- const baseOptionsWithoutModel = {
59
- system: this.config.instructions,
60
- messages: messages.map((m) => ({
61
- role: m.role as "user" | "assistant" | "system",
62
- content: m.content,
63
- })),
64
- stopWhen: stepCountIs(this.config.maxSteps ?? 20),
65
- // Anthropic prompt cache control on the system message — 90% cost
66
- // reduction on cached prefix tokens. No-op for non-Anthropic providers.
67
- providerOptions: withAnthropicCacheControl(),
68
- experimental_telemetry: telemetry,
69
- };
70
-
71
- // Run through the multi-provider fallback chain. The chain is filtered
72
- // to providers with API keys present (so single-provider deploys are
73
- // backward-compatible — only the configured provider is tried).
74
- const { result } = await runWithFallback(
75
- async (model) => {
76
- const streamOptions = {
77
- ...baseOptionsWithoutModel,
78
- model,
79
- ...(toolSet !== undefined ? { tools: toolSet } : {}),
80
- } as StreamTextParams;
81
- const r = streamText(streamOptions);
82
- // Materialize the result so retryable errors surface inside the
83
- // try/catch in runWithFallback() rather than escaping as unhandled
84
- // rejections from the lazy stream.
85
- const text = await r.text;
86
- const usage = await r.usage;
87
- return { text, usage };
88
- },
89
- { model: this.config.model },
90
- );
91
-
92
- const { text, usage } = result;
93
-
94
- // AI SDK v6 uses inputTokens/outputTokens on LanguageModelUsage
95
- const inputTokens = usage?.inputTokens ?? 0;
96
- const outputTokens = usage?.outputTokens ?? 0;
97
-
98
- const responseMessages: AgentMessage[] = [
99
- ...messages,
100
- { role: "assistant" as const, content: text, timestamp: new Date() },
101
- ];
102
-
103
- return {
104
- messages: responseMessages,
105
- usage: {
106
- promptTokens: inputTokens,
107
- completionTokens: outputTokens,
108
- totalTokens: inputTokens + outputTokens,
109
- },
110
- finishReason: "stop",
111
- agentId: this.config.id,
112
- };
113
- }
114
- }