@nebutra/agents 1.1.0 → 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 (67) 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 +73 -27
  37. package/.turbo/turbo-build.log +0 -40
  38. package/.turbo/turbo-test.log +0 -18
  39. package/.turbo/turbo-typecheck.log +0 -4
  40. package/AGENTS.md +0 -63
  41. package/CHANGELOG.md +0 -26
  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/agent.ts +0 -117
  47. package/src/context.ts +0 -99
  48. package/src/fallback.ts +0 -358
  49. package/src/generation/index.ts +0 -157
  50. package/src/generation/mock-provider.ts +0 -123
  51. package/src/generation/types.ts +0 -87
  52. package/src/index.ts +0 -104
  53. package/src/memory.ts +0 -126
  54. package/src/observability.ts +0 -102
  55. package/src/orchestrator.ts +0 -147
  56. package/src/providers/langchain.ts +0 -28
  57. package/src/providers/vercel-ai.ts +0 -114
  58. package/src/router.ts +0 -158
  59. package/src/sdk/config.ts +0 -73
  60. package/src/sdk/index.ts +0 -214
  61. package/src/sdk/models.ts +0 -57
  62. package/src/sdk/provider.ts +0 -80
  63. package/src/tenant.ts +0 -52
  64. package/src/tools.ts +0 -65
  65. package/src/types.ts +0 -114
  66. package/tsconfig.json +0 -12
  67. package/tsup.config.ts +0 -21
@@ -1,123 +0,0 @@
1
- /**
2
- * Deterministic mock generation provider.
3
- *
4
- * Always available (`envKey: null`) so CI and flag-gated demos never need a
5
- * paid secret. Output is a stable, content-addressed SVG `data:` URI: the same
6
- * prompt + size always yields byte-identical bytes, which makes canvas
7
- * placement and websocket-sync tests deterministic.
8
- *
9
- * Wiring a real provider (Replicate / OpenAI images / Volces) later is purely
10
- * additive — register it with a non-null `envKey` and it takes priority over
11
- * `mock` in the fallback chain whenever its key is present.
12
- */
13
-
14
- import type {
15
- GenerationContext,
16
- GenerationProvider,
17
- GenerationResult,
18
- ImageGenerationRequest,
19
- VideoGenerationRequest,
20
- } from "./types";
21
-
22
- /** FNV-1a — small, stable, no deps. Used to derive a deterministic hue. */
23
- function hash(input: string): number {
24
- let h = 0x811c9dc5;
25
- for (let i = 0; i < input.length; i++) {
26
- h ^= input.charCodeAt(i);
27
- h = Math.imul(h, 0x01000193);
28
- }
29
- return h >>> 0;
30
- }
31
-
32
- function escapeXml(s: string): string {
33
- return s
34
- .replace(/&/g, "&amp;")
35
- .replace(/</g, "&lt;")
36
- .replace(/>/g, "&gt;")
37
- .replace(/"/g, "&quot;");
38
- }
39
-
40
- function svgDataUri(label: string, prompt: string, w: number, h: number): string {
41
- const hue = hash(prompt) % 360;
42
- const hue2 = (hue + 40) % 360;
43
- // Wrap the prompt to ~32 chars/line, max 4 lines, so the placeholder
44
- // visibly carries its prompt (useful when eyeballing a canvas demo).
45
- const words = prompt.split(/\s+/);
46
- const lines: string[] = [];
47
- let cur = "";
48
- for (const word of words) {
49
- if ((cur + " " + word).trim().length > 32) {
50
- lines.push(cur.trim());
51
- cur = word;
52
- } else {
53
- cur = `${cur} ${word}`;
54
- }
55
- if (lines.length === 4) break;
56
- }
57
- if (cur && lines.length < 4) lines.push(cur.trim());
58
-
59
- const tspans = lines
60
- .map((ln, i) => `<tspan x="50%" dy="${i === 0 ? 0 : 26}">${escapeXml(ln)}</tspan>`)
61
- .join("");
62
-
63
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
64
- <defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
65
- <stop offset="0" stop-color="hsl(${hue} 70% 55%)"/>
66
- <stop offset="1" stop-color="hsl(${hue2} 70% 45%)"/>
67
- </linearGradient></defs>
68
- <rect width="${w}" height="${h}" fill="url(#g)"/>
69
- <text x="50%" y="14%" fill="rgba(255,255,255,.7)" font-family="sans-serif" font-size="20" text-anchor="middle">${escapeXml(label)}</text>
70
- <text x="50%" y="46%" fill="#fff" font-family="sans-serif" font-size="22" font-weight="600" text-anchor="middle">${tspans}</text>
71
- </svg>`;
72
-
73
- // base64 keeps the URI well-formed regardless of prompt characters.
74
- const b64 =
75
- typeof btoa === "function"
76
- ? btoa(unescape(encodeURIComponent(svg)))
77
- : Buffer.from(svg, "utf8").toString("base64");
78
- return `data:image/svg+xml;base64,${b64}`;
79
- }
80
-
81
- export const mockGenerationProvider: GenerationProvider = {
82
- name: "mock",
83
- envKey: null,
84
- capabilities: ["image", "video"],
85
-
86
- async generateImage(
87
- req: ImageGenerationRequest,
88
- _ctx: GenerationContext,
89
- ): Promise<GenerationResult> {
90
- const width = req.width ?? 1024;
91
- const height = req.height ?? 1024;
92
- return {
93
- modality: "image",
94
- mimeType: "image/svg+xml",
95
- url: svgDataUri("mock · image", req.prompt, width, height),
96
- width,
97
- height,
98
- providerName: "mock",
99
- model: req.model ?? "mock-image-1",
100
- usage: { units: 1 },
101
- };
102
- },
103
-
104
- async generateVideo(
105
- req: VideoGenerationRequest,
106
- _ctx: GenerationContext,
107
- ): Promise<GenerationResult> {
108
- const width = req.width ?? 1280;
109
- const height = req.height ?? 720;
110
- const seconds = req.durationSeconds ?? 5;
111
- // No real codec in mock mode — return a poster frame the canvas can embed.
112
- return {
113
- modality: "video",
114
- mimeType: "image/svg+xml",
115
- url: svgDataUri(`mock · video · ${seconds}s`, req.prompt, width, height),
116
- width,
117
- height,
118
- providerName: "mock",
119
- model: req.model ?? "mock-video-1",
120
- usage: { units: seconds },
121
- };
122
- },
123
- };
@@ -1,87 +0,0 @@
1
- /**
2
- * Image / video generation modality for `@nebutra/agents`.
3
- *
4
- * The text + embedding modalities wrap the Vercel AI SDK. Image / video
5
- * generation is a *new modality on the same provider layer*: providers are
6
- * env-key gated exactly like the LLM fallback chain (see `fallback.ts`), so
7
- * single-provider — or zero-provider (mock) — deploys just work.
8
- *
9
- * Generation is tenant-scoped: every call carries a {@link GenerationContext}
10
- * so downstream metering / audit can attribute units to an organization.
11
- */
12
-
13
- /** What a provider can produce. */
14
- export type GenerationModality = "image" | "video";
15
-
16
- /** Tenant-scoped attribution for a generation call (mirrors AgentContext). */
17
- export interface GenerationContext {
18
- readonly tenantId: string;
19
- readonly userId: string;
20
- /** Optional logical grouping (e.g. a canvas / conversation id). */
21
- readonly conversationId?: string;
22
- }
23
-
24
- export interface ImageGenerationRequest {
25
- readonly prompt: string;
26
- /** Pixel width — defaults to 1024. */
27
- readonly width?: number;
28
- /** Pixel height — defaults to 1024. */
29
- readonly height?: number;
30
- /** Optional model id / preset; provider-specific passthrough. */
31
- readonly model?: string;
32
- /** Reference images (data: URI or URL) for edit / variation flows. */
33
- readonly inputImages?: readonly string[];
34
- }
35
-
36
- export interface VideoGenerationRequest {
37
- readonly prompt: string;
38
- /** Clip length in seconds — defaults to 5. */
39
- readonly durationSeconds?: number;
40
- readonly width?: number;
41
- readonly height?: number;
42
- readonly model?: string;
43
- /** Optional first-frame image (data: URI or URL). */
44
- readonly inputImage?: string;
45
- }
46
-
47
- export interface GenerationResult {
48
- readonly modality: GenerationModality;
49
- /** e.g. "image/svg+xml", "image/png", "video/mp4". */
50
- readonly mimeType: string;
51
- /** `data:` URI (mock / inline) or a remote URL the caller can fetch. */
52
- readonly url: string;
53
- readonly width: number;
54
- readonly height: number;
55
- /** Provider that actually produced the asset. */
56
- readonly providerName: string;
57
- /** Model id reported by the provider. */
58
- readonly model: string;
59
- /**
60
- * Best-effort billable units for `@nebutra/metering` (e.g. 1 image,
61
- * N seconds of video). Callers decide the meter mapping.
62
- */
63
- readonly usage: { readonly units: number };
64
- }
65
-
66
- /**
67
- * A generation backend. `envKey` mirrors the LLM provider gating: when the
68
- * variable is absent the provider is filtered out of the chain. `null` means
69
- * "always available" — reserved for the deterministic mock provider so CI and
70
- * flag-gated demos never need a paid secret.
71
- */
72
- export interface GenerationProvider {
73
- readonly name: string;
74
- readonly envKey: string | null;
75
- readonly capabilities: readonly GenerationModality[];
76
- generateImage?(req: ImageGenerationRequest, ctx: GenerationContext): Promise<GenerationResult>;
77
- generateVideo?(req: VideoGenerationRequest, ctx: GenerationContext): Promise<GenerationResult>;
78
- }
79
-
80
- export interface GenerationCallOptions {
81
- /**
82
- * Ordered provider-name preference. Unknown / unavailable names are skipped.
83
- * Defaults to `GENERATION_FALLBACK_CHAIN` env (comma-separated) then registry
84
- * order, always ending at `mock` so a result is guaranteed.
85
- */
86
- readonly chain?: readonly string[];
87
- }
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
- }