@agentforge-ai/cli 0.5.4 → 0.6.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,302 @@
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 Mastra Agent
7
+ * 3. Assistant response stored back in Convex
8
+ * 4. Real-time subscription updates the UI automatically
9
+ *
10
+ * Uses Mastra-native model routing via Agent.generate() with "provider/model-name" format.
11
+ * Mastra auto-reads provider API keys from environment variables.
12
+ */
13
+ import { action, mutation, query } from "./_generated/server";
14
+ import { v } from "convex/values";
15
+ import { api } from "./_generated/api";
16
+ import { Agent } from "@mastra/core/agent";
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 the provider via Mastra Agent.generate()
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 Mastra Agent
184
+ let responseText: string;
185
+ let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
186
+
187
+ try {
188
+ // Resolve the model provider and ID
189
+ const provider = agent.provider || "openrouter";
190
+ const modelId = agent.model || "openai/gpt-4o-mini";
191
+ const modelKey = `${provider}/${modelId}`;
192
+
193
+ // Generate via Mastra Agent
194
+ const mastraAgent = new Agent({
195
+ name: "agentforge-executor",
196
+ instructions: agent.instructions || "You are a helpful AI assistant built with AgentForge.",
197
+ model: modelKey,
198
+ });
199
+
200
+ const result = await mastraAgent.generate(conversationMessages);
201
+
202
+ responseText = result.text;
203
+
204
+ // Extract usage if available
205
+ if (result.usage) {
206
+ usageData = {
207
+ promptTokens: result.usage.promptTokens || 0,
208
+ completionTokens: result.usage.completionTokens || 0,
209
+ totalTokens: (result.usage.promptTokens || 0) + (result.usage.completionTokens || 0),
210
+ };
211
+ }
212
+ } catch (error: unknown) {
213
+ const errorMessage = error instanceof Error ? error.message : String(error);
214
+ console.error("[chat.sendMessage] Mastra error:", errorMessage);
215
+
216
+ // Store error as assistant message so user sees feedback
217
+ responseText = `I encountered an error while processing your request: ${errorMessage}`;
218
+ }
219
+
220
+ // 5. Store the assistant response
221
+ await ctx.runMutation(api.chat.addAssistantMessage, {
222
+ threadId: args.threadId,
223
+ content: responseText,
224
+ metadata: usageData ? { usage: usageData } : undefined,
225
+ });
226
+
227
+ // 6. Record usage metrics (non-blocking, best-effort)
228
+ if (usageData) {
229
+ try {
230
+ await ctx.runMutation(api.usage.record, {
231
+ agentId: args.agentId,
232
+ provider: agent.provider || "openrouter",
233
+ model: agent.model || "unknown",
234
+ promptTokens: usageData.promptTokens,
235
+ completionTokens: usageData.completionTokens,
236
+ totalTokens: usageData.totalTokens,
237
+ userId: args.userId,
238
+ });
239
+ } catch (e) {
240
+ console.error("[chat.sendMessage] Usage recording failed:", e);
241
+ }
242
+ }
243
+
244
+ // 7. Log the interaction
245
+ try {
246
+ await ctx.runMutation(api.logs.add, {
247
+ level: "info",
248
+ source: "chat",
249
+ message: `Agent "${agent.name}" responded to user message`,
250
+ metadata: {
251
+ agentId: args.agentId,
252
+ threadId: args.threadId,
253
+ usage: usageData,
254
+ },
255
+ userId: args.userId,
256
+ });
257
+ } catch (e) {
258
+ console.error("[chat.sendMessage] Logging failed:", e);
259
+ }
260
+
261
+ return {
262
+ success: true,
263
+ threadId: args.threadId,
264
+ response: responseText,
265
+ usage: usageData,
266
+ };
267
+ },
268
+ });
269
+
270
+ /**
271
+ * Create a new thread and send the first message in one action.
272
+ * Convenience action for starting a new conversation.
273
+ */
274
+ export const startNewChat = action({
275
+ args: {
276
+ agentId: v.string(),
277
+ content: v.string(),
278
+ threadName: v.optional(v.string()),
279
+ userId: v.optional(v.string()),
280
+ },
281
+ handler: async (ctx, args) => {
282
+ // Create a new thread
283
+ const threadId = await ctx.runMutation(api.chat.createThread, {
284
+ agentId: args.agentId,
285
+ name: args.threadName || "New Chat",
286
+ userId: args.userId,
287
+ });
288
+
289
+ // Send the first message
290
+ const result = await ctx.runAction(api.chat.sendMessage, {
291
+ agentId: args.agentId,
292
+ threadId,
293
+ content: args.content,
294
+ userId: args.userId,
295
+ });
296
+
297
+ return {
298
+ ...result,
299
+ threadId,
300
+ };
301
+ },
302
+ });
@@ -1,24 +1,38 @@
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 Mastra-native model routing 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 Mastra Agent with "provider/model-name" format
10
11
  */
12
+ import { action } from "./_generated/server";
13
+ import { v } from "convex/values";
14
+ import { api } from "./_generated/api";
15
+ import { Agent } from "@mastra/core/agent";
11
16
 
12
- // Return type for executeAgent to break circular type inference
17
+ // Return type for executeAgent
13
18
  type ExecuteAgentResult = {
14
19
  success: boolean;
15
20
  threadId: string;
16
21
  sessionId: string;
17
22
  response: string;
18
- usage?: Record<string, unknown>;
23
+ usage?: {
24
+ promptTokens: number;
25
+ completionTokens: number;
26
+ totalTokens: number;
27
+ };
19
28
  };
20
29
 
21
- // Action: Execute agent with Mastra
30
+ /**
31
+ * Execute an agent with a prompt and return the response.
32
+ *
33
+ * This is the programmatic API for agent execution. For chat UI,
34
+ * prefer `chat.sendMessage` which handles thread management automatically.
35
+ */
22
36
  export const executeAgent = action({
23
37
  args: {
24
38
  agentId: v.string(),
@@ -30,7 +44,7 @@ export const executeAgent = action({
30
44
  handler: async (ctx, args): Promise<ExecuteAgentResult> => {
31
45
  // Get agent configuration from database
32
46
  const agent = await ctx.runQuery(api.agents.get, { id: args.agentId });
33
-
47
+
34
48
  if (!agent) {
35
49
  throw new Error(`Agent ${args.agentId} not found`);
36
50
  }
@@ -58,50 +72,34 @@ export const executeAgent = action({
58
72
  threadId,
59
73
  agentId: args.agentId,
60
74
  userId: args.userId,
61
- channel: "dashboard",
75
+ channel: "api",
62
76
  });
63
77
 
64
78
  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
- });
79
+ // Resolve the model
80
+ const provider = agent.provider || "openrouter";
81
+ const modelId = agent.model || "openai/gpt-4o-mini";
82
+ const modelKey = `${provider}/${modelId}`;
85
83
 
86
84
  // 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,
85
+ const messages = await ctx.runQuery(api.messages.list, { threadId });
86
+ const conversationMessages = (messages as Array<{ role: string; content: string }>)
87
+ .slice(-20)
88
+ .map((m) => ({
89
+ role: m.role as "user" | "assistant" | "system",
90
+ content: m.content,
91
+ }));
92
+
93
+ // Execute via Mastra Agent
94
+ const mastraAgent = new Agent({
95
+ name: "agentforge-executor",
96
+ instructions: agent.instructions || "You are a helpful AI assistant.",
97
+ model: modelKey,
99
98
  });
100
99
 
101
- // Extract response content
102
- const responseContent: string = typeof result === "string"
103
- ? result
104
- : result.text || result.content || JSON.stringify(result);
100
+ const result = await mastraAgent.generate(conversationMessages);
101
+
102
+ const responseContent = result.text;
105
103
 
106
104
  // Add assistant message to thread
107
105
  await ctx.runMutation(api.messages.add, {
@@ -116,17 +114,27 @@ export const executeAgent = action({
116
114
  status: "completed",
117
115
  });
118
116
 
119
- // Record usage (if available in result)
120
- if (result.usage) {
117
+ // Build usage data
118
+ const usage = result.usage
119
+ ? {
120
+ promptTokens: result.usage.promptTokens || 0,
121
+ completionTokens: result.usage.completionTokens || 0,
122
+ totalTokens:
123
+ (result.usage.promptTokens || 0) +
124
+ (result.usage.completionTokens || 0),
125
+ }
126
+ : undefined;
127
+
128
+ // Record usage
129
+ if (usage) {
121
130
  await ctx.runMutation(api.usage.record, {
122
131
  agentId: args.agentId,
123
132
  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,
133
+ provider: agent.provider || "openrouter",
134
+ model: agent.model || "unknown",
135
+ promptTokens: usage.promptTokens,
136
+ completionTokens: usage.completionTokens,
137
+ totalTokens: usage.totalTokens,
130
138
  userId: args.userId,
131
139
  });
132
140
  }
@@ -136,10 +144,11 @@ export const executeAgent = action({
136
144
  threadId: threadId as string,
137
145
  sessionId,
138
146
  response: responseContent,
139
- usage: result.usage,
147
+ usage,
140
148
  };
141
149
  } catch (error: unknown) {
142
- const errorMessage = error instanceof Error ? error.message : String(error);
150
+ const errorMessage =
151
+ error instanceof Error ? error.message : String(error);
143
152
 
144
153
  // Update session status to error
145
154
  await ctx.runMutation(api.sessions.updateStatus, {
@@ -147,19 +156,37 @@ export const executeAgent = action({
147
156
  status: "error",
148
157
  });
149
158
 
150
- // Add error message
159
+ // Add error message to thread
151
160
  await ctx.runMutation(api.messages.add, {
152
161
  threadId,
153
162
  role: "assistant",
154
163
  content: `Error: ${errorMessage}`,
155
164
  });
156
165
 
166
+ // Log the error
167
+ await ctx.runMutation(api.logs.add, {
168
+ level: "error",
169
+ source: "mastraIntegration",
170
+ message: `Agent execution failed: ${errorMessage}`,
171
+ metadata: {
172
+ agentId: args.agentId,
173
+ threadId,
174
+ sessionId,
175
+ },
176
+ userId: args.userId,
177
+ });
178
+
157
179
  throw error;
158
180
  }
159
181
  },
160
182
  });
161
183
 
162
- // Action: Stream agent response
184
+ /**
185
+ * Stream agent response (placeholder — streaming requires SSE/WebSocket).
186
+ *
187
+ * For now, this falls back to non-streaming execution.
188
+ * Full streaming support will be added via Convex HTTP actions + SSE.
189
+ */
163
190
  export const streamAgent = action({
164
191
  args: {
165
192
  agentId: v.string(),
@@ -168,26 +195,31 @@ export const streamAgent = action({
168
195
  userId: v.optional(v.string()),
169
196
  },
170
197
  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
198
+ // Fall back to non-streaming execution
199
+ const result = await ctx.runAction(api.mastraIntegration.executeAgent, {
200
+ agentId: args.agentId,
201
+ prompt: args.prompt,
202
+ threadId: args.threadId,
203
+ userId: args.userId,
204
+ });
205
+
174
206
  return {
175
- success: true,
176
- message: "Streaming support coming soon",
207
+ success: result.success,
208
+ message: result.response,
177
209
  };
178
210
  },
179
211
  });
180
212
 
181
- // Action: Execute workflow with multiple agents
213
+ /**
214
+ * Execute workflow with multiple agents (placeholder).
215
+ */
182
216
  export const executeWorkflow = action({
183
217
  args: {
184
218
  workflowId: v.string(),
185
219
  input: v.any(),
186
220
  userId: v.optional(v.string()),
187
221
  },
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
222
+ handler: async (_ctx, _args): Promise<{ success: boolean; message: string }> => {
191
223
  return {
192
224
  success: true,
193
225
  message: "Workflow execution coming soon",