@agentforge-ai/cli 0.5.4 → 0.5.5

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,331 @@
1
+ /**
2
+ * Chat Actions for AgentForge
3
+ *
4
+ * This module provides the core chat execution pipeline:
5
+ * 1. User sends a message → stored via mutation
6
+ * 2. Convex action triggers LLM generation via OpenRouter (AI SDK)
7
+ * 3. Assistant response stored back in Convex
8
+ * 4. Real-time subscription updates the UI automatically
9
+ *
10
+ * Uses the Vercel AI SDK with OpenRouter as an OpenAI-compatible provider.
11
+ * No dynamic imports of @mastra/core — we use the AI SDK directly in Convex
12
+ * Node.js actions for maximum reliability.
13
+ */
14
+ import { action, mutation, query } from "./_generated/server";
15
+ import { v } from "convex/values";
16
+ import { api } from "./_generated/api";
17
+
18
+ // ============================================================
19
+ // Queries
20
+ // ============================================================
21
+
22
+ /**
23
+ * Get the current chat state for a thread: messages + thread metadata.
24
+ */
25
+ export const getThreadMessages = query({
26
+ args: { threadId: v.id("threads") },
27
+ handler: async (ctx, args) => {
28
+ const messages = await ctx.db
29
+ .query("messages")
30
+ .withIndex("byThread", (q) => q.eq("threadId", args.threadId))
31
+ .collect();
32
+ return messages;
33
+ },
34
+ });
35
+
36
+ /**
37
+ * List all threads for a user, ordered by most recent activity.
38
+ */
39
+ export const listThreads = query({
40
+ args: {
41
+ userId: v.optional(v.string()),
42
+ agentId: v.optional(v.string()),
43
+ },
44
+ handler: async (ctx, args) => {
45
+ let threads;
46
+ if (args.agentId) {
47
+ threads = await ctx.db
48
+ .query("threads")
49
+ .withIndex("byAgentId", (q) => q.eq("agentId", args.agentId!))
50
+ .collect();
51
+ } else if (args.userId) {
52
+ threads = await ctx.db
53
+ .query("threads")
54
+ .withIndex("byUserId", (q) => q.eq("userId", args.userId!))
55
+ .collect();
56
+ } else {
57
+ threads = await ctx.db.query("threads").collect();
58
+ }
59
+ // Sort by most recently updated
60
+ return threads.sort((a, b) => b.updatedAt - a.updatedAt);
61
+ },
62
+ });
63
+
64
+ // ============================================================
65
+ // Mutations
66
+ // ============================================================
67
+
68
+ /**
69
+ * Create a new chat thread for an agent.
70
+ */
71
+ export const createThread = mutation({
72
+ args: {
73
+ agentId: v.string(),
74
+ name: v.optional(v.string()),
75
+ userId: v.optional(v.string()),
76
+ },
77
+ handler: async (ctx, args) => {
78
+ const now = Date.now();
79
+ const threadId = await ctx.db.insert("threads", {
80
+ name: args.name || "New Chat",
81
+ agentId: args.agentId,
82
+ userId: args.userId,
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ });
86
+ return threadId;
87
+ },
88
+ });
89
+
90
+ /**
91
+ * Store a user message in a thread (called before triggering LLM).
92
+ */
93
+ export const addUserMessage = mutation({
94
+ args: {
95
+ threadId: v.id("threads"),
96
+ content: v.string(),
97
+ },
98
+ handler: async (ctx, args) => {
99
+ const messageId = await ctx.db.insert("messages", {
100
+ threadId: args.threadId,
101
+ role: "user",
102
+ content: args.content,
103
+ createdAt: Date.now(),
104
+ });
105
+ await ctx.db.patch(args.threadId, { updatedAt: Date.now() });
106
+ return messageId;
107
+ },
108
+ });
109
+
110
+ /**
111
+ * Store an assistant message in a thread (called after LLM responds).
112
+ */
113
+ export const addAssistantMessage = mutation({
114
+ args: {
115
+ threadId: v.id("threads"),
116
+ content: v.string(),
117
+ metadata: v.optional(v.any()),
118
+ },
119
+ handler: async (ctx, args) => {
120
+ const messageId = await ctx.db.insert("messages", {
121
+ threadId: args.threadId,
122
+ role: "assistant",
123
+ content: args.content,
124
+ metadata: args.metadata,
125
+ createdAt: Date.now(),
126
+ });
127
+ await ctx.db.patch(args.threadId, { updatedAt: Date.now() });
128
+ return messageId;
129
+ },
130
+ });
131
+
132
+ // ============================================================
133
+ // Actions (Node.js runtime — can call external APIs)
134
+ // ============================================================
135
+
136
+ /**
137
+ * Send a message and get an AI response.
138
+ *
139
+ * This is the main chat action. It:
140
+ * 1. Looks up the agent config from the database
141
+ * 2. Stores the user message
142
+ * 3. Builds conversation history from the thread
143
+ * 4. Calls OpenRouter (or other provider) via the AI SDK
144
+ * 5. Stores the assistant response
145
+ * 6. Records usage metrics
146
+ *
147
+ * The UI subscribes to `chat.getThreadMessages` which auto-updates
148
+ * when new messages are inserted.
149
+ */
150
+ export const sendMessage = action({
151
+ args: {
152
+ agentId: v.string(),
153
+ threadId: v.id("threads"),
154
+ content: v.string(),
155
+ userId: v.optional(v.string()),
156
+ },
157
+ handler: async (ctx, args) => {
158
+ // 1. Get agent configuration
159
+ const agent = await ctx.runQuery(api.agents.get, { id: args.agentId });
160
+ if (!agent) {
161
+ throw new Error(`Agent "${args.agentId}" not found. Please create an agent first.`);
162
+ }
163
+
164
+ // 2. Store the user message
165
+ await ctx.runMutation(api.chat.addUserMessage, {
166
+ threadId: args.threadId,
167
+ content: args.content,
168
+ });
169
+
170
+ // 3. Get conversation history for context
171
+ const history = await ctx.runQuery(api.chat.getThreadMessages, {
172
+ threadId: args.threadId,
173
+ });
174
+
175
+ // Build messages array for the LLM (last 20 messages for context window)
176
+ const conversationMessages = history
177
+ .slice(-20)
178
+ .map((msg: { role: string; content: string }) => ({
179
+ role: msg.role as "user" | "assistant" | "system",
180
+ content: msg.content,
181
+ }));
182
+
183
+ // 4. Call the LLM via AI SDK
184
+ let responseText: string;
185
+ let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
186
+
187
+ try {
188
+ // Dynamically import the AI SDK (available in Convex Node.js actions)
189
+ const { generateText } = await import("ai");
190
+ const { createOpenAI } = await import("@ai-sdk/openai");
191
+
192
+ // Resolve the model provider and ID
193
+ const provider = agent.provider || "openrouter";
194
+ const modelId = agent.model || "openai/gpt-4o-mini";
195
+
196
+ // Create the appropriate provider instance
197
+ let model;
198
+ if (provider === "openrouter") {
199
+ const openrouter = createOpenAI({
200
+ baseURL: "https://openrouter.ai/api/v1",
201
+ apiKey: process.env.OPENROUTER_API_KEY,
202
+ });
203
+ model = openrouter(modelId);
204
+ } else if (provider === "openai") {
205
+ const openai = createOpenAI({
206
+ apiKey: process.env.OPENAI_API_KEY,
207
+ });
208
+ model = openai(modelId);
209
+ } else {
210
+ // Default to OpenRouter for any provider — it routes to all models
211
+ const openrouter = createOpenAI({
212
+ baseURL: "https://openrouter.ai/api/v1",
213
+ apiKey: process.env.OPENROUTER_API_KEY,
214
+ });
215
+ // Use provider/model format for OpenRouter routing
216
+ const routerModelId = modelId.includes("/")
217
+ ? modelId
218
+ : `${provider}/${modelId}`;
219
+ model = openrouter(routerModelId);
220
+ }
221
+
222
+ // Generate the response
223
+ const result = await generateText({
224
+ model,
225
+ system: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
226
+ messages: conversationMessages,
227
+ ...(agent.temperature != null && { temperature: agent.temperature }),
228
+ ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
229
+ });
230
+
231
+ responseText = result.text;
232
+
233
+ // Extract usage if available
234
+ if (result.usage) {
235
+ usageData = {
236
+ promptTokens: result.usage.promptTokens || 0,
237
+ completionTokens: result.usage.completionTokens || 0,
238
+ totalTokens: (result.usage.promptTokens || 0) + (result.usage.completionTokens || 0),
239
+ };
240
+ }
241
+ } catch (error: unknown) {
242
+ const errorMessage = error instanceof Error ? error.message : String(error);
243
+ console.error("[chat.sendMessage] LLM error:", errorMessage);
244
+
245
+ // Store error as assistant message so user sees feedback
246
+ responseText = `I encountered an error while processing your request: ${errorMessage}`;
247
+ }
248
+
249
+ // 5. Store the assistant response
250
+ await ctx.runMutation(api.chat.addAssistantMessage, {
251
+ threadId: args.threadId,
252
+ content: responseText,
253
+ metadata: usageData ? { usage: usageData } : undefined,
254
+ });
255
+
256
+ // 6. Record usage metrics (non-blocking, best-effort)
257
+ if (usageData) {
258
+ try {
259
+ await ctx.runMutation(api.usage.record, {
260
+ agentId: args.agentId,
261
+ provider: agent.provider || "openrouter",
262
+ model: agent.model || "unknown",
263
+ promptTokens: usageData.promptTokens,
264
+ completionTokens: usageData.completionTokens,
265
+ totalTokens: usageData.totalTokens,
266
+ userId: args.userId,
267
+ });
268
+ } catch (e) {
269
+ console.error("[chat.sendMessage] Usage recording failed:", e);
270
+ }
271
+ }
272
+
273
+ // 7. Log the interaction
274
+ try {
275
+ await ctx.runMutation(api.logs.add, {
276
+ level: "info",
277
+ source: "chat",
278
+ message: `Agent "${agent.name}" responded to user message`,
279
+ metadata: {
280
+ agentId: args.agentId,
281
+ threadId: args.threadId,
282
+ usage: usageData,
283
+ },
284
+ userId: args.userId,
285
+ });
286
+ } catch (e) {
287
+ console.error("[chat.sendMessage] Logging failed:", e);
288
+ }
289
+
290
+ return {
291
+ success: true,
292
+ threadId: args.threadId,
293
+ response: responseText,
294
+ usage: usageData,
295
+ };
296
+ },
297
+ });
298
+
299
+ /**
300
+ * Create a new thread and send the first message in one action.
301
+ * Convenience action for starting a new conversation.
302
+ */
303
+ export const startNewChat = action({
304
+ args: {
305
+ agentId: v.string(),
306
+ content: v.string(),
307
+ threadName: v.optional(v.string()),
308
+ userId: v.optional(v.string()),
309
+ },
310
+ handler: async (ctx, args) => {
311
+ // Create a new thread
312
+ const threadId = await ctx.runMutation(api.chat.createThread, {
313
+ agentId: args.agentId,
314
+ name: args.threadName || "New Chat",
315
+ userId: args.userId,
316
+ });
317
+
318
+ // Send the first message
319
+ const result = await ctx.runAction(api.chat.sendMessage, {
320
+ agentId: args.agentId,
321
+ threadId,
322
+ content: args.content,
323
+ userId: args.userId,
324
+ });
325
+
326
+ return {
327
+ ...result,
328
+ threadId,
329
+ };
330
+ },
331
+ });
@@ -1,24 +1,89 @@
1
- import { action } from "./_generated/server";
2
- import { v } from "convex/values";
3
- import { api } from "./_generated/api";
4
-
5
1
  /**
6
2
  * Mastra Integration Actions for Convex
7
- *
8
- * These actions run in the Node.js runtime and can use Mastra
9
- * to execute agents and manage workflows.
3
+ *
4
+ * These actions run in the Convex Node.js runtime and execute LLM calls
5
+ * using the Vercel AI SDK with OpenRouter as the default provider.
6
+ *
7
+ * Architecture:
8
+ * - For chat: use `chat.sendMessage` (preferred entry point)
9
+ * - For programmatic agent execution: use `mastraIntegration.executeAgent`
10
+ * - Model resolution: uses AI SDK providers directly (OpenRouter, OpenAI, etc.)
11
+ *
12
+ * This replaces the previous broken approach of dynamically importing
13
+ * @mastra/core inside Convex actions. The AI SDK is the correct way to
14
+ * call LLMs from Convex Node.js actions.
10
15
  */
16
+ import { action } from "./_generated/server";
17
+ import { v } from "convex/values";
18
+ import { api } from "./_generated/api";
11
19
 
12
- // Return type for executeAgent to break circular type inference
20
+ // Return type for executeAgent
13
21
  type ExecuteAgentResult = {
14
22
  success: boolean;
15
23
  threadId: string;
16
24
  sessionId: string;
17
25
  response: string;
18
- usage?: Record<string, unknown>;
26
+ usage?: {
27
+ promptTokens: number;
28
+ completionTokens: number;
29
+ totalTokens: number;
30
+ };
19
31
  };
20
32
 
21
- // Action: Execute agent with Mastra
33
+ /**
34
+ * Resolve a model instance from provider + modelId using the AI SDK.
35
+ *
36
+ * Supports: openrouter, openai, anthropic, google, venice, custom.
37
+ * Falls back to OpenRouter for unknown providers (it routes to all models).
38
+ */
39
+ async function resolveModel(provider: string, modelId: string) {
40
+ const { createOpenAI } = await import("@ai-sdk/openai");
41
+
42
+ switch (provider) {
43
+ case "openai": {
44
+ const openai = createOpenAI({
45
+ apiKey: process.env.OPENAI_API_KEY,
46
+ });
47
+ return openai(modelId);
48
+ }
49
+
50
+ case "anthropic": {
51
+ const { createAnthropic } = await import("@ai-sdk/anthropic");
52
+ const anthropic = createAnthropic({
53
+ apiKey: process.env.ANTHROPIC_API_KEY,
54
+ });
55
+ return anthropic(modelId);
56
+ }
57
+
58
+ case "google": {
59
+ const { createGoogleGenerativeAI } = await import("@ai-sdk/google");
60
+ const google = createGoogleGenerativeAI({
61
+ apiKey: process.env.GEMINI_API_KEY,
62
+ });
63
+ return google(modelId);
64
+ }
65
+
66
+ case "openrouter":
67
+ default: {
68
+ // OpenRouter is OpenAI-compatible and routes to all providers
69
+ const openrouter = createOpenAI({
70
+ baseURL: "https://openrouter.ai/api/v1",
71
+ apiKey: process.env.OPENROUTER_API_KEY,
72
+ });
73
+ const routerModelId = modelId.includes("/")
74
+ ? modelId
75
+ : `${provider}/${modelId}`;
76
+ return openrouter(routerModelId);
77
+ }
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Execute an agent with a prompt and return the response.
83
+ *
84
+ * This is the programmatic API for agent execution. For chat UI,
85
+ * prefer `chat.sendMessage` which handles thread management automatically.
86
+ */
22
87
  export const executeAgent = action({
23
88
  args: {
24
89
  agentId: v.string(),
@@ -30,7 +95,7 @@ export const executeAgent = action({
30
95
  handler: async (ctx, args): Promise<ExecuteAgentResult> => {
31
96
  // Get agent configuration from database
32
97
  const agent = await ctx.runQuery(api.agents.get, { id: args.agentId });
33
-
98
+
34
99
  if (!agent) {
35
100
  throw new Error(`Agent ${args.agentId} not found`);
36
101
  }
@@ -58,50 +123,36 @@ export const executeAgent = action({
58
123
  threadId,
59
124
  agentId: args.agentId,
60
125
  userId: args.userId,
61
- channel: "dashboard",
126
+ channel: "api",
62
127
  });
63
128
 
64
129
  try {
65
- // Import Mastra dynamically (Node.js runtime)
66
- // @ts-expect-error - Mastra is installed at runtime in the user's project
67
- const { Agent } = await import("@mastra/core/agent");
68
-
69
- // Format model string for Mastra
70
- const modelString = agent.model.includes("/")
71
- ? agent.model
72
- : `${agent.provider}/${agent.model}`;
73
-
74
- // Create Mastra agent
75
- const mastraAgent = new Agent({
76
- id: agent.id,
77
- name: agent.name,
78
- instructions: agent.instructions,
79
- model: modelString,
80
- tools: agent.tools || {},
81
- ...(agent.temperature && { temperature: agent.temperature }),
82
- ...(agent.maxTokens && { maxTokens: agent.maxTokens }),
83
- ...(agent.topP && { topP: agent.topP }),
84
- });
130
+ const { generateText } = await import("ai");
131
+
132
+ // Resolve the model
133
+ const provider = agent.provider || "openrouter";
134
+ const modelId = agent.model || "openai/gpt-4o-mini";
135
+ const model = await resolveModel(provider, modelId);
85
136
 
86
137
  // Get conversation history for context
87
- const messages = await ctx.runQuery(api.messages.list, { threadId }) as Array<{ role: string; content: string }>;
88
-
89
- // Build context from message history
90
- const context = messages
91
- .slice(-10) // Last 10 messages for context
92
- .map((m) => `${m.role}: ${m.content}`)
93
- .join("\n");
94
-
95
- // Execute agent
96
- const result: any = await mastraAgent.generate(args.prompt, {
97
- ...(args.stream && { stream: args.stream }),
98
- context: context || undefined,
138
+ const messages = await ctx.runQuery(api.messages.list, { threadId });
139
+ const conversationMessages = (messages as Array<{ role: string; content: string }>)
140
+ .slice(-20)
141
+ .map((m) => ({
142
+ role: m.role as "user" | "assistant" | "system",
143
+ content: m.content,
144
+ }));
145
+
146
+ // Execute the LLM call
147
+ const result = await generateText({
148
+ model,
149
+ system: agent.instructions || "You are a helpful AI assistant.",
150
+ messages: conversationMessages,
151
+ ...(agent.temperature != null && { temperature: agent.temperature }),
152
+ ...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
99
153
  });
100
154
 
101
- // Extract response content
102
- const responseContent: string = typeof result === "string"
103
- ? result
104
- : result.text || result.content || JSON.stringify(result);
155
+ const responseContent = result.text;
105
156
 
106
157
  // Add assistant message to thread
107
158
  await ctx.runMutation(api.messages.add, {
@@ -116,17 +167,27 @@ export const executeAgent = action({
116
167
  status: "completed",
117
168
  });
118
169
 
119
- // Record usage (if available in result)
120
- if (result.usage) {
170
+ // Build usage data
171
+ const usage = result.usage
172
+ ? {
173
+ promptTokens: result.usage.promptTokens || 0,
174
+ completionTokens: result.usage.completionTokens || 0,
175
+ totalTokens:
176
+ (result.usage.promptTokens || 0) +
177
+ (result.usage.completionTokens || 0),
178
+ }
179
+ : undefined;
180
+
181
+ // Record usage
182
+ if (usage) {
121
183
  await ctx.runMutation(api.usage.record, {
122
184
  agentId: args.agentId,
123
185
  sessionId,
124
- provider: agent.provider,
125
- model: agent.model,
126
- promptTokens: result.usage.promptTokens || 0,
127
- completionTokens: result.usage.completionTokens || 0,
128
- totalTokens: result.usage.totalTokens || 0,
129
- cost: result.usage.cost,
186
+ provider: agent.provider || "openrouter",
187
+ model: agent.model || "unknown",
188
+ promptTokens: usage.promptTokens,
189
+ completionTokens: usage.completionTokens,
190
+ totalTokens: usage.totalTokens,
130
191
  userId: args.userId,
131
192
  });
132
193
  }
@@ -136,10 +197,11 @@ export const executeAgent = action({
136
197
  threadId: threadId as string,
137
198
  sessionId,
138
199
  response: responseContent,
139
- usage: result.usage,
200
+ usage,
140
201
  };
141
202
  } catch (error: unknown) {
142
- const errorMessage = error instanceof Error ? error.message : String(error);
203
+ const errorMessage =
204
+ error instanceof Error ? error.message : String(error);
143
205
 
144
206
  // Update session status to error
145
207
  await ctx.runMutation(api.sessions.updateStatus, {
@@ -147,19 +209,37 @@ export const executeAgent = action({
147
209
  status: "error",
148
210
  });
149
211
 
150
- // Add error message
212
+ // Add error message to thread
151
213
  await ctx.runMutation(api.messages.add, {
152
214
  threadId,
153
215
  role: "assistant",
154
216
  content: `Error: ${errorMessage}`,
155
217
  });
156
218
 
219
+ // Log the error
220
+ await ctx.runMutation(api.logs.add, {
221
+ level: "error",
222
+ source: "mastraIntegration",
223
+ message: `Agent execution failed: ${errorMessage}`,
224
+ metadata: {
225
+ agentId: args.agentId,
226
+ threadId,
227
+ sessionId,
228
+ },
229
+ userId: args.userId,
230
+ });
231
+
157
232
  throw error;
158
233
  }
159
234
  },
160
235
  });
161
236
 
162
- // Action: Stream agent response
237
+ /**
238
+ * Stream agent response (placeholder — streaming requires SSE/WebSocket).
239
+ *
240
+ * For now, this falls back to non-streaming execution.
241
+ * Full streaming support will be added via Convex HTTP actions + SSE.
242
+ */
163
243
  export const streamAgent = action({
164
244
  args: {
165
245
  agentId: v.string(),
@@ -168,26 +248,31 @@ export const streamAgent = action({
168
248
  userId: v.optional(v.string()),
169
249
  },
170
250
  handler: async (ctx, args): Promise<{ success: boolean; message: string }> => {
171
- // Similar to executeAgent but with streaming support
172
- // This would require WebSocket or SSE implementation
173
- // For now, return a placeholder
251
+ // Fall back to non-streaming execution
252
+ const result = await ctx.runAction(api.mastraIntegration.executeAgent, {
253
+ agentId: args.agentId,
254
+ prompt: args.prompt,
255
+ threadId: args.threadId,
256
+ userId: args.userId,
257
+ });
258
+
174
259
  return {
175
- success: true,
176
- message: "Streaming support coming soon",
260
+ success: result.success,
261
+ message: result.response,
177
262
  };
178
263
  },
179
264
  });
180
265
 
181
- // Action: Execute workflow with multiple agents
266
+ /**
267
+ * Execute workflow with multiple agents (placeholder).
268
+ */
182
269
  export const executeWorkflow = action({
183
270
  args: {
184
271
  workflowId: v.string(),
185
272
  input: v.any(),
186
273
  userId: v.optional(v.string()),
187
274
  },
188
- handler: async (ctx, args): Promise<{ success: boolean; message: string }> => {
189
- // Placeholder for workflow execution
190
- // This would orchestrate multiple agents in sequence or parallel
275
+ handler: async (_ctx, _args): Promise<{ success: boolean; message: string }> => {
191
276
  return {
192
277
  success: true,
193
278
  message: "Workflow execution coming soon",