@nebutra/agents 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Top-level Vercel AI SDK helpers — absorbed from the former
3
+ * `@nebutra/ai-sdk` package during the AI consolidation.
4
+ *
5
+ * Public API (unchanged):
6
+ * configure(), getConfig()
7
+ * generateText(), streamText()
8
+ * embed(), embedMany()
9
+ * createModel(), createEmbeddingModel()
10
+ * models, resolveModel()
11
+ */
12
+
13
+ import {
14
+ embed as _embed,
15
+ embedMany as _embedMany,
16
+ generateText as _generateText,
17
+ streamText as _streamText,
18
+ type GenerateTextResult,
19
+ type JSONValue,
20
+ type ModelMessage,
21
+ type StreamTextResult,
22
+ } from "ai";
23
+ import { runEmbedWithFallback } from "../fallback";
24
+ import {
25
+ type NebutraAIConfig,
26
+ NebutraAIConfigSchema,
27
+ type ResolvedNebutraAIConfig,
28
+ } from "./config";
29
+ import { createModel } from "./provider";
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Singleton config — call `configure()` once at app startup
33
+ // ---------------------------------------------------------------------------
34
+
35
+ let _resolved: ResolvedNebutraAIConfig = NebutraAIConfigSchema.parse({});
36
+
37
+ /**
38
+ * Initialise the global Nebutra AI configuration.
39
+ * Call once in your app entry point (e.g. instrumentation.ts or layout.tsx).
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { configure } from "@nebutra/agents";
44
+ *
45
+ * configure({ provider: "openrouter" });
46
+ * // → reads OPENROUTER_API_KEY from env automatically
47
+ * ```
48
+ */
49
+ export function configure(config: NebutraAIConfig = {}): void {
50
+ _resolved = NebutraAIConfigSchema.parse(config);
51
+ }
52
+
53
+ /** Returns the current resolved config (read-only). */
54
+ export function getConfig(): Readonly<ResolvedNebutraAIConfig> {
55
+ return _resolved;
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Core generation helpers
60
+ // ---------------------------------------------------------------------------
61
+
62
+ /**
63
+ * Stream-completion event passed to {@link StreamOptions.onFinish}.
64
+ * Mirrors the AI SDK's `streamText.onFinish` event shape but is decoupled
65
+ * from the SDK internals so consumers don't break on SDK version bumps.
66
+ */
67
+ export interface StreamFinishEvent {
68
+ /** Final accumulated text of the model's response. */
69
+ text: string;
70
+ /** Reason the stream terminated (e.g. "stop", "length", "tool-calls"). */
71
+ finishReason: string;
72
+ /** Token usage for the request (best-effort; provider-dependent). */
73
+ usage: {
74
+ inputTokens: number | undefined;
75
+ outputTokens: number | undefined;
76
+ totalTokens: number | undefined;
77
+ };
78
+ }
79
+
80
+ export interface GenerateOptions {
81
+ /** Model ID or preset alias (e.g. "flagship", "fast", "anthropic/claude-sonnet-4"). */
82
+ model?: string;
83
+ /** System prompt prepended to the conversation. */
84
+ system?: string;
85
+ temperature?: number;
86
+ maxTokens?: number;
87
+ /** OpenRouter-specific provider options (reasoning, cacheControl, etc.). */
88
+ providerOptions?: Record<string, JSONValue | undefined>;
89
+ }
90
+
91
+ /**
92
+ * Options accepted by {@link streamText}. Adds the durable `onFinish` hook on
93
+ * top of {@link GenerateOptions}.
94
+ */
95
+ export interface StreamOptions extends GenerateOptions {
96
+ /**
97
+ * Invoked once the model has finished streaming, regardless of whether the
98
+ * client kept the response connection open. Use this for server-side
99
+ * persistence (e.g. saving chat sessions to a database) — it is the durable
100
+ * hook for write-once side-effects.
101
+ *
102
+ * Errors thrown inside the callback are caught by the AI SDK and logged;
103
+ * they do not surface to the streaming client.
104
+ */
105
+ onFinish?: (event: StreamFinishEvent) => void | Promise<void>;
106
+ }
107
+
108
+ /**
109
+ * Generate a complete text response.
110
+ */
111
+ export async function generateText(
112
+ messages: ModelMessage[],
113
+ options: GenerateOptions = {},
114
+ ): Promise<GenerateTextResult<Record<string, never>, never>> {
115
+ const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
116
+
117
+ return await _generateText({
118
+ model,
119
+ messages,
120
+ ...(options.system ? { system: options.system } : {}),
121
+ temperature: options.temperature ?? _resolved.temperature,
122
+ ...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
123
+ ...(options.providerOptions
124
+ ? { providerOptions: { openrouter: options.providerOptions } }
125
+ : {}),
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Stream a text response for real-time UI.
131
+ */
132
+ export async function streamText(
133
+ messages: ModelMessage[],
134
+ options: StreamOptions = {},
135
+ ): Promise<StreamTextResult<Record<string, never>, never>> {
136
+ const model = createModel(options.model ?? _resolved.defaultModel, _resolved);
137
+ const userOnFinish = options.onFinish;
138
+
139
+ return _streamText({
140
+ model,
141
+ messages,
142
+ ...(options.system ? { system: options.system } : {}),
143
+ temperature: options.temperature ?? _resolved.temperature,
144
+ ...(options.maxTokens ? { maxTokens: options.maxTokens } : {}),
145
+ ...(options.providerOptions
146
+ ? { providerOptions: { openrouter: options.providerOptions } }
147
+ : {}),
148
+ ...(userOnFinish
149
+ ? {
150
+ onFinish: ({ text, finishReason, totalUsage }) => {
151
+ const event: StreamFinishEvent = {
152
+ text,
153
+ finishReason: String(finishReason),
154
+ usage: {
155
+ inputTokens: totalUsage?.inputTokens,
156
+ outputTokens: totalUsage?.outputTokens,
157
+ totalTokens: totalUsage?.totalTokens,
158
+ },
159
+ };
160
+ return userOnFinish(event);
161
+ },
162
+ }
163
+ : {}),
164
+ });
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Embeddings
169
+ // ---------------------------------------------------------------------------
170
+
171
+ export interface EmbedOptions {
172
+ /** Embedding model ID or preset alias. Defaults to "embedding". */
173
+ model?: string;
174
+ }
175
+
176
+ /**
177
+ * Generate an embedding vector for a single value.
178
+ *
179
+ * Uses `runEmbedWithFallback()` so retryable failures (429 / 5xx / network)
180
+ * automatically rotate to the next provider in `LLM_EMBEDDING_FALLBACK_CHAIN`.
181
+ */
182
+ export async function embed(value: string, options: EmbedOptions = {}) {
183
+ const { result } = await runEmbedWithFallback(async (model) => _embed({ model, value }), {
184
+ model: options.model ?? "embedding",
185
+ });
186
+ return result;
187
+ }
188
+
189
+ /**
190
+ * Generate embedding vectors for multiple values in a single request.
191
+ *
192
+ * Uses `runEmbedWithFallback()` for provider rotation on retryable errors.
193
+ */
194
+ export async function embedMany(values: string[], options: EmbedOptions = {}) {
195
+ const { result } = await runEmbedWithFallback(async (model) => _embedMany({ model, values }), {
196
+ model: options.model ?? "embedding",
197
+ });
198
+ return result;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Re-exports
203
+ // ---------------------------------------------------------------------------
204
+
205
+ export type { GenerateTextResult, ModelMessage, StreamTextResult } from "ai";
206
+ export {
207
+ type NebutraAIConfig,
208
+ NebutraAIConfigSchema,
209
+ type ProviderType,
210
+ type ResolvedNebutraAIConfig,
211
+ } from "./config";
212
+ export type { ModelPreset } from "./models";
213
+ export { models, resolveModel } from "./models";
214
+ export { createEmbeddingModel, createModel } from "./provider";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Model presets for common Nebutra use cases.
3
+ *
4
+ * All IDs use "vendor/model" format (OpenRouter / SiliconFlow).
5
+ * When using direct OpenAI provider, only OpenAI models are valid.
6
+ * When using Vercel AI Gateway, use "provider/model" format.
7
+ * When using SiliconFlow, use "Vendor/Model" format (e.g. "Qwen/Qwen2.5-72B-Instruct").
8
+ */
9
+ export const models = {
10
+ /** High-quality reasoning — default for complex tasks */
11
+ flagship: "anthropic/claude-sonnet-4",
12
+
13
+ /** Deep reasoning for architecture and research */
14
+ reasoning: "anthropic/claude-opus-4",
15
+
16
+ /** Fast + cheap — chat, summaries, classification */
17
+ fast: "anthropic/claude-haiku-4",
18
+
19
+ /** OpenAI flagship */
20
+ "openai-flagship": "openai/gpt-5.4",
21
+
22
+ /** Google flagship */
23
+ "google-flagship": "google/gemini-2.5-pro",
24
+
25
+ /** Google fast */
26
+ "google-fast": "google/gemini-2.5-flash",
27
+
28
+ /** Embedding model */
29
+ embedding: "openai/text-embedding-3-small",
30
+
31
+ /** Embedding model (high-dimensional) */
32
+ "embedding-large": "openai/text-embedding-3-large",
33
+
34
+ // --- SiliconFlow presets (use with provider: "siliconflow") ---
35
+
36
+ /** SiliconFlow — Qwen 2.5 72B (flagship open-source) */
37
+ "sf-qwen": "Qwen/Qwen2.5-72B-Instruct",
38
+
39
+ /** SiliconFlow — DeepSeek R1 reasoning (via SiliconFlow Pro) */
40
+ "sf-deepseek-r1": "Pro/deepseek-ai/DeepSeek-R1",
41
+
42
+ /** SiliconFlow — DeepSeek V3 (fast, capable) */
43
+ "sf-deepseek-v3": "deepseek-ai/DeepSeek-V3",
44
+ } as const;
45
+
46
+ export type ModelPreset = keyof typeof models;
47
+
48
+ /**
49
+ * Resolves a model preset alias to its full model ID.
50
+ * If the input is not a preset key, returns it as-is (passthrough).
51
+ */
52
+ export function resolveModel(modelOrPreset: string): string {
53
+ if (modelOrPreset in models) {
54
+ return models[modelOrPreset as ModelPreset];
55
+ }
56
+ return modelOrPreset;
57
+ }
@@ -0,0 +1,80 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { createOpenRouter } from "@openrouter/ai-sdk-provider";
3
+ import type { EmbeddingModel, LanguageModel } from "ai";
4
+ import { type ResolvedNebutraAIConfig, resolveApiKey } from "./config";
5
+ import { resolveModel } from "./models";
6
+
7
+ /**
8
+ * Creates a language model instance based on the resolved config.
9
+ *
10
+ * Provider routing:
11
+ * - "openrouter" → @openrouter/ai-sdk-provider (300+ models, failover)
12
+ * - "openai" → @ai-sdk/openai (direct OpenAI API)
13
+ * - "siliconflow" → @ai-sdk/openai with SiliconFlow baseURL (OpenAI-compatible)
14
+ * - "gateway" → Vercel AI Gateway via plain model string (OIDC auth)
15
+ */
16
+ export function createModel(modelOrPreset: string, config: ResolvedNebutraAIConfig): LanguageModel {
17
+ const modelId = resolveModel(modelOrPreset);
18
+ const apiKey = resolveApiKey(config);
19
+
20
+ switch (config.provider) {
21
+ case "openrouter": {
22
+ const provider = createOpenRouter({
23
+ apiKey,
24
+ ...(config.headers ? { headers: config.headers } : {}),
25
+ ...(config.extraBody ? { extraBody: config.extraBody } : {}),
26
+ });
27
+ return provider.chat(modelId);
28
+ }
29
+
30
+ case "openai": {
31
+ const provider = createOpenAI({ apiKey });
32
+ return provider(modelId);
33
+ }
34
+
35
+ case "siliconflow": {
36
+ // SiliconFlow: OpenAI-compatible API with China-optimized infra
37
+ // Models use "Vendor/Model" format, e.g. "Qwen/Qwen2.5-72B-Instruct"
38
+ const provider = createOpenAI({
39
+ apiKey,
40
+ baseURL: "https://api.siliconflow.cn/v1",
41
+ });
42
+ return provider(modelId);
43
+ }
44
+
45
+ case "gateway": {
46
+ // Vercel AI Gateway: use @ai-sdk/openai with gateway baseURL
47
+ // OIDC token is passed as apiKey, routed through Vercel's proxy
48
+ const provider = createOpenAI({
49
+ apiKey,
50
+ baseURL: "https://ai-gateway.vercel.sh/v1",
51
+ });
52
+ return provider(modelId);
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Creates an embedding model instance for vector operations.
59
+ * Only supported with OpenRouter provider.
60
+ */
61
+ export function createEmbeddingModel(
62
+ modelOrPreset: string,
63
+ config: ResolvedNebutraAIConfig,
64
+ ): EmbeddingModel {
65
+ const modelId = resolveModel(modelOrPreset);
66
+ const apiKey = resolveApiKey(config);
67
+
68
+ if (config.provider !== "openrouter") {
69
+ throw new Error(
70
+ `[@nebutra/agents] Embedding models are currently only supported with the "openrouter" provider.`,
71
+ );
72
+ }
73
+
74
+ const provider = createOpenRouter({
75
+ apiKey,
76
+ ...(config.headers ? { headers: config.headers } : {}),
77
+ });
78
+
79
+ return provider.textEmbeddingModel(modelId);
80
+ }
package/src/tenant.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Tenant-scoped agent execution context.
3
+ *
4
+ * Ensures every agent operation carries tenantId for RLS,
5
+ * billing, and audit trail purposes.
6
+ */
7
+
8
+ import type { AgentContext } from "./types";
9
+
10
+ /**
11
+ * Create a fully-populated AgentContext.
12
+ * Generates a random conversationId when none is provided.
13
+ */
14
+ export function createAgentContext(
15
+ tenantId: string,
16
+ userId: string,
17
+ conversationId?: string,
18
+ metadata?: Record<string, unknown>,
19
+ ): AgentContext {
20
+ return {
21
+ tenantId,
22
+ userId,
23
+ conversationId: conversationId ?? crypto.randomUUID(),
24
+ ...(metadata !== undefined ? { metadata } : {}),
25
+ };
26
+ }
27
+
28
+ /**
29
+ * Validate that a tenant has remaining quota for agent execution.
30
+ *
31
+ * Returns `{ allowed: true, remaining: -1 }` (unlimited) by default.
32
+ * Integrate with @nebutra/billing entitlements for production usage.
33
+ */
34
+ export async function checkAgentQuota(
35
+ tenantId: string,
36
+ ): Promise<{ allowed: boolean; remaining: number }> {
37
+ try {
38
+ const { getCreditBalance } = await import("@nebutra/billing/credits");
39
+ const balance = await getCreditBalance(tenantId);
40
+
41
+ // Simple quota check: tenant must have positive credits
42
+ // In production, we might check plan-specific monthly limits beforehand
43
+ if (balance.balance > 0) {
44
+ return { allowed: true, remaining: balance.balance };
45
+ }
46
+
47
+ return { allowed: false, remaining: 0 };
48
+ } catch (_err) {
49
+ // Failsafe open if billing is unconfigured
50
+ return { allowed: true, remaining: -1 };
51
+ }
52
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Built-in tool registry for agents.
3
+ *
4
+ * These are safe, tenant-scoped stubs that demonstrate the tool interface.
5
+ * Each tool requires configuration (API keys, vector stores, etc.) before
6
+ * it produces real results.
7
+ */
8
+
9
+ import type { AgentTool } from "./types";
10
+
11
+ /** Search the web for current information. */
12
+ export const webSearchTool: AgentTool = {
13
+ name: "web_search",
14
+ description: "Search the web for current information",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: { query: { type: "string" } },
18
+ required: ["query"],
19
+ },
20
+ execute: async (_input, _context) => {
21
+ return {
22
+ results: [],
23
+ note: "Configure TAVILY_API_KEY or SERPER_API_KEY for live search",
24
+ };
25
+ },
26
+ };
27
+
28
+ /** Query the tenant's database (tenant-scoped via RLS). */
29
+ export const databaseQueryTool: AgentTool = {
30
+ name: "database_query",
31
+ description: "Query the tenant's database",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: { query: { type: "string" } },
35
+ required: ["query"],
36
+ },
37
+ execute: async (_input, context) => {
38
+ return {
39
+ note: `Query would execute in tenant ${context.tenantId} scope`,
40
+ };
41
+ },
42
+ };
43
+
44
+ /** RAG-style knowledge base retrieval. */
45
+ export const knowledgeBaseTool: AgentTool = {
46
+ name: "knowledge_base",
47
+ description: "Search the organization's knowledge base for relevant documents",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {
51
+ query: { type: "string" },
52
+ limit: { type: "number" },
53
+ },
54
+ },
55
+ execute: async (_input, _context) => {
56
+ return { documents: [], note: "Configure vector store for RAG" };
57
+ },
58
+ };
59
+
60
+ /** All pre-built tools. */
61
+ export const BUILT_IN_TOOLS: readonly AgentTool[] = [
62
+ webSearchTool,
63
+ databaseQueryTool,
64
+ knowledgeBaseTool,
65
+ ];
package/src/types.ts ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Core types for the multi-agent orchestration engine.
3
+ *
4
+ * All tenant-scoped operations require an AgentContext carrying `tenantId`.
5
+ * Usage tracking is emitted on every execution for downstream billing.
6
+ */
7
+
8
+ // ─── Agent Configuration ──────────────────────────────────────────────────────
9
+
10
+ export interface AgentConfig {
11
+ /** Unique identifier for this agent */
12
+ readonly id: string;
13
+ /** Human-readable name */
14
+ readonly name: string;
15
+ /** What this agent does (used by the router for intent matching) */
16
+ readonly description: string;
17
+ /** Model identifier, e.g. "openai/gpt-5.4" or "anthropic/claude-sonnet-4.6" */
18
+ readonly model: string;
19
+ /** System prompt / instructions */
20
+ readonly instructions: string;
21
+ /** Tools this agent can invoke */
22
+ readonly tools?: readonly AgentTool[];
23
+ /** Maximum tool-loop iterations (default 20) */
24
+ readonly maxSteps?: number;
25
+ /** Memory configuration */
26
+ readonly memory?: MemoryConfig;
27
+ }
28
+
29
+ // ─── Tools ────────────────────────────────────────────────────────────────────
30
+
31
+ export interface AgentTool {
32
+ readonly name: string;
33
+ readonly description: string;
34
+ readonly inputSchema: Record<string, unknown>;
35
+ readonly execute: (input: unknown, context: AgentContext) => Promise<unknown>;
36
+ }
37
+
38
+ // ─── Execution Context ────────────────────────────────────────────────────────
39
+
40
+ export interface AgentContext {
41
+ readonly tenantId: string;
42
+ readonly userId: string;
43
+ readonly conversationId: string;
44
+ readonly metadata?: Record<string, unknown>;
45
+ }
46
+
47
+ // ─── Messages ─────────────────────────────────────────────────────────────────
48
+
49
+ export interface AgentMessage {
50
+ readonly role: "user" | "assistant" | "system" | "tool";
51
+ readonly content: string;
52
+ readonly toolCalls?: readonly ToolCallResult[];
53
+ readonly timestamp: Date;
54
+ }
55
+
56
+ export interface ToolCallResult {
57
+ readonly toolName: string;
58
+ readonly args: unknown;
59
+ readonly result: unknown;
60
+ }
61
+
62
+ // ─── Response ─────────────────────────────────────────────────────────────────
63
+
64
+ export interface AgentResponse {
65
+ readonly messages: readonly AgentMessage[];
66
+ readonly usage: TokenUsage;
67
+ readonly finishReason: string;
68
+ readonly agentId: string;
69
+ }
70
+
71
+ export interface TokenUsage {
72
+ readonly promptTokens: number;
73
+ readonly completionTokens: number;
74
+ readonly totalTokens: number;
75
+ }
76
+
77
+ // ─── Memory ───────────────────────────────────────────────────────────────────
78
+
79
+ export interface MemoryConfig {
80
+ readonly shortTerm?: { readonly maxMessages: number };
81
+ readonly longTerm?: { readonly enabled: boolean };
82
+ }
83
+
84
+ // ─── Orchestrator ─────────────────────────────────────────────────────────────
85
+
86
+ export interface OrchestratorConfig {
87
+ readonly agents: readonly AgentConfig[];
88
+ readonly router?: RouterConfig;
89
+ readonly defaultAgentId?: string;
90
+ }
91
+
92
+ export interface RouterConfig {
93
+ readonly strategy: "keyword" | "llm" | "custom";
94
+ readonly customRouter?: (message: string, context: AgentContext) => Promise<string>;
95
+ }
96
+
97
+ export interface PipelineStep {
98
+ readonly agentId: string;
99
+ readonly transformInput?: (prevOutput: string) => string;
100
+ }
101
+
102
+ // ─── Usage / Billing ──────────────────────────────────────────────────────────
103
+
104
+ export interface AgentUsageEvent {
105
+ readonly tenantId: string;
106
+ readonly userId: string;
107
+ readonly agentId: string;
108
+ readonly model: string;
109
+ readonly promptTokens: number;
110
+ readonly completionTokens: number;
111
+ readonly totalTokens: number;
112
+ readonly durationMs: number;
113
+ readonly timestamp: Date;
114
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "target": "esnext",
7
+ "types": ["node"],
8
+ "incremental": false
9
+ },
10
+ "include": ["src"],
11
+ "exclude": ["node_modules", "dist"]
12
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: [
5
+ "src/index.ts",
6
+ "src/tools.ts",
7
+ "src/providers/vercel-ai.ts",
8
+ "src/providers/langchain.ts",
9
+ "src/sdk/index.ts",
10
+ "src/sdk/config.ts",
11
+ "src/sdk/models.ts",
12
+ "src/sdk/provider.ts",
13
+ ],
14
+ format: ["esm"],
15
+ dts: true,
16
+ clean: true,
17
+ target: "node20",
18
+ outDir: "dist",
19
+ splitting: true,
20
+ external: ["ai", "@nebutra/logger", "@nebutra/cache", "@nebutra/billing"],
21
+ });