@nordsym/apiclaw 1.4.0 → 1.4.2
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/PRD-ANALYTICS-AGENTS-TEAMS.md +710 -0
- package/PRD-LOGS-SUBAGENTS-V2.md +267 -0
- package/convex/_generated/api.d.ts +4 -0
- package/convex/agents.ts +188 -0
- package/convex/chains.ts +257 -104
- package/convex/logs.ts +94 -0
- package/convex/schema.ts +38 -0
- package/convex/searchLogs.ts +141 -0
- package/convex/teams.ts +243 -0
- package/dist/index.js +97 -2
- package/dist/index.js.map +1 -1
- package/docs/SUBAGENT-NAMING.md +94 -0
- package/landing/src/app/workspace/chains/page.tsx +3 -3
- package/landing/src/app/workspace/page.tsx +1903 -224
- package/landing/src/lib/stats.json +1 -1
- package/package.json +14 -2
- package/src/index.ts +102 -2
- package/src/chain-types.ts +0 -270
package/convex/chains.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation, query, action, internalMutation, internalAction } from "./_generated/server";
|
|
3
|
-
import { internal
|
|
4
|
-
import { Id
|
|
3
|
+
import { internal } from "./_generated/api";
|
|
4
|
+
import { Id } from "./_generated/dataModel";
|
|
5
5
|
|
|
6
6
|
// ============================================
|
|
7
7
|
// TYPES
|
|
@@ -15,13 +15,11 @@ interface ChainStep {
|
|
|
15
15
|
provider: string;
|
|
16
16
|
action: string;
|
|
17
17
|
params: Record<string, unknown>;
|
|
18
|
-
// Execution modifiers
|
|
19
18
|
onError?: {
|
|
20
19
|
retry?: { attempts: number; backoff?: number[] };
|
|
21
20
|
fallback?: ChainStep;
|
|
22
21
|
abort?: boolean;
|
|
23
22
|
};
|
|
24
|
-
// Parallel execution
|
|
25
23
|
parallel?: ChainStep[];
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -41,13 +39,255 @@ function generateResumeToken(chainId: string, stepIndex: number): string {
|
|
|
41
39
|
return `chain_${chainId.slice(-8)}_step_${stepIndex}_${random}`;
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
// ============================================
|
|
43
|
+
// DASHBOARD QUERIES (for workspace chains page)
|
|
44
|
+
// ============================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get chain executions for a workspace (authenticated via session token)
|
|
48
|
+
*/
|
|
49
|
+
export const getChainExecutions = query({
|
|
50
|
+
args: {
|
|
51
|
+
token: v.string(),
|
|
52
|
+
limit: v.optional(v.number()),
|
|
53
|
+
status: v.optional(v.string()),
|
|
54
|
+
},
|
|
55
|
+
handler: async (ctx, args) => {
|
|
56
|
+
// Validate session
|
|
57
|
+
const session = await ctx.db
|
|
58
|
+
.query("agentSessions")
|
|
59
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
60
|
+
.first();
|
|
61
|
+
|
|
62
|
+
if (!session) {
|
|
63
|
+
return { error: "Invalid session" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Get chains for workspace
|
|
67
|
+
const allChains = await ctx.db
|
|
68
|
+
.query("chains")
|
|
69
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
70
|
+
.order("desc")
|
|
71
|
+
.collect();
|
|
72
|
+
|
|
73
|
+
// Filter by status if provided
|
|
74
|
+
let filteredChains = allChains;
|
|
75
|
+
if (args.status && args.status !== "all") {
|
|
76
|
+
filteredChains = allChains.filter((c) => c.status === args.status);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Apply limit
|
|
80
|
+
const limit = args.limit || 50;
|
|
81
|
+
const chains = filteredChains.slice(0, limit);
|
|
82
|
+
|
|
83
|
+
// For each chain, get step count from chainExecutions
|
|
84
|
+
const chainsWithStepCount = await Promise.all(
|
|
85
|
+
chains.map(async (chain) => {
|
|
86
|
+
const steps = await ctx.db
|
|
87
|
+
.query("chainExecutions")
|
|
88
|
+
.withIndex("by_chainId", (q) => q.eq("chainId", chain._id))
|
|
89
|
+
.collect();
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
_id: chain._id,
|
|
93
|
+
status: chain.status,
|
|
94
|
+
currentStep: chain.currentStep,
|
|
95
|
+
stepsCount: steps.length || chain.steps?.length || 0,
|
|
96
|
+
totalCostCents: chain.totalCostCents || 0,
|
|
97
|
+
totalLatencyMs: chain.totalLatencyMs || 0,
|
|
98
|
+
error: chain.error,
|
|
99
|
+
canResume: chain.canResume,
|
|
100
|
+
resumeToken: chain.resumeToken,
|
|
101
|
+
createdAt: chain.createdAt,
|
|
102
|
+
startedAt: chain.startedAt,
|
|
103
|
+
completedAt: chain.completedAt,
|
|
104
|
+
};
|
|
105
|
+
})
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return chainsWithStepCount;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get full trace for a single chain (authenticated via session token)
|
|
114
|
+
*/
|
|
115
|
+
export const getChainTraceAuth = query({
|
|
116
|
+
args: {
|
|
117
|
+
token: v.string(),
|
|
118
|
+
chainId: v.id("chains"),
|
|
119
|
+
},
|
|
120
|
+
handler: async (ctx, args) => {
|
|
121
|
+
// Validate session
|
|
122
|
+
const session = await ctx.db
|
|
123
|
+
.query("agentSessions")
|
|
124
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
125
|
+
.first();
|
|
126
|
+
|
|
127
|
+
if (!session) {
|
|
128
|
+
return { error: "Invalid session" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Get the chain
|
|
132
|
+
const chain = await ctx.db.get(args.chainId);
|
|
133
|
+
if (!chain || chain.workspaceId !== session.workspaceId) {
|
|
134
|
+
return { error: "Chain not found" };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get all step executions
|
|
138
|
+
const executions = await ctx.db
|
|
139
|
+
.query("chainExecutions")
|
|
140
|
+
.withIndex("by_chainId", (q) => q.eq("chainId", args.chainId))
|
|
141
|
+
.collect();
|
|
142
|
+
|
|
143
|
+
// Sort by stepIndex
|
|
144
|
+
executions.sort((a, b) => a.stepIndex - b.stepIndex);
|
|
145
|
+
|
|
146
|
+
// Calculate total tokens saved (estimate: ~400 tokens per step avoided)
|
|
147
|
+
const completedSteps = executions.filter((e) => e.status === "completed");
|
|
148
|
+
const tokensSaved = completedSteps.length > 1 ? (completedSteps.length - 1) * 400 : 0;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
chain: {
|
|
152
|
+
_id: chain._id,
|
|
153
|
+
status: chain.status,
|
|
154
|
+
currentStep: chain.currentStep,
|
|
155
|
+
steps: chain.steps,
|
|
156
|
+
results: chain.results,
|
|
157
|
+
error: chain.error,
|
|
158
|
+
continueOnError: chain.continueOnError,
|
|
159
|
+
timeout: chain.timeout,
|
|
160
|
+
canResume: chain.canResume,
|
|
161
|
+
resumeToken: chain.resumeToken,
|
|
162
|
+
totalCostCents: chain.totalCostCents || 0,
|
|
163
|
+
totalLatencyMs: chain.totalLatencyMs || 0,
|
|
164
|
+
createdAt: chain.createdAt,
|
|
165
|
+
startedAt: chain.startedAt,
|
|
166
|
+
completedAt: chain.completedAt,
|
|
167
|
+
},
|
|
168
|
+
executions: executions.map((e) => ({
|
|
169
|
+
_id: e._id,
|
|
170
|
+
stepId: e.stepId,
|
|
171
|
+
stepIndex: e.stepIndex,
|
|
172
|
+
status: e.status,
|
|
173
|
+
input: e.input,
|
|
174
|
+
output: e.output,
|
|
175
|
+
latencyMs: e.latencyMs,
|
|
176
|
+
costCents: e.costCents,
|
|
177
|
+
error: e.error,
|
|
178
|
+
parallelGroup: e.parallelGroup,
|
|
179
|
+
createdAt: e.createdAt,
|
|
180
|
+
startedAt: e.startedAt,
|
|
181
|
+
completedAt: e.completedAt,
|
|
182
|
+
})),
|
|
183
|
+
tokensSaved,
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resume a failed/paused chain (authenticated via session token)
|
|
190
|
+
*/
|
|
191
|
+
export const resumeChainAuth = mutation({
|
|
192
|
+
args: {
|
|
193
|
+
token: v.string(),
|
|
194
|
+
chainId: v.id("chains"),
|
|
195
|
+
overrides: v.optional(v.any()),
|
|
196
|
+
},
|
|
197
|
+
handler: async (ctx, args) => {
|
|
198
|
+
// Validate session
|
|
199
|
+
const session = await ctx.db
|
|
200
|
+
.query("agentSessions")
|
|
201
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
202
|
+
.first();
|
|
203
|
+
|
|
204
|
+
if (!session) {
|
|
205
|
+
return { error: "Invalid session" };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get the chain
|
|
209
|
+
const chain = await ctx.db.get(args.chainId);
|
|
210
|
+
if (!chain || chain.workspaceId !== session.workspaceId) {
|
|
211
|
+
return { error: "Chain not found" };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!chain.canResume) {
|
|
215
|
+
return { error: "Chain cannot be resumed" };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update chain status to pending (orchestrator will pick it up)
|
|
219
|
+
await ctx.db.patch(args.chainId, {
|
|
220
|
+
status: "pending",
|
|
221
|
+
error: undefined,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return { success: true, chainId: args.chainId };
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get chain statistics for workspace (authenticated via session token)
|
|
230
|
+
*/
|
|
231
|
+
export const getChainStatsAuth = query({
|
|
232
|
+
args: {
|
|
233
|
+
token: v.string(),
|
|
234
|
+
},
|
|
235
|
+
handler: async (ctx, args) => {
|
|
236
|
+
// Validate session
|
|
237
|
+
const session = await ctx.db
|
|
238
|
+
.query("agentSessions")
|
|
239
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
240
|
+
.first();
|
|
241
|
+
|
|
242
|
+
if (!session) {
|
|
243
|
+
return { error: "Invalid session" };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const chains = await ctx.db
|
|
247
|
+
.query("chains")
|
|
248
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
249
|
+
.collect();
|
|
250
|
+
|
|
251
|
+
const total = chains.length;
|
|
252
|
+
const completed = chains.filter((c) => c.status === "completed").length;
|
|
253
|
+
const failed = chains.filter((c) => c.status === "failed").length;
|
|
254
|
+
const running = chains.filter((c) => c.status === "running").length;
|
|
255
|
+
const paused = chains.filter((c) => c.status === "paused").length;
|
|
256
|
+
|
|
257
|
+
const totalCostCents = chains.reduce((acc, c) => acc + (c.totalCostCents || 0), 0);
|
|
258
|
+
const totalLatencyMs = chains.reduce((acc, c) => acc + (c.totalLatencyMs || 0), 0);
|
|
259
|
+
|
|
260
|
+
// Count total steps across all chains
|
|
261
|
+
const allExecutions = await Promise.all(
|
|
262
|
+
chains.map((c) =>
|
|
263
|
+
ctx.db
|
|
264
|
+
.query("chainExecutions")
|
|
265
|
+
.withIndex("by_chainId", (q) => q.eq("chainId", c._id))
|
|
266
|
+
.collect()
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
const totalSteps = allExecutions.flat().length;
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
total,
|
|
273
|
+
completed,
|
|
274
|
+
failed,
|
|
275
|
+
running,
|
|
276
|
+
paused,
|
|
277
|
+
totalCostCents,
|
|
278
|
+
totalLatencyMs,
|
|
279
|
+
totalSteps,
|
|
280
|
+
successRate: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
44
285
|
// ============================================
|
|
45
286
|
// MUTATIONS
|
|
46
287
|
// ============================================
|
|
47
288
|
|
|
48
289
|
/**
|
|
49
290
|
* Create a new chain execution record (internal)
|
|
50
|
-
* Validates chain structure and initializes execution state
|
|
51
291
|
*/
|
|
52
292
|
export const createChainInternal = internalMutation({
|
|
53
293
|
args: {
|
|
@@ -63,7 +303,6 @@ export const createChainInternal = internalMutation({
|
|
|
63
303
|
for (let i = 0; i < args.steps.length; i++) {
|
|
64
304
|
const step = args.steps[i] as ChainStep;
|
|
65
305
|
|
|
66
|
-
// Check for parallel steps
|
|
67
306
|
if (step.parallel) {
|
|
68
307
|
for (const pStep of step.parallel) {
|
|
69
308
|
if (!pStep.id || !pStep.provider || !pStep.action) {
|
|
@@ -94,7 +333,6 @@ export const createChainInternal = internalMutation({
|
|
|
94
333
|
const step = args.steps[i] as ChainStep;
|
|
95
334
|
|
|
96
335
|
if (step.parallel) {
|
|
97
|
-
// Create execution records for parallel steps
|
|
98
336
|
const parallelGroup = `parallel_${i}_${Date.now()}`;
|
|
99
337
|
for (const pStep of step.parallel) {
|
|
100
338
|
await ctx.db.insert("chainExecutions", {
|
|
@@ -134,11 +372,10 @@ export const createChain = mutation({
|
|
|
134
372
|
handler: async (ctx, args) => {
|
|
135
373
|
const now = Date.now();
|
|
136
374
|
|
|
137
|
-
// Validate steps
|
|
375
|
+
// Validate steps
|
|
138
376
|
for (let i = 0; i < args.steps.length; i++) {
|
|
139
377
|
const step = args.steps[i] as ChainStep;
|
|
140
378
|
|
|
141
|
-
// Check for parallel steps
|
|
142
379
|
if (step.parallel) {
|
|
143
380
|
for (const pStep of step.parallel) {
|
|
144
381
|
if (!pStep.id || !pStep.provider || !pStep.action) {
|
|
@@ -150,7 +387,6 @@ export const createChain = mutation({
|
|
|
150
387
|
}
|
|
151
388
|
}
|
|
152
389
|
|
|
153
|
-
// Create chain record
|
|
154
390
|
const chainId = await ctx.db.insert("chains", {
|
|
155
391
|
workspaceId: args.workspaceId,
|
|
156
392
|
steps: args.steps,
|
|
@@ -164,7 +400,7 @@ export const createChain = mutation({
|
|
|
164
400
|
createdAt: now,
|
|
165
401
|
});
|
|
166
402
|
|
|
167
|
-
// Create execution records
|
|
403
|
+
// Create execution records
|
|
168
404
|
for (let i = 0; i < args.steps.length; i++) {
|
|
169
405
|
const step = args.steps[i] as ChainStep;
|
|
170
406
|
|
|
@@ -205,7 +441,6 @@ export const createChainFromTemplate = mutation({
|
|
|
205
441
|
inputs: v.optional(v.any()),
|
|
206
442
|
},
|
|
207
443
|
handler: async (ctx, args) => {
|
|
208
|
-
// Get template
|
|
209
444
|
const template = await ctx.db
|
|
210
445
|
.query("chainTemplates")
|
|
211
446
|
.withIndex("by_name", (q) =>
|
|
@@ -219,7 +454,6 @@ export const createChainFromTemplate = mutation({
|
|
|
219
454
|
|
|
220
455
|
const now = Date.now();
|
|
221
456
|
|
|
222
|
-
// Create chain from template
|
|
223
457
|
const chainId = await ctx.db.insert("chains", {
|
|
224
458
|
workspaceId: args.workspaceId,
|
|
225
459
|
steps: template.chain,
|
|
@@ -231,7 +465,6 @@ export const createChainFromTemplate = mutation({
|
|
|
231
465
|
createdAt: now,
|
|
232
466
|
});
|
|
233
467
|
|
|
234
|
-
// Create execution records
|
|
235
468
|
for (let i = 0; i < template.chain.length; i++) {
|
|
236
469
|
const step = template.chain[i] as ChainStep;
|
|
237
470
|
|
|
@@ -258,7 +491,6 @@ export const createChainFromTemplate = mutation({
|
|
|
258
491
|
}
|
|
259
492
|
}
|
|
260
493
|
|
|
261
|
-
// Increment template use count
|
|
262
494
|
await ctx.db.patch(template._id, {
|
|
263
495
|
useCount: (template.useCount || 0) + 1,
|
|
264
496
|
lastUsedAt: now,
|
|
@@ -270,7 +502,6 @@ export const createChainFromTemplate = mutation({
|
|
|
270
502
|
|
|
271
503
|
/**
|
|
272
504
|
* Execute a single step and store the result
|
|
273
|
-
* Called by the orchestrator action
|
|
274
505
|
*/
|
|
275
506
|
export const executeStep = internalMutation({
|
|
276
507
|
args: {
|
|
@@ -282,7 +513,6 @@ export const executeStep = internalMutation({
|
|
|
282
513
|
handler: async (ctx, args) => {
|
|
283
514
|
const now = Date.now();
|
|
284
515
|
|
|
285
|
-
// Get execution record
|
|
286
516
|
const execution = await ctx.db
|
|
287
517
|
.query("chainExecutions")
|
|
288
518
|
.withIndex("by_chainId_stepId", (q) =>
|
|
@@ -294,14 +524,12 @@ export const executeStep = internalMutation({
|
|
|
294
524
|
throw new Error(`No execution record found for step ${args.stepId}`);
|
|
295
525
|
}
|
|
296
526
|
|
|
297
|
-
// Mark as running
|
|
298
527
|
await ctx.db.patch(execution._id, {
|
|
299
528
|
status: "running",
|
|
300
529
|
input: args.input,
|
|
301
530
|
startedAt: now,
|
|
302
531
|
});
|
|
303
532
|
|
|
304
|
-
// Update chain status
|
|
305
533
|
const chain = await ctx.db.get(args.chainId);
|
|
306
534
|
if (chain && chain.status === "pending") {
|
|
307
535
|
await ctx.db.patch(args.chainId, {
|
|
@@ -328,7 +556,6 @@ export const completeStep = internalMutation({
|
|
|
328
556
|
handler: async (ctx, args) => {
|
|
329
557
|
const now = Date.now();
|
|
330
558
|
|
|
331
|
-
// Get execution record
|
|
332
559
|
const execution = await ctx.db
|
|
333
560
|
.query("chainExecutions")
|
|
334
561
|
.withIndex("by_chainId_stepId", (q) =>
|
|
@@ -340,7 +567,6 @@ export const completeStep = internalMutation({
|
|
|
340
567
|
throw new Error(`No execution record found for step ${args.stepId}`);
|
|
341
568
|
}
|
|
342
569
|
|
|
343
|
-
// Mark as completed
|
|
344
570
|
await ctx.db.patch(execution._id, {
|
|
345
571
|
status: "completed",
|
|
346
572
|
output: args.output,
|
|
@@ -349,7 +575,6 @@ export const completeStep = internalMutation({
|
|
|
349
575
|
completedAt: now,
|
|
350
576
|
});
|
|
351
577
|
|
|
352
|
-
// Update chain results
|
|
353
578
|
const chain = await ctx.db.get(args.chainId);
|
|
354
579
|
if (chain) {
|
|
355
580
|
const results = { ...(chain.results || {}), [args.stepId]: args.output };
|
|
@@ -368,7 +593,7 @@ export const completeStep = internalMutation({
|
|
|
368
593
|
});
|
|
369
594
|
|
|
370
595
|
/**
|
|
371
|
-
* Advance chain to next step
|
|
596
|
+
* Advance chain to next step
|
|
372
597
|
*/
|
|
373
598
|
export const advanceChain = internalMutation({
|
|
374
599
|
args: {
|
|
@@ -382,17 +607,14 @@ export const advanceChain = internalMutation({
|
|
|
382
607
|
|
|
383
608
|
const nextStep = chain.currentStep + 1;
|
|
384
609
|
|
|
385
|
-
// Check if chain is complete
|
|
386
610
|
if (nextStep >= chain.steps.length) {
|
|
387
611
|
return { complete: true, nextStep: null };
|
|
388
612
|
}
|
|
389
613
|
|
|
390
|
-
// Update current step
|
|
391
614
|
await ctx.db.patch(args.chainId, {
|
|
392
615
|
currentStep: nextStep,
|
|
393
616
|
});
|
|
394
617
|
|
|
395
|
-
// Generate resume token for this position
|
|
396
618
|
const resumeToken = generateResumeToken(args.chainId, nextStep);
|
|
397
619
|
await ctx.db.patch(args.chainId, {
|
|
398
620
|
resumeToken,
|
|
@@ -409,7 +631,6 @@ export const advanceChain = internalMutation({
|
|
|
409
631
|
|
|
410
632
|
/**
|
|
411
633
|
* Handle chain failure
|
|
412
|
-
* Stores partial results and generates resume token
|
|
413
634
|
*/
|
|
414
635
|
export const failChain = internalMutation({
|
|
415
636
|
args: {
|
|
@@ -424,7 +645,6 @@ export const failChain = internalMutation({
|
|
|
424
645
|
handler: async (ctx, args) => {
|
|
425
646
|
const now = Date.now();
|
|
426
647
|
|
|
427
|
-
// Update execution record
|
|
428
648
|
const execution = await ctx.db
|
|
429
649
|
.query("chainExecutions")
|
|
430
650
|
.withIndex("by_chainId_stepId", (q) =>
|
|
@@ -443,16 +663,13 @@ export const failChain = internalMutation({
|
|
|
443
663
|
});
|
|
444
664
|
}
|
|
445
665
|
|
|
446
|
-
// Get chain to determine resume position
|
|
447
666
|
const chain = await ctx.db.get(args.chainId);
|
|
448
667
|
if (!chain) {
|
|
449
668
|
throw new Error("Chain not found");
|
|
450
669
|
}
|
|
451
670
|
|
|
452
|
-
// Generate resume token
|
|
453
671
|
const resumeToken = generateResumeToken(args.chainId, chain.currentStep);
|
|
454
672
|
|
|
455
|
-
// Update chain status
|
|
456
673
|
await ctx.db.patch(args.chainId, {
|
|
457
674
|
status: "failed",
|
|
458
675
|
error: {
|
|
@@ -477,10 +694,9 @@ export const failChain = internalMutation({
|
|
|
477
694
|
export const resumeChain = mutation({
|
|
478
695
|
args: {
|
|
479
696
|
resumeToken: v.string(),
|
|
480
|
-
overrides: v.optional(v.any()),
|
|
697
|
+
overrides: v.optional(v.any()),
|
|
481
698
|
},
|
|
482
699
|
handler: async (ctx, args) => {
|
|
483
|
-
// Find chain by resume token
|
|
484
700
|
const chain = await ctx.db
|
|
485
701
|
.query("chains")
|
|
486
702
|
.withIndex("by_resumeToken", (q) => q.eq("resumeToken", args.resumeToken))
|
|
@@ -494,7 +710,6 @@ export const resumeChain = mutation({
|
|
|
494
710
|
throw new Error("Chain cannot be resumed");
|
|
495
711
|
}
|
|
496
712
|
|
|
497
|
-
// Reset failed step to pending
|
|
498
713
|
const executions = await ctx.db
|
|
499
714
|
.query("chainExecutions")
|
|
500
715
|
.withIndex("by_chainId_stepIndex", (q) =>
|
|
@@ -513,7 +728,6 @@ export const resumeChain = mutation({
|
|
|
513
728
|
}
|
|
514
729
|
}
|
|
515
730
|
|
|
516
|
-
// Update chain status
|
|
517
731
|
await ctx.db.patch(chain._id, {
|
|
518
732
|
status: "pending",
|
|
519
733
|
error: undefined,
|
|
@@ -563,7 +777,7 @@ export const completeChain = internalMutation({
|
|
|
563
777
|
});
|
|
564
778
|
|
|
565
779
|
/**
|
|
566
|
-
* Pause chain execution
|
|
780
|
+
* Pause chain execution
|
|
567
781
|
*/
|
|
568
782
|
export const pauseChain = mutation({
|
|
569
783
|
args: {
|
|
@@ -595,9 +809,6 @@ export const pauseChain = mutation({
|
|
|
595
809
|
// CHAIN TEMPLATE MUTATIONS
|
|
596
810
|
// ============================================
|
|
597
811
|
|
|
598
|
-
/**
|
|
599
|
-
* Create or update a chain template
|
|
600
|
-
*/
|
|
601
812
|
export const saveChainTemplate = mutation({
|
|
602
813
|
args: {
|
|
603
814
|
id: v.optional(v.id("chainTemplates")),
|
|
@@ -611,7 +822,6 @@ export const saveChainTemplate = mutation({
|
|
|
611
822
|
const now = Date.now();
|
|
612
823
|
|
|
613
824
|
if (args.id) {
|
|
614
|
-
// Update existing
|
|
615
825
|
await ctx.db.patch(args.id, {
|
|
616
826
|
name: args.name,
|
|
617
827
|
description: args.description,
|
|
@@ -622,7 +832,6 @@ export const saveChainTemplate = mutation({
|
|
|
622
832
|
return args.id;
|
|
623
833
|
}
|
|
624
834
|
|
|
625
|
-
// Create new
|
|
626
835
|
return await ctx.db.insert("chainTemplates", {
|
|
627
836
|
workspaceId: args.workspaceId,
|
|
628
837
|
name: args.name,
|
|
@@ -636,9 +845,6 @@ export const saveChainTemplate = mutation({
|
|
|
636
845
|
},
|
|
637
846
|
});
|
|
638
847
|
|
|
639
|
-
/**
|
|
640
|
-
* Delete a chain template
|
|
641
|
-
*/
|
|
642
848
|
export const deleteChainTemplate = mutation({
|
|
643
849
|
args: {
|
|
644
850
|
id: v.id("chainTemplates"),
|
|
@@ -653,9 +859,6 @@ export const deleteChainTemplate = mutation({
|
|
|
653
859
|
// QUERIES
|
|
654
860
|
// ============================================
|
|
655
861
|
|
|
656
|
-
/**
|
|
657
|
-
* Get chain status and results
|
|
658
|
-
*/
|
|
659
862
|
export const getChain = query({
|
|
660
863
|
args: {
|
|
661
864
|
chainId: v.id("chains"),
|
|
@@ -684,9 +887,6 @@ export const getChain = query({
|
|
|
684
887
|
},
|
|
685
888
|
});
|
|
686
889
|
|
|
687
|
-
/**
|
|
688
|
-
* Get full execution trace for dashboard
|
|
689
|
-
*/
|
|
690
890
|
export const getChainTrace = query({
|
|
691
891
|
args: {
|
|
692
892
|
chainId: v.id("chains"),
|
|
@@ -697,16 +897,13 @@ export const getChainTrace = query({
|
|
|
697
897
|
return null;
|
|
698
898
|
}
|
|
699
899
|
|
|
700
|
-
// Get all step executions
|
|
701
900
|
const executions = await ctx.db
|
|
702
901
|
.query("chainExecutions")
|
|
703
902
|
.withIndex("by_chainId", (q) => q.eq("chainId", args.chainId))
|
|
704
903
|
.collect();
|
|
705
904
|
|
|
706
|
-
// Sort by stepIndex
|
|
707
905
|
executions.sort((a, b) => a.stepIndex - b.stepIndex);
|
|
708
906
|
|
|
709
|
-
// Build trace
|
|
710
907
|
const trace = executions.map((exec) => ({
|
|
711
908
|
stepId: exec.stepId,
|
|
712
909
|
stepIndex: exec.stepIndex,
|
|
@@ -721,7 +918,6 @@ export const getChainTrace = query({
|
|
|
721
918
|
completedAt: exec.completedAt,
|
|
722
919
|
}));
|
|
723
920
|
|
|
724
|
-
// Calculate tokens saved (estimate: ~400 tokens per avoided round-trip)
|
|
725
921
|
const completedSteps = executions.filter((e) => e.status === "completed");
|
|
726
922
|
const tokensSaved = completedSteps.length > 1 ? (completedSteps.length - 1) * 400 : 0;
|
|
727
923
|
|
|
@@ -746,9 +942,6 @@ export const getChainTrace = query({
|
|
|
746
942
|
},
|
|
747
943
|
});
|
|
748
944
|
|
|
749
|
-
/**
|
|
750
|
-
* List chains for a workspace
|
|
751
|
-
*/
|
|
752
945
|
export const listChains = query({
|
|
753
946
|
args: {
|
|
754
947
|
workspaceId: v.id("workspaces"),
|
|
@@ -758,14 +951,12 @@ export const listChains = query({
|
|
|
758
951
|
handler: async (ctx, args) => {
|
|
759
952
|
const limit = args.limit ?? 50;
|
|
760
953
|
|
|
761
|
-
|
|
954
|
+
const chains = await ctx.db
|
|
762
955
|
.query("chains")
|
|
763
956
|
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
|
|
764
|
-
.order("desc")
|
|
765
|
-
|
|
766
|
-
const chains = await chainsQuery.take(limit);
|
|
957
|
+
.order("desc")
|
|
958
|
+
.take(limit);
|
|
767
959
|
|
|
768
|
-
// Filter by status if provided
|
|
769
960
|
let filtered = chains;
|
|
770
961
|
if (args.status && args.status !== "all") {
|
|
771
962
|
filtered = chains.filter((c) => c.status === args.status);
|
|
@@ -786,9 +977,6 @@ export const listChains = query({
|
|
|
786
977
|
},
|
|
787
978
|
});
|
|
788
979
|
|
|
789
|
-
/**
|
|
790
|
-
* List chain templates for a workspace
|
|
791
|
-
*/
|
|
792
980
|
export const listChainTemplates = query({
|
|
793
981
|
args: {
|
|
794
982
|
workspaceId: v.id("workspaces"),
|
|
@@ -801,9 +989,6 @@ export const listChainTemplates = query({
|
|
|
801
989
|
},
|
|
802
990
|
});
|
|
803
991
|
|
|
804
|
-
/**
|
|
805
|
-
* Get chain template by name
|
|
806
|
-
*/
|
|
807
992
|
export const getChainTemplate = query({
|
|
808
993
|
args: {
|
|
809
994
|
workspaceId: v.id("workspaces"),
|
|
@@ -819,9 +1004,6 @@ export const getChainTemplate = query({
|
|
|
819
1004
|
},
|
|
820
1005
|
});
|
|
821
1006
|
|
|
822
|
-
/**
|
|
823
|
-
* Get chain statistics for workspace
|
|
824
|
-
*/
|
|
825
1007
|
export const getChainStats = query({
|
|
826
1008
|
args: {
|
|
827
1009
|
workspaceId: v.id("workspaces"),
|
|
@@ -841,7 +1023,6 @@ export const getChainStats = query({
|
|
|
841
1023
|
const totalCostCents = chains.reduce((acc, c) => acc + (c.totalCostCents || 0), 0);
|
|
842
1024
|
const totalLatencyMs = chains.reduce((acc, c) => acc + (c.totalLatencyMs || 0), 0);
|
|
843
1025
|
|
|
844
|
-
// Count total steps across all chains
|
|
845
1026
|
const allExecutions = await Promise.all(
|
|
846
1027
|
chains.map((c) =>
|
|
847
1028
|
ctx.db
|
|
@@ -870,10 +1051,6 @@ export const getChainStats = query({
|
|
|
870
1051
|
// ACTIONS (Orchestration Logic)
|
|
871
1052
|
// ============================================
|
|
872
1053
|
|
|
873
|
-
/**
|
|
874
|
-
* Main chain orchestrator
|
|
875
|
-
* Validates chain → executes steps sequentially/parallel → handles errors
|
|
876
|
-
*/
|
|
877
1054
|
export const runChain = action({
|
|
878
1055
|
args: {
|
|
879
1056
|
workspaceId: v.id("workspaces"),
|
|
@@ -894,9 +1071,8 @@ export const runChain = action({
|
|
|
894
1071
|
totalLatencyMs?: number;
|
|
895
1072
|
}> => {
|
|
896
1073
|
const startTime = Date.now();
|
|
897
|
-
const timeout = args.timeout || 30000;
|
|
1074
|
+
const timeout = args.timeout || 30000;
|
|
898
1075
|
|
|
899
|
-
// 1. Create chain record
|
|
900
1076
|
const chainId = await ctx.runMutation(internal.chains.createChainInternal, {
|
|
901
1077
|
workspaceId: args.workspaceId,
|
|
902
1078
|
steps: args.steps,
|
|
@@ -908,16 +1084,13 @@ export const runChain = action({
|
|
|
908
1084
|
let currentResults: Record<string, unknown> = {};
|
|
909
1085
|
|
|
910
1086
|
try {
|
|
911
|
-
// 2. Execute steps
|
|
912
1087
|
for (let i = 0; i < args.steps.length; i++) {
|
|
913
|
-
// Check timeout
|
|
914
1088
|
if (Date.now() - startTime > timeout) {
|
|
915
1089
|
throw new Error("TIMEOUT: Chain execution exceeded timeout");
|
|
916
1090
|
}
|
|
917
1091
|
|
|
918
1092
|
const step = args.steps[i] as ChainStep;
|
|
919
1093
|
|
|
920
|
-
// Handle parallel steps
|
|
921
1094
|
if (step.parallel && step.parallel.length > 0) {
|
|
922
1095
|
const parallelResults = await ctx.runAction(internal.chains.runParallelSteps, {
|
|
923
1096
|
chainId,
|
|
@@ -925,13 +1098,11 @@ export const runChain = action({
|
|
|
925
1098
|
stepIndex: i,
|
|
926
1099
|
});
|
|
927
1100
|
|
|
928
|
-
// Merge parallel results
|
|
929
1101
|
for (const [stepId, result] of Object.entries(parallelResults)) {
|
|
930
1102
|
currentResults[stepId] = result;
|
|
931
1103
|
completedSteps.push(stepId);
|
|
932
1104
|
}
|
|
933
1105
|
} else {
|
|
934
|
-
// Sequential step
|
|
935
1106
|
await ctx.runMutation(internal.chains.executeStep, {
|
|
936
1107
|
chainId,
|
|
937
1108
|
stepId: step.id,
|
|
@@ -939,7 +1110,6 @@ export const runChain = action({
|
|
|
939
1110
|
input: step.params,
|
|
940
1111
|
});
|
|
941
1112
|
|
|
942
|
-
// Execute the actual API call (placeholder - in production calls Direct Call)
|
|
943
1113
|
const stepStartTime = Date.now();
|
|
944
1114
|
const result = await executeProviderCall(ctx, step);
|
|
945
1115
|
const latencyMs = Date.now() - stepStartTime;
|
|
@@ -956,11 +1126,9 @@ export const runChain = action({
|
|
|
956
1126
|
completedSteps.push(step.id);
|
|
957
1127
|
}
|
|
958
1128
|
|
|
959
|
-
// Advance to next step
|
|
960
1129
|
await ctx.runMutation(internal.chains.advanceChain, { chainId });
|
|
961
1130
|
}
|
|
962
1131
|
|
|
963
|
-
// 3. Complete chain
|
|
964
1132
|
const finalResult = await ctx.runMutation(internal.chains.completeChain, { chainId });
|
|
965
1133
|
|
|
966
1134
|
return {
|
|
@@ -976,11 +1144,9 @@ export const runChain = action({
|
|
|
976
1144
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
977
1145
|
const errorCode = errorMessage.startsWith("TIMEOUT") ? "TIMEOUT" : "EXECUTION_ERROR";
|
|
978
1146
|
|
|
979
|
-
// Find current step
|
|
980
1147
|
const currentStep = args.steps[completedSteps.length] as ChainStep | undefined;
|
|
981
1148
|
const failedStepId = currentStep?.id || "unknown";
|
|
982
1149
|
|
|
983
|
-
// Handle failure
|
|
984
1150
|
const failureResult = await ctx.runMutation(internal.chains.failChain, {
|
|
985
1151
|
chainId,
|
|
986
1152
|
stepId: failedStepId,
|
|
@@ -1007,9 +1173,6 @@ export const runChain = action({
|
|
|
1007
1173
|
},
|
|
1008
1174
|
});
|
|
1009
1175
|
|
|
1010
|
-
/**
|
|
1011
|
-
* Execute parallel steps concurrently
|
|
1012
|
-
*/
|
|
1013
1176
|
export const runParallelSteps = internalAction({
|
|
1014
1177
|
args: {
|
|
1015
1178
|
chainId: v.id("chains"),
|
|
@@ -1019,7 +1182,6 @@ export const runParallelSteps = internalAction({
|
|
|
1019
1182
|
handler: async (ctx, args): Promise<Record<string, unknown>> => {
|
|
1020
1183
|
const results: Record<string, unknown> = {};
|
|
1021
1184
|
|
|
1022
|
-
// Mark all steps as running
|
|
1023
1185
|
for (const step of args.steps as ChainStep[]) {
|
|
1024
1186
|
await ctx.runMutation(internal.chains.executeStep, {
|
|
1025
1187
|
chainId: args.chainId,
|
|
@@ -1029,7 +1191,6 @@ export const runParallelSteps = internalAction({
|
|
|
1029
1191
|
});
|
|
1030
1192
|
}
|
|
1031
1193
|
|
|
1032
|
-
// Execute all in parallel
|
|
1033
1194
|
const promises = (args.steps as ChainStep[]).map(async (step) => {
|
|
1034
1195
|
const startTime = Date.now();
|
|
1035
1196
|
|
|
@@ -1062,22 +1223,14 @@ export const runParallelSteps = internalAction({
|
|
|
1062
1223
|
});
|
|
1063
1224
|
|
|
1064
1225
|
// ============================================
|
|
1065
|
-
// HELPER: Execute provider call
|
|
1066
|
-
// Placeholder - in production, this calls the Direct Call API
|
|
1226
|
+
// HELPER: Execute provider call (placeholder)
|
|
1067
1227
|
// ============================================
|
|
1068
1228
|
|
|
1069
1229
|
async function executeProviderCall(
|
|
1070
1230
|
ctx: any,
|
|
1071
1231
|
step: ChainStep
|
|
1072
1232
|
): Promise<{ success: boolean; data?: unknown; costCents?: number }> {
|
|
1073
|
-
//
|
|
1074
|
-
// In production, this would:
|
|
1075
|
-
// 1. Look up the provider's Direct Call config
|
|
1076
|
-
// 2. Find the action definition
|
|
1077
|
-
// 3. Execute the HTTP request with params
|
|
1078
|
-
// 4. Return the mapped result
|
|
1079
|
-
|
|
1080
|
-
// Simulate some latency
|
|
1233
|
+
// Simulate latency
|
|
1081
1234
|
await new Promise((resolve) => setTimeout(resolve, 50 + Math.random() * 100));
|
|
1082
1235
|
|
|
1083
1236
|
return {
|
|
@@ -1090,6 +1243,6 @@ async function executeProviderCall(
|
|
|
1090
1243
|
mockResult: `Executed ${step.action} on ${step.provider}`,
|
|
1091
1244
|
timestamp: Date.now(),
|
|
1092
1245
|
},
|
|
1093
|
-
costCents: Math.ceil(Math.random() * 5),
|
|
1246
|
+
costCents: Math.ceil(Math.random() * 5),
|
|
1094
1247
|
};
|
|
1095
1248
|
}
|