@agentforge-ai/cli 0.5.5 → 0.7.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.
- package/dist/default/convex/chat.ts +15 -44
- package/dist/default/convex/mastraIntegration.ts +11 -64
- package/dist/default/dashboard/app/routes/skills.tsx +73 -11
- package/package.json +3 -2
- package/templates/default/convex/chat.ts +15 -44
- package/templates/default/convex/mastraIntegration.ts +11 -64
- package/templates/default/dashboard/app/routes/skills.tsx +73 -11
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module provides the core chat execution pipeline:
|
|
5
5
|
* 1. User sends a message → stored via mutation
|
|
6
|
-
* 2. Convex action triggers LLM generation via
|
|
6
|
+
* 2. Convex action triggers LLM generation via Mastra Agent
|
|
7
7
|
* 3. Assistant response stored back in Convex
|
|
8
8
|
* 4. Real-time subscription updates the UI automatically
|
|
9
9
|
*
|
|
10
|
-
* Uses
|
|
11
|
-
*
|
|
12
|
-
* Node.js actions for maximum reliability.
|
|
10
|
+
* Uses Mastra-native model routing via Agent.generate() with "provider/model-name" format.
|
|
11
|
+
* Mastra auto-reads provider API keys from environment variables.
|
|
13
12
|
*/
|
|
14
13
|
import { action, mutation, query } from "./_generated/server";
|
|
15
14
|
import { v } from "convex/values";
|
|
16
15
|
import { api } from "./_generated/api";
|
|
16
|
+
import { Agent } from "@mastra/core/agent";
|
|
17
17
|
|
|
18
18
|
// ============================================================
|
|
19
19
|
// Queries
|
|
@@ -140,7 +140,7 @@ export const addAssistantMessage = mutation({
|
|
|
140
140
|
* 1. Looks up the agent config from the database
|
|
141
141
|
* 2. Stores the user message
|
|
142
142
|
* 3. Builds conversation history from the thread
|
|
143
|
-
* 4. Calls
|
|
143
|
+
* 4. Calls the provider via Mastra Agent.generate()
|
|
144
144
|
* 5. Stores the assistant response
|
|
145
145
|
* 6. Records usage metrics
|
|
146
146
|
*
|
|
@@ -180,54 +180,25 @@ export const sendMessage = action({
|
|
|
180
180
|
content: msg.content,
|
|
181
181
|
}));
|
|
182
182
|
|
|
183
|
-
// 4. Call the LLM via
|
|
183
|
+
// 4. Call the LLM via Mastra Agent
|
|
184
184
|
let responseText: string;
|
|
185
185
|
let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
|
|
186
186
|
|
|
187
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
188
|
// Resolve the model provider and ID
|
|
193
189
|
const provider = agent.provider || "openrouter";
|
|
194
190
|
const modelId = agent.model || "openai/gpt-4o-mini";
|
|
191
|
+
const modelKey = `${provider}/${modelId}`;
|
|
195
192
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 }),
|
|
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,
|
|
229
198
|
});
|
|
230
199
|
|
|
200
|
+
const result = await mastraAgent.generate(conversationMessages);
|
|
201
|
+
|
|
231
202
|
responseText = result.text;
|
|
232
203
|
|
|
233
204
|
// Extract usage if available
|
|
@@ -240,7 +211,7 @@ export const sendMessage = action({
|
|
|
240
211
|
}
|
|
241
212
|
} catch (error: unknown) {
|
|
242
213
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
243
|
-
console.error("[chat.sendMessage]
|
|
214
|
+
console.error("[chat.sendMessage] Mastra error:", errorMessage);
|
|
244
215
|
|
|
245
216
|
// Store error as assistant message so user sees feedback
|
|
246
217
|
responseText = `I encountered an error while processing your request: ${errorMessage}`;
|
|
@@ -2,20 +2,17 @@
|
|
|
2
2
|
* Mastra Integration Actions for Convex
|
|
3
3
|
*
|
|
4
4
|
* These actions run in the Convex Node.js runtime and execute LLM calls
|
|
5
|
-
* using
|
|
5
|
+
* using Mastra-native model routing with OpenRouter as the default provider.
|
|
6
6
|
*
|
|
7
7
|
* Architecture:
|
|
8
8
|
* - For chat: use `chat.sendMessage` (preferred entry point)
|
|
9
9
|
* - For programmatic agent execution: use `mastraIntegration.executeAgent`
|
|
10
|
-
* - Model resolution: uses
|
|
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
|
+
* - Model resolution: uses Mastra Agent with "provider/model-name" format
|
|
15
11
|
*/
|
|
16
12
|
import { action } from "./_generated/server";
|
|
17
13
|
import { v } from "convex/values";
|
|
18
14
|
import { api } from "./_generated/api";
|
|
15
|
+
import { Agent } from "@mastra/core/agent";
|
|
19
16
|
|
|
20
17
|
// Return type for executeAgent
|
|
21
18
|
type ExecuteAgentResult = {
|
|
@@ -30,54 +27,6 @@ type ExecuteAgentResult = {
|
|
|
30
27
|
};
|
|
31
28
|
};
|
|
32
29
|
|
|
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
30
|
/**
|
|
82
31
|
* Execute an agent with a prompt and return the response.
|
|
83
32
|
*
|
|
@@ -127,12 +76,10 @@ export const executeAgent = action({
|
|
|
127
76
|
});
|
|
128
77
|
|
|
129
78
|
try {
|
|
130
|
-
const { generateText } = await import("ai");
|
|
131
|
-
|
|
132
79
|
// Resolve the model
|
|
133
80
|
const provider = agent.provider || "openrouter";
|
|
134
81
|
const modelId = agent.model || "openai/gpt-4o-mini";
|
|
135
|
-
const
|
|
82
|
+
const modelKey = `${provider}/${modelId}`;
|
|
136
83
|
|
|
137
84
|
// Get conversation history for context
|
|
138
85
|
const messages = await ctx.runQuery(api.messages.list, { threadId });
|
|
@@ -143,15 +90,15 @@ export const executeAgent = action({
|
|
|
143
90
|
content: m.content,
|
|
144
91
|
}));
|
|
145
92
|
|
|
146
|
-
// Execute
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
...(agent.temperature != null && { temperature: agent.temperature }),
|
|
152
|
-
...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
|
|
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,
|
|
153
98
|
});
|
|
154
99
|
|
|
100
|
+
const result = await mastraAgent.generate(conversationMessages);
|
|
101
|
+
|
|
155
102
|
const responseContent = result.text;
|
|
156
103
|
|
|
157
104
|
// Add assistant message to thread
|
|
@@ -38,7 +38,7 @@ export const webSearch = createTool({
|
|
|
38
38
|
{
|
|
39
39
|
name: 'code-executor',
|
|
40
40
|
displayName: 'Code Executor',
|
|
41
|
-
description: '
|
|
41
|
+
description: 'Execute JavaScript/TypeScript code snippets via the AgentForge sandbox API and return the output.',
|
|
42
42
|
category: 'Tools',
|
|
43
43
|
version: '1.0.0',
|
|
44
44
|
author: 'AgentForge',
|
|
@@ -48,18 +48,25 @@ import { z } from 'zod';
|
|
|
48
48
|
|
|
49
49
|
export const codeExecutor = createTool({
|
|
50
50
|
id: 'code-executor',
|
|
51
|
-
description: 'Execute JavaScript code and return the result',
|
|
51
|
+
description: 'Execute JavaScript code via the sandbox API and return the result',
|
|
52
52
|
inputSchema: z.object({
|
|
53
53
|
code: z.string().describe('JavaScript code to execute'),
|
|
54
|
+
language: z.enum(['javascript', 'typescript']).default('javascript'),
|
|
54
55
|
}),
|
|
55
56
|
execute: async ({ context }) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// Code is executed server-side via the AgentForge sandbox API,
|
|
58
|
+
// not via eval or Function constructor, to prevent arbitrary code execution risks.
|
|
59
|
+
const response = await fetch('/api/sandbox/execute', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ code: context.code, language: context.language }),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
66
|
+
return { success: false, error: err.error ?? 'Sandbox execution failed' };
|
|
62
67
|
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
return { success: true, output: String(data.output ?? '') };
|
|
63
70
|
},
|
|
64
71
|
});`,
|
|
65
72
|
},
|
|
@@ -74,16 +81,71 @@ export const codeExecutor = createTool({
|
|
|
74
81
|
code: `import { createTool } from '@mastra/core';
|
|
75
82
|
import { z } from 'zod';
|
|
76
83
|
|
|
84
|
+
// Safe arithmetic evaluator — no eval or Function constructor used.
|
|
85
|
+
function evaluateExpression(expr: string): number {
|
|
86
|
+
// Allow only digits, operators, parentheses, dots, and whitespace.
|
|
87
|
+
if (!/^[0-9+\\-*/().%\\s]+$/.test(expr)) {
|
|
88
|
+
throw new Error('Expression contains invalid characters');
|
|
89
|
+
}
|
|
90
|
+
// Recursive descent parser for: expr = term (('+' | '-') term)*
|
|
91
|
+
let pos = 0;
|
|
92
|
+
const peek = () => expr[pos] ?? '';
|
|
93
|
+
const consume = () => expr[pos++];
|
|
94
|
+
const skipWs = () => { while (peek() === ' ') consume(); };
|
|
95
|
+
|
|
96
|
+
function parseNumber(): number {
|
|
97
|
+
skipWs();
|
|
98
|
+
let num = '';
|
|
99
|
+
if (peek() === '(') { consume(); const v = parseExpr(); skipWs(); consume(); return v; }
|
|
100
|
+
while (/[0-9.]/.test(peek())) num += consume();
|
|
101
|
+
if (num === '') throw new Error('Expected number');
|
|
102
|
+
return parseFloat(num);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseFactor(): number {
|
|
106
|
+
skipWs();
|
|
107
|
+
if (peek() === '-') { consume(); return -parseFactor(); }
|
|
108
|
+
return parseNumber();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseTerm(): number {
|
|
112
|
+
let left = parseFactor();
|
|
113
|
+
skipWs();
|
|
114
|
+
while (peek() === '*' || peek() === '/') {
|
|
115
|
+
const op = consume(); skipWs();
|
|
116
|
+
const right = parseFactor();
|
|
117
|
+
left = op === '*' ? left * right : left / right;
|
|
118
|
+
skipWs();
|
|
119
|
+
}
|
|
120
|
+
return left;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseExpr(): number {
|
|
124
|
+
let left = parseTerm();
|
|
125
|
+
skipWs();
|
|
126
|
+
while (peek() === '+' || peek() === '-') {
|
|
127
|
+
const op = consume(); skipWs();
|
|
128
|
+
const right = parseTerm();
|
|
129
|
+
left = op === '+' ? left + right : left - right;
|
|
130
|
+
skipWs();
|
|
131
|
+
}
|
|
132
|
+
return left;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = parseExpr();
|
|
136
|
+
if (pos !== expr.length) throw new Error('Unexpected token at position ' + pos);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
77
140
|
export const calculator = createTool({
|
|
78
141
|
id: 'calculator',
|
|
79
|
-
description: 'Evaluate a mathematical expression',
|
|
142
|
+
description: 'Evaluate a mathematical expression safely',
|
|
80
143
|
inputSchema: z.object({
|
|
81
144
|
expression: z.string().describe('Math expression to evaluate, e.g. "2 + 2 * 3"'),
|
|
82
145
|
}),
|
|
83
146
|
execute: async ({ context }) => {
|
|
84
147
|
try {
|
|
85
|
-
const
|
|
86
|
-
const result = Function('"use strict"; return (' + sanitized + ')')();
|
|
148
|
+
const result = evaluateExpression(context.expression.trim());
|
|
87
149
|
return { result: Number(result), expression: context.expression };
|
|
88
150
|
} catch (error: any) {
|
|
89
151
|
return { error: 'Invalid expression: ' + error.message };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI tool for creating, running, and managing AgentForge projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"fs-extra": "^11.2.0",
|
|
20
20
|
"gray-matter": "^4.0.3",
|
|
21
21
|
"ora": "^8.0.0",
|
|
22
|
-
"prompts": "^2.4.2"
|
|
22
|
+
"prompts": "^2.4.2",
|
|
23
|
+
"@agentforge-ai/core": "0.7.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This module provides the core chat execution pipeline:
|
|
5
5
|
* 1. User sends a message → stored via mutation
|
|
6
|
-
* 2. Convex action triggers LLM generation via
|
|
6
|
+
* 2. Convex action triggers LLM generation via Mastra Agent
|
|
7
7
|
* 3. Assistant response stored back in Convex
|
|
8
8
|
* 4. Real-time subscription updates the UI automatically
|
|
9
9
|
*
|
|
10
|
-
* Uses
|
|
11
|
-
*
|
|
12
|
-
* Node.js actions for maximum reliability.
|
|
10
|
+
* Uses Mastra-native model routing via Agent.generate() with "provider/model-name" format.
|
|
11
|
+
* Mastra auto-reads provider API keys from environment variables.
|
|
13
12
|
*/
|
|
14
13
|
import { action, mutation, query } from "./_generated/server";
|
|
15
14
|
import { v } from "convex/values";
|
|
16
15
|
import { api } from "./_generated/api";
|
|
16
|
+
import { Agent } from "@mastra/core/agent";
|
|
17
17
|
|
|
18
18
|
// ============================================================
|
|
19
19
|
// Queries
|
|
@@ -140,7 +140,7 @@ export const addAssistantMessage = mutation({
|
|
|
140
140
|
* 1. Looks up the agent config from the database
|
|
141
141
|
* 2. Stores the user message
|
|
142
142
|
* 3. Builds conversation history from the thread
|
|
143
|
-
* 4. Calls
|
|
143
|
+
* 4. Calls the provider via Mastra Agent.generate()
|
|
144
144
|
* 5. Stores the assistant response
|
|
145
145
|
* 6. Records usage metrics
|
|
146
146
|
*
|
|
@@ -180,54 +180,25 @@ export const sendMessage = action({
|
|
|
180
180
|
content: msg.content,
|
|
181
181
|
}));
|
|
182
182
|
|
|
183
|
-
// 4. Call the LLM via
|
|
183
|
+
// 4. Call the LLM via Mastra Agent
|
|
184
184
|
let responseText: string;
|
|
185
185
|
let usageData: { promptTokens: number; completionTokens: number; totalTokens: number } | null = null;
|
|
186
186
|
|
|
187
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
188
|
// Resolve the model provider and ID
|
|
193
189
|
const provider = agent.provider || "openrouter";
|
|
194
190
|
const modelId = agent.model || "openai/gpt-4o-mini";
|
|
191
|
+
const modelKey = `${provider}/${modelId}`;
|
|
195
192
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 }),
|
|
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,
|
|
229
198
|
});
|
|
230
199
|
|
|
200
|
+
const result = await mastraAgent.generate(conversationMessages);
|
|
201
|
+
|
|
231
202
|
responseText = result.text;
|
|
232
203
|
|
|
233
204
|
// Extract usage if available
|
|
@@ -240,7 +211,7 @@ export const sendMessage = action({
|
|
|
240
211
|
}
|
|
241
212
|
} catch (error: unknown) {
|
|
242
213
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
243
|
-
console.error("[chat.sendMessage]
|
|
214
|
+
console.error("[chat.sendMessage] Mastra error:", errorMessage);
|
|
244
215
|
|
|
245
216
|
// Store error as assistant message so user sees feedback
|
|
246
217
|
responseText = `I encountered an error while processing your request: ${errorMessage}`;
|
|
@@ -2,20 +2,17 @@
|
|
|
2
2
|
* Mastra Integration Actions for Convex
|
|
3
3
|
*
|
|
4
4
|
* These actions run in the Convex Node.js runtime and execute LLM calls
|
|
5
|
-
* using
|
|
5
|
+
* using Mastra-native model routing with OpenRouter as the default provider.
|
|
6
6
|
*
|
|
7
7
|
* Architecture:
|
|
8
8
|
* - For chat: use `chat.sendMessage` (preferred entry point)
|
|
9
9
|
* - For programmatic agent execution: use `mastraIntegration.executeAgent`
|
|
10
|
-
* - Model resolution: uses
|
|
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
|
+
* - Model resolution: uses Mastra Agent with "provider/model-name" format
|
|
15
11
|
*/
|
|
16
12
|
import { action } from "./_generated/server";
|
|
17
13
|
import { v } from "convex/values";
|
|
18
14
|
import { api } from "./_generated/api";
|
|
15
|
+
import { Agent } from "@mastra/core/agent";
|
|
19
16
|
|
|
20
17
|
// Return type for executeAgent
|
|
21
18
|
type ExecuteAgentResult = {
|
|
@@ -30,54 +27,6 @@ type ExecuteAgentResult = {
|
|
|
30
27
|
};
|
|
31
28
|
};
|
|
32
29
|
|
|
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
30
|
/**
|
|
82
31
|
* Execute an agent with a prompt and return the response.
|
|
83
32
|
*
|
|
@@ -127,12 +76,10 @@ export const executeAgent = action({
|
|
|
127
76
|
});
|
|
128
77
|
|
|
129
78
|
try {
|
|
130
|
-
const { generateText } = await import("ai");
|
|
131
|
-
|
|
132
79
|
// Resolve the model
|
|
133
80
|
const provider = agent.provider || "openrouter";
|
|
134
81
|
const modelId = agent.model || "openai/gpt-4o-mini";
|
|
135
|
-
const
|
|
82
|
+
const modelKey = `${provider}/${modelId}`;
|
|
136
83
|
|
|
137
84
|
// Get conversation history for context
|
|
138
85
|
const messages = await ctx.runQuery(api.messages.list, { threadId });
|
|
@@ -143,15 +90,15 @@ export const executeAgent = action({
|
|
|
143
90
|
content: m.content,
|
|
144
91
|
}));
|
|
145
92
|
|
|
146
|
-
// Execute
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
...(agent.temperature != null && { temperature: agent.temperature }),
|
|
152
|
-
...(agent.maxTokens != null && { maxTokens: agent.maxTokens }),
|
|
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,
|
|
153
98
|
});
|
|
154
99
|
|
|
100
|
+
const result = await mastraAgent.generate(conversationMessages);
|
|
101
|
+
|
|
155
102
|
const responseContent = result.text;
|
|
156
103
|
|
|
157
104
|
// Add assistant message to thread
|
|
@@ -38,7 +38,7 @@ export const webSearch = createTool({
|
|
|
38
38
|
{
|
|
39
39
|
name: 'code-executor',
|
|
40
40
|
displayName: 'Code Executor',
|
|
41
|
-
description: '
|
|
41
|
+
description: 'Execute JavaScript/TypeScript code snippets via the AgentForge sandbox API and return the output.',
|
|
42
42
|
category: 'Tools',
|
|
43
43
|
version: '1.0.0',
|
|
44
44
|
author: 'AgentForge',
|
|
@@ -48,18 +48,25 @@ import { z } from 'zod';
|
|
|
48
48
|
|
|
49
49
|
export const codeExecutor = createTool({
|
|
50
50
|
id: 'code-executor',
|
|
51
|
-
description: 'Execute JavaScript code and return the result',
|
|
51
|
+
description: 'Execute JavaScript code via the sandbox API and return the result',
|
|
52
52
|
inputSchema: z.object({
|
|
53
53
|
code: z.string().describe('JavaScript code to execute'),
|
|
54
|
+
language: z.enum(['javascript', 'typescript']).default('javascript'),
|
|
54
55
|
}),
|
|
55
56
|
execute: async ({ context }) => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// Code is executed server-side via the AgentForge sandbox API,
|
|
58
|
+
// not via eval or Function constructor, to prevent arbitrary code execution risks.
|
|
59
|
+
const response = await fetch('/api/sandbox/execute', {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: { 'Content-Type': 'application/json' },
|
|
62
|
+
body: JSON.stringify({ code: context.code, language: context.language }),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
const err = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
66
|
+
return { success: false, error: err.error ?? 'Sandbox execution failed' };
|
|
62
67
|
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
return { success: true, output: String(data.output ?? '') };
|
|
63
70
|
},
|
|
64
71
|
});`,
|
|
65
72
|
},
|
|
@@ -74,16 +81,71 @@ export const codeExecutor = createTool({
|
|
|
74
81
|
code: `import { createTool } from '@mastra/core';
|
|
75
82
|
import { z } from 'zod';
|
|
76
83
|
|
|
84
|
+
// Safe arithmetic evaluator — no eval or Function constructor used.
|
|
85
|
+
function evaluateExpression(expr: string): number {
|
|
86
|
+
// Allow only digits, operators, parentheses, dots, and whitespace.
|
|
87
|
+
if (!/^[0-9+\\-*/().%\\s]+$/.test(expr)) {
|
|
88
|
+
throw new Error('Expression contains invalid characters');
|
|
89
|
+
}
|
|
90
|
+
// Recursive descent parser for: expr = term (('+' | '-') term)*
|
|
91
|
+
let pos = 0;
|
|
92
|
+
const peek = () => expr[pos] ?? '';
|
|
93
|
+
const consume = () => expr[pos++];
|
|
94
|
+
const skipWs = () => { while (peek() === ' ') consume(); };
|
|
95
|
+
|
|
96
|
+
function parseNumber(): number {
|
|
97
|
+
skipWs();
|
|
98
|
+
let num = '';
|
|
99
|
+
if (peek() === '(') { consume(); const v = parseExpr(); skipWs(); consume(); return v; }
|
|
100
|
+
while (/[0-9.]/.test(peek())) num += consume();
|
|
101
|
+
if (num === '') throw new Error('Expected number');
|
|
102
|
+
return parseFloat(num);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseFactor(): number {
|
|
106
|
+
skipWs();
|
|
107
|
+
if (peek() === '-') { consume(); return -parseFactor(); }
|
|
108
|
+
return parseNumber();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseTerm(): number {
|
|
112
|
+
let left = parseFactor();
|
|
113
|
+
skipWs();
|
|
114
|
+
while (peek() === '*' || peek() === '/') {
|
|
115
|
+
const op = consume(); skipWs();
|
|
116
|
+
const right = parseFactor();
|
|
117
|
+
left = op === '*' ? left * right : left / right;
|
|
118
|
+
skipWs();
|
|
119
|
+
}
|
|
120
|
+
return left;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseExpr(): number {
|
|
124
|
+
let left = parseTerm();
|
|
125
|
+
skipWs();
|
|
126
|
+
while (peek() === '+' || peek() === '-') {
|
|
127
|
+
const op = consume(); skipWs();
|
|
128
|
+
const right = parseTerm();
|
|
129
|
+
left = op === '+' ? left + right : left - right;
|
|
130
|
+
skipWs();
|
|
131
|
+
}
|
|
132
|
+
return left;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = parseExpr();
|
|
136
|
+
if (pos !== expr.length) throw new Error('Unexpected token at position ' + pos);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
77
140
|
export const calculator = createTool({
|
|
78
141
|
id: 'calculator',
|
|
79
|
-
description: 'Evaluate a mathematical expression',
|
|
142
|
+
description: 'Evaluate a mathematical expression safely',
|
|
80
143
|
inputSchema: z.object({
|
|
81
144
|
expression: z.string().describe('Math expression to evaluate, e.g. "2 + 2 * 3"'),
|
|
82
145
|
}),
|
|
83
146
|
execute: async ({ context }) => {
|
|
84
147
|
try {
|
|
85
|
-
const
|
|
86
|
-
const result = Function('"use strict"; return (' + sanitized + ')')();
|
|
148
|
+
const result = evaluateExpression(context.expression.trim());
|
|
87
149
|
return { result: Number(result), expression: context.expression };
|
|
88
150
|
} catch (error: any) {
|
|
89
151
|
return { error: 'Invalid expression: ' + error.message };
|