@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-ai/cli",
3
- "version": "0.5.4",
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
- * Example AgentForge Configuration File
3
- *
4
- * This file defines your agents and their configurations.
5
- * The CLI uses this to deploy to AgentForge Cloud.
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: 'anthropic/claude-3-opus',
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
- // Action: Run agent with Mastra (to be implemented with Mastra integration)
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
- // Add user message
180
- await ctx.runMutation(api.messages.add, {
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: responseMessage,
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
+ });