@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.
- package/LICENSE +197 -0
- package/dist/default/agentforge.config.ts +126 -6
- package/dist/default/convex/agents.ts +15 -21
- package/dist/default/convex/chat.ts +302 -0
- package/dist/default/convex/mastraIntegration.ts +101 -69
- package/dist/default/dashboard/app/routes/chat.tsx +462 -167
- package/dist/default/skills/browser-automation/SKILL.md +137 -0
- package/dist/default/skills/browser-automation/config.json +11 -0
- package/dist/default/skills/browser-automation/index.ts +93 -0
- package/dist/default/skills/skill-creator/SKILL.md +69 -230
- package/dist/index.js +2455 -290
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/templates/default/agentforge.config.ts +126 -6
- package/templates/default/convex/agents.ts +15 -21
- package/templates/default/convex/chat.ts +302 -0
- package/templates/default/convex/mastraIntegration.ts +101 -69
- package/templates/default/dashboard/app/routes/chat.tsx +462 -167
- package/templates/default/skills/browser-automation/SKILL.md +137 -0
- package/templates/default/skills/browser-automation/config.json +11 -0
- package/templates/default/skills/browser-automation/index.ts +93 -0
- package/templates/default/skills/skill-creator/SKILL.md +69 -230
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI tool for creating, running, and managing AgentForge projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,20 +12,12 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsup",
|
|
17
|
-
"test": "vitest run",
|
|
18
|
-
"test:coverage": "vitest run --coverage",
|
|
19
|
-
"test:watch": "vitest",
|
|
20
|
-
"typecheck": "tsc --noEmit",
|
|
21
|
-
"lint": "eslint src/",
|
|
22
|
-
"clean": "rm -rf dist"
|
|
23
|
-
},
|
|
24
15
|
"dependencies": {
|
|
16
|
+
"chalk": "^5.3.0",
|
|
25
17
|
"commander": "^12.0.0",
|
|
26
18
|
"convex": "^1.17.0",
|
|
27
|
-
"chalk": "^5.3.0",
|
|
28
19
|
"fs-extra": "^11.2.0",
|
|
20
|
+
"gray-matter": "^4.0.3",
|
|
29
21
|
"ora": "^8.0.0",
|
|
30
22
|
"prompts": "^2.4.2"
|
|
31
23
|
},
|
|
@@ -48,5 +40,14 @@
|
|
|
48
40
|
"type": "git",
|
|
49
41
|
"url": "https://github.com/Agentic-Engineering-Agency/agentforge.git",
|
|
50
42
|
"directory": "packages/cli"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:coverage": "vitest run --coverage",
|
|
48
|
+
"test:watch": "vitest",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"lint": "eslint src/",
|
|
51
|
+
"clean": "rm -rf dist"
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
+
}
|
|
@@ -1,23 +1,117 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This file defines your agents and
|
|
5
|
-
* The CLI uses this to
|
|
2
|
+
* AgentForge Configuration File
|
|
3
|
+
*
|
|
4
|
+
* This file defines your agents, workspace, skills, and model failover
|
|
5
|
+
* configuration. The CLI uses this to manage your AgentForge project.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
name: 'my-agent-project',
|
|
10
10
|
version: '1.0.0',
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Workspace configuration — Mastra Workspace integration.
|
|
14
|
+
*
|
|
15
|
+
* Skills are auto-discovered from the directories listed in `skills`.
|
|
16
|
+
* Each skill directory should contain a SKILL.md file following the
|
|
17
|
+
* Agent Skills Specification.
|
|
18
|
+
*
|
|
19
|
+
* @see https://mastra.ai/docs/workspace/skills
|
|
20
|
+
*/
|
|
21
|
+
workspace: {
|
|
22
|
+
/** Base path for the workspace filesystem. */
|
|
23
|
+
basePath: './workspace',
|
|
24
|
+
/** Directories containing agent skills (relative to basePath). */
|
|
25
|
+
skills: ['/skills'],
|
|
26
|
+
/** Enable BM25 keyword search for skill discovery. */
|
|
27
|
+
search: true,
|
|
28
|
+
/** Paths to auto-index for search. */
|
|
29
|
+
autoIndexPaths: ['/skills'],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Model Failover Configuration
|
|
34
|
+
*
|
|
35
|
+
* Defines the global default failover chain and retry policy.
|
|
36
|
+
* Individual agents can override these settings.
|
|
37
|
+
*
|
|
38
|
+
* Environment variables required:
|
|
39
|
+
* OPENROUTER_API_KEY — OpenRouter API key (routes to all providers)
|
|
40
|
+
* OPENAI_API_KEY — OpenAI direct API key
|
|
41
|
+
* ANTHROPIC_API_KEY — Anthropic direct API key
|
|
42
|
+
* GEMINI_API_KEY — Google Gemini API key
|
|
43
|
+
* VENICE_API_KEY — Venice AI API key (optional)
|
|
44
|
+
*/
|
|
45
|
+
failover: {
|
|
46
|
+
/**
|
|
47
|
+
* Global default failover chain.
|
|
48
|
+
* Used when an agent does not specify its own `failoverModels`.
|
|
49
|
+
* Models are tried in order: primary → fallback1 → fallback2 → ...
|
|
50
|
+
*/
|
|
51
|
+
defaultChain: [
|
|
52
|
+
{ provider: 'openrouter', model: 'openai/gpt-4o-mini' },
|
|
53
|
+
{ provider: 'openai', model: 'gpt-4o-mini' },
|
|
54
|
+
{ provider: 'anthropic', model: 'claude-3-5-haiku-20241022' },
|
|
55
|
+
{ provider: 'google', model: 'gemini-2.0-flash' },
|
|
56
|
+
],
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Retry policy for each model in the chain.
|
|
60
|
+
*/
|
|
61
|
+
retryPolicy: {
|
|
62
|
+
/** Max retries per model before failing over to next. */
|
|
63
|
+
maxRetries: 2,
|
|
64
|
+
/** Initial backoff delay in ms. */
|
|
65
|
+
backoffMs: 1000,
|
|
66
|
+
/** Backoff multiplier for exponential backoff. */
|
|
67
|
+
backoffMultiplier: 2,
|
|
68
|
+
/** Maximum backoff delay in ms. */
|
|
69
|
+
maxBackoffMs: 30000,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Circuit breaker configuration.
|
|
74
|
+
* Opens the circuit (skips the provider) after repeated failures.
|
|
75
|
+
*/
|
|
76
|
+
circuitBreaker: {
|
|
77
|
+
/** Consecutive failures before opening the circuit. */
|
|
78
|
+
failureThreshold: 5,
|
|
79
|
+
/** Time in ms before attempting to close the circuit. */
|
|
80
|
+
resetTimeoutMs: 60000,
|
|
81
|
+
/** Successes in half-open state before fully closing. */
|
|
82
|
+
successThreshold: 2,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/** Global timeout per LLM request in ms. */
|
|
86
|
+
timeoutMs: 30000,
|
|
87
|
+
|
|
88
|
+
/** Enable cost tracking per request. */
|
|
89
|
+
trackCost: true,
|
|
90
|
+
|
|
91
|
+
/** Enable latency monitoring. */
|
|
92
|
+
trackLatency: true,
|
|
93
|
+
},
|
|
94
|
+
|
|
12
95
|
agents: [
|
|
13
96
|
{
|
|
14
97
|
id: 'support-agent',
|
|
15
98
|
name: 'Customer Support Agent',
|
|
16
99
|
model: 'gpt-4o',
|
|
100
|
+
provider: 'openrouter',
|
|
17
101
|
instructions: `You are a helpful customer support agent.
|
|
18
102
|
|
|
19
103
|
Be polite, professional, and try to resolve customer issues quickly.
|
|
20
104
|
If you don't know the answer, escalate to a human agent.`,
|
|
105
|
+
/**
|
|
106
|
+
* Agent-level failover models.
|
|
107
|
+
* These override the global `failover.defaultChain` for this agent.
|
|
108
|
+
* The primary model (provider + model above) is always tried first.
|
|
109
|
+
*/
|
|
110
|
+
failoverModels: [
|
|
111
|
+
{ provider: 'openai', model: 'gpt-4o' },
|
|
112
|
+
{ provider: 'anthropic', model: 'claude-sonnet-4-20250514' },
|
|
113
|
+
{ provider: 'google', model: 'gemini-2.5-flash' },
|
|
114
|
+
],
|
|
21
115
|
tools: [
|
|
22
116
|
{
|
|
23
117
|
name: 'searchKnowledgeBase',
|
|
@@ -48,11 +142,19 @@ If you don't know the answer, escalate to a human agent.`,
|
|
|
48
142
|
{
|
|
49
143
|
id: 'code-assistant',
|
|
50
144
|
name: 'Code Assistant',
|
|
51
|
-
model: '
|
|
145
|
+
model: 'claude-sonnet-4-20250514',
|
|
146
|
+
provider: 'anthropic',
|
|
52
147
|
instructions: `You are an expert programming assistant.
|
|
53
148
|
|
|
54
149
|
Help users write clean, efficient, and well-documented code.
|
|
55
150
|
Explain your reasoning and provide examples when helpful.`,
|
|
151
|
+
/**
|
|
152
|
+
* Agent-level failover: Anthropic → OpenAI → Google
|
|
153
|
+
*/
|
|
154
|
+
failoverModels: [
|
|
155
|
+
{ provider: 'openai', model: 'gpt-4.1' },
|
|
156
|
+
{ provider: 'google', model: 'gemini-2.5-pro' },
|
|
157
|
+
],
|
|
56
158
|
tools: [
|
|
57
159
|
{
|
|
58
160
|
name: 'runCode',
|
|
@@ -70,6 +172,24 @@ Explain your reasoning and provide examples when helpful.`,
|
|
|
70
172
|
},
|
|
71
173
|
],
|
|
72
174
|
|
|
175
|
+
// Sandbox configuration for agent tool execution isolation
|
|
176
|
+
sandbox: {
|
|
177
|
+
// Provider: 'local' (default), 'docker', 'e2b', or 'none'
|
|
178
|
+
provider: 'local',
|
|
179
|
+
// Docker-specific options (only used when provider is 'docker')
|
|
180
|
+
docker: {
|
|
181
|
+
image: 'node:22-slim',
|
|
182
|
+
resourceLimits: {
|
|
183
|
+
memoryMb: 512,
|
|
184
|
+
cpuShares: 512,
|
|
185
|
+
networkDisabled: false,
|
|
186
|
+
pidsLimit: 256,
|
|
187
|
+
},
|
|
188
|
+
// Timeout in seconds before auto-killing the container
|
|
189
|
+
timeout: 300,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
73
193
|
// Optional: Environment variables available to all agents
|
|
74
194
|
env: {
|
|
75
195
|
SUPPORT_EMAIL: 'support@example.com',
|
|
@@ -38,13 +38,13 @@ export const listActive = query({
|
|
|
38
38
|
const activeQuery = ctx.db
|
|
39
39
|
.query("agents")
|
|
40
40
|
.withIndex("byIsActive", (q) => q.eq("isActive", true));
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
const agents = await activeQuery.collect();
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
if (args.userId) {
|
|
45
45
|
return agents.filter((agent) => agent.userId === args.userId);
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
return agents;
|
|
49
49
|
},
|
|
50
50
|
});
|
|
@@ -151,7 +151,12 @@ export const toggleActive = mutation({
|
|
|
151
151
|
},
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Run an agent with a prompt.
|
|
156
|
+
*
|
|
157
|
+
* Delegates to `chat.sendMessage` for the actual LLM execution.
|
|
158
|
+
* This action handles thread creation if no threadId is provided.
|
|
159
|
+
*/
|
|
155
160
|
export const run = action({
|
|
156
161
|
args: {
|
|
157
162
|
agentId: v.string(),
|
|
@@ -162,7 +167,7 @@ export const run = action({
|
|
|
162
167
|
handler: async (ctx, args): Promise<{ threadId: string; message: string; agentId: string }> => {
|
|
163
168
|
// Get agent configuration
|
|
164
169
|
const agent = await ctx.runQuery(api.agents.get, { id: args.agentId });
|
|
165
|
-
|
|
170
|
+
|
|
166
171
|
if (!agent) {
|
|
167
172
|
throw new Error(`Agent with id ${args.agentId} not found`);
|
|
168
173
|
}
|
|
@@ -176,28 +181,17 @@ export const run = action({
|
|
|
176
181
|
});
|
|
177
182
|
}
|
|
178
183
|
|
|
179
|
-
//
|
|
180
|
-
await ctx.
|
|
184
|
+
// Delegate to the chat action for actual LLM execution
|
|
185
|
+
const result = await ctx.runAction(api.chat.sendMessage, {
|
|
186
|
+
agentId: args.agentId,
|
|
181
187
|
threadId,
|
|
182
|
-
role: "user",
|
|
183
188
|
content: args.prompt,
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// TODO: Integrate with Mastra to run the agent
|
|
187
|
-
// This will be implemented in the Mastra integration phase
|
|
188
|
-
// For now, return a placeholder response
|
|
189
|
-
const responseMessage = "Agent execution will be implemented with Mastra integration";
|
|
190
|
-
|
|
191
|
-
// Add assistant message placeholder
|
|
192
|
-
await ctx.runMutation(api.messages.add, {
|
|
193
|
-
threadId,
|
|
194
|
-
role: "assistant",
|
|
195
|
-
content: responseMessage,
|
|
189
|
+
userId: args.userId,
|
|
196
190
|
});
|
|
197
191
|
|
|
198
192
|
return {
|
|
199
193
|
threadId: threadId as string,
|
|
200
|
-
message:
|
|
194
|
+
message: result.response,
|
|
201
195
|
agentId: args.agentId,
|
|
202
196
|
};
|
|
203
197
|
},
|
|
@@ -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
|
+
});
|