@nordsym/apiclaw 1.3.13 → 1.4.1
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-API-CHAINING.md +483 -0
- package/PRD-HARDEN-SHELL.md +18 -12
- package/PRD-LOGS-SUBAGENTS-V2.md +267 -0
- package/convex/_generated/api.d.ts +6 -0
- package/convex/agents.ts +188 -0
- package/convex/chains.ts +1248 -0
- package/convex/logs.ts +94 -0
- package/convex/schema.ts +139 -0
- package/convex/searchLogs.ts +141 -0
- package/convex/teams.ts +243 -0
- package/dist/chain-types.d.ts +187 -0
- package/dist/chain-types.d.ts.map +1 -0
- package/dist/chain-types.js +33 -0
- package/dist/chain-types.js.map +1 -0
- package/dist/chainExecutor.d.ts +122 -0
- package/dist/chainExecutor.d.ts.map +1 -0
- package/dist/chainExecutor.js +454 -0
- package/dist/chainExecutor.js.map +1 -0
- package/dist/chainResolver.d.ts +100 -0
- package/dist/chainResolver.d.ts.map +1 -0
- package/dist/chainResolver.js +519 -0
- package/dist/chainResolver.js.map +1 -0
- package/dist/chainResolver.test.d.ts +5 -0
- package/dist/chainResolver.test.d.ts.map +1 -0
- package/dist/chainResolver.test.js +201 -0
- package/dist/chainResolver.test.js.map +1 -0
- package/dist/execute.d.ts +4 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +3 -0
- package/dist/execute.js.map +1 -1
- package/dist/index.js +478 -3
- package/dist/index.js.map +1 -1
- package/docs/SUBAGENT-NAMING.md +94 -0
- package/landing/public/logos/chattgpt.svg +1 -0
- package/landing/public/logos/claude.svg +1 -0
- package/landing/public/logos/gemini.svg +1 -0
- package/landing/public/logos/grok.svg +1 -0
- package/landing/src/app/page.tsx +12 -21
- package/landing/src/app/workspace/chains/page.tsx +520 -0
- package/landing/src/app/workspace/page.tsx +1903 -224
- package/landing/src/components/AITestimonials.tsx +15 -9
- package/landing/src/components/ChainStepDetail.tsx +310 -0
- package/landing/src/components/ChainTrace.tsx +261 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +14 -2
- package/src/chainExecutor.ts +730 -0
- package/src/chainResolver.test.ts +246 -0
- package/src/chainResolver.ts +658 -0
- package/src/execute.ts +23 -0
- package/src/index.ts +524 -3
package/convex/chains.ts
ADDED
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, action, internalMutation, internalAction } from "./_generated/server";
|
|
3
|
+
import { internal } from "./_generated/api";
|
|
4
|
+
import { Id } from "./_generated/dataModel";
|
|
5
|
+
|
|
6
|
+
// ============================================
|
|
7
|
+
// TYPES
|
|
8
|
+
// ============================================
|
|
9
|
+
|
|
10
|
+
type ChainStatus = "pending" | "running" | "completed" | "failed" | "paused";
|
|
11
|
+
type StepStatus = "pending" | "running" | "completed" | "failed" | "skipped";
|
|
12
|
+
|
|
13
|
+
interface ChainStep {
|
|
14
|
+
id: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
action: string;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
onError?: {
|
|
19
|
+
retry?: { attempts: number; backoff?: number[] };
|
|
20
|
+
fallback?: ChainStep;
|
|
21
|
+
abort?: boolean;
|
|
22
|
+
};
|
|
23
|
+
parallel?: ChainStep[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChainError {
|
|
27
|
+
stepId: string;
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
retryAfter?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// HELPER: Generate resume token
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
function generateResumeToken(chainId: string, stepIndex: number): string {
|
|
38
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
39
|
+
return `chain_${chainId.slice(-8)}_step_${stepIndex}_${random}`;
|
|
40
|
+
}
|
|
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
|
+
|
|
285
|
+
// ============================================
|
|
286
|
+
// MUTATIONS
|
|
287
|
+
// ============================================
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create a new chain execution record (internal)
|
|
291
|
+
*/
|
|
292
|
+
export const createChainInternal = internalMutation({
|
|
293
|
+
args: {
|
|
294
|
+
workspaceId: v.id("workspaces"),
|
|
295
|
+
steps: v.array(v.any()),
|
|
296
|
+
continueOnError: v.optional(v.boolean()),
|
|
297
|
+
timeout: v.optional(v.number()),
|
|
298
|
+
},
|
|
299
|
+
handler: async (ctx, args) => {
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
|
|
302
|
+
// Validate steps have required fields
|
|
303
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
304
|
+
const step = args.steps[i] as ChainStep;
|
|
305
|
+
|
|
306
|
+
if (step.parallel) {
|
|
307
|
+
for (const pStep of step.parallel) {
|
|
308
|
+
if (!pStep.id || !pStep.provider || !pStep.action) {
|
|
309
|
+
throw new Error(`Parallel step at index ${i} missing required fields (id, provider, action)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} else if (!step.id || !step.provider || !step.action) {
|
|
313
|
+
throw new Error(`Step at index ${i} missing required fields (id, provider, action)`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Create chain record
|
|
318
|
+
const chainId = await ctx.db.insert("chains", {
|
|
319
|
+
workspaceId: args.workspaceId,
|
|
320
|
+
steps: args.steps,
|
|
321
|
+
status: "pending",
|
|
322
|
+
currentStep: 0,
|
|
323
|
+
results: {},
|
|
324
|
+
continueOnError: args.continueOnError ?? false,
|
|
325
|
+
timeout: args.timeout,
|
|
326
|
+
totalCostCents: 0,
|
|
327
|
+
totalLatencyMs: 0,
|
|
328
|
+
createdAt: now,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Create execution records for each step
|
|
332
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
333
|
+
const step = args.steps[i] as ChainStep;
|
|
334
|
+
|
|
335
|
+
if (step.parallel) {
|
|
336
|
+
const parallelGroup = `parallel_${i}_${Date.now()}`;
|
|
337
|
+
for (const pStep of step.parallel) {
|
|
338
|
+
await ctx.db.insert("chainExecutions", {
|
|
339
|
+
chainId,
|
|
340
|
+
stepId: pStep.id,
|
|
341
|
+
stepIndex: i,
|
|
342
|
+
status: "pending",
|
|
343
|
+
parallelGroup,
|
|
344
|
+
createdAt: now,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
await ctx.db.insert("chainExecutions", {
|
|
349
|
+
chainId,
|
|
350
|
+
stepId: step.id,
|
|
351
|
+
stepIndex: i,
|
|
352
|
+
status: "pending",
|
|
353
|
+
createdAt: now,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return chainId;
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Create a new chain execution record (public API)
|
|
364
|
+
*/
|
|
365
|
+
export const createChain = mutation({
|
|
366
|
+
args: {
|
|
367
|
+
workspaceId: v.id("workspaces"),
|
|
368
|
+
steps: v.array(v.any()),
|
|
369
|
+
continueOnError: v.optional(v.boolean()),
|
|
370
|
+
timeout: v.optional(v.number()),
|
|
371
|
+
},
|
|
372
|
+
handler: async (ctx, args) => {
|
|
373
|
+
const now = Date.now();
|
|
374
|
+
|
|
375
|
+
// Validate steps
|
|
376
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
377
|
+
const step = args.steps[i] as ChainStep;
|
|
378
|
+
|
|
379
|
+
if (step.parallel) {
|
|
380
|
+
for (const pStep of step.parallel) {
|
|
381
|
+
if (!pStep.id || !pStep.provider || !pStep.action) {
|
|
382
|
+
throw new Error(`Parallel step at index ${i} missing required fields (id, provider, action)`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
} else if (!step.id || !step.provider || !step.action) {
|
|
386
|
+
throw new Error(`Step at index ${i} missing required fields (id, provider, action)`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const chainId = await ctx.db.insert("chains", {
|
|
391
|
+
workspaceId: args.workspaceId,
|
|
392
|
+
steps: args.steps,
|
|
393
|
+
status: "pending",
|
|
394
|
+
currentStep: 0,
|
|
395
|
+
results: {},
|
|
396
|
+
continueOnError: args.continueOnError ?? false,
|
|
397
|
+
timeout: args.timeout,
|
|
398
|
+
totalCostCents: 0,
|
|
399
|
+
totalLatencyMs: 0,
|
|
400
|
+
createdAt: now,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Create execution records
|
|
404
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
405
|
+
const step = args.steps[i] as ChainStep;
|
|
406
|
+
|
|
407
|
+
if (step.parallel) {
|
|
408
|
+
const parallelGroup = `parallel_${i}_${Date.now()}`;
|
|
409
|
+
for (const pStep of step.parallel) {
|
|
410
|
+
await ctx.db.insert("chainExecutions", {
|
|
411
|
+
chainId,
|
|
412
|
+
stepId: pStep.id,
|
|
413
|
+
stepIndex: i,
|
|
414
|
+
status: "pending",
|
|
415
|
+
parallelGroup,
|
|
416
|
+
createdAt: now,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
await ctx.db.insert("chainExecutions", {
|
|
421
|
+
chainId,
|
|
422
|
+
stepId: step.id,
|
|
423
|
+
stepIndex: i,
|
|
424
|
+
status: "pending",
|
|
425
|
+
createdAt: now,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return chainId;
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Create chain from template
|
|
436
|
+
*/
|
|
437
|
+
export const createChainFromTemplate = mutation({
|
|
438
|
+
args: {
|
|
439
|
+
workspaceId: v.id("workspaces"),
|
|
440
|
+
templateName: v.string(),
|
|
441
|
+
inputs: v.optional(v.any()),
|
|
442
|
+
},
|
|
443
|
+
handler: async (ctx, args) => {
|
|
444
|
+
const template = await ctx.db
|
|
445
|
+
.query("chainTemplates")
|
|
446
|
+
.withIndex("by_name", (q) =>
|
|
447
|
+
q.eq("workspaceId", args.workspaceId).eq("name", args.templateName)
|
|
448
|
+
)
|
|
449
|
+
.first();
|
|
450
|
+
|
|
451
|
+
if (!template) {
|
|
452
|
+
throw new Error(`Template '${args.templateName}' not found`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const now = Date.now();
|
|
456
|
+
|
|
457
|
+
const chainId = await ctx.db.insert("chains", {
|
|
458
|
+
workspaceId: args.workspaceId,
|
|
459
|
+
steps: template.chain,
|
|
460
|
+
status: "pending",
|
|
461
|
+
currentStep: 0,
|
|
462
|
+
results: {},
|
|
463
|
+
totalCostCents: 0,
|
|
464
|
+
totalLatencyMs: 0,
|
|
465
|
+
createdAt: now,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < template.chain.length; i++) {
|
|
469
|
+
const step = template.chain[i] as ChainStep;
|
|
470
|
+
|
|
471
|
+
if (step.parallel) {
|
|
472
|
+
const parallelGroup = `parallel_${i}_${Date.now()}`;
|
|
473
|
+
for (const pStep of step.parallel) {
|
|
474
|
+
await ctx.db.insert("chainExecutions", {
|
|
475
|
+
chainId,
|
|
476
|
+
stepId: pStep.id,
|
|
477
|
+
stepIndex: i,
|
|
478
|
+
status: "pending",
|
|
479
|
+
parallelGroup,
|
|
480
|
+
createdAt: now,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
await ctx.db.insert("chainExecutions", {
|
|
485
|
+
chainId,
|
|
486
|
+
stepId: step.id,
|
|
487
|
+
stepIndex: i,
|
|
488
|
+
status: "pending",
|
|
489
|
+
createdAt: now,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await ctx.db.patch(template._id, {
|
|
495
|
+
useCount: (template.useCount || 0) + 1,
|
|
496
|
+
lastUsedAt: now,
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return chainId;
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Execute a single step and store the result
|
|
505
|
+
*/
|
|
506
|
+
export const executeStep = internalMutation({
|
|
507
|
+
args: {
|
|
508
|
+
chainId: v.id("chains"),
|
|
509
|
+
stepId: v.string(),
|
|
510
|
+
stepIndex: v.number(),
|
|
511
|
+
input: v.any(),
|
|
512
|
+
},
|
|
513
|
+
handler: async (ctx, args) => {
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
|
|
516
|
+
const execution = await ctx.db
|
|
517
|
+
.query("chainExecutions")
|
|
518
|
+
.withIndex("by_chainId_stepId", (q) =>
|
|
519
|
+
q.eq("chainId", args.chainId).eq("stepId", args.stepId)
|
|
520
|
+
)
|
|
521
|
+
.first();
|
|
522
|
+
|
|
523
|
+
if (!execution) {
|
|
524
|
+
throw new Error(`No execution record found for step ${args.stepId}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
await ctx.db.patch(execution._id, {
|
|
528
|
+
status: "running",
|
|
529
|
+
input: args.input,
|
|
530
|
+
startedAt: now,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const chain = await ctx.db.get(args.chainId);
|
|
534
|
+
if (chain && chain.status === "pending") {
|
|
535
|
+
await ctx.db.patch(args.chainId, {
|
|
536
|
+
status: "running",
|
|
537
|
+
startedAt: now,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return execution._id;
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Record step completion with result
|
|
547
|
+
*/
|
|
548
|
+
export const completeStep = internalMutation({
|
|
549
|
+
args: {
|
|
550
|
+
chainId: v.id("chains"),
|
|
551
|
+
stepId: v.string(),
|
|
552
|
+
output: v.any(),
|
|
553
|
+
latencyMs: v.number(),
|
|
554
|
+
costCents: v.number(),
|
|
555
|
+
},
|
|
556
|
+
handler: async (ctx, args) => {
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
|
|
559
|
+
const execution = await ctx.db
|
|
560
|
+
.query("chainExecutions")
|
|
561
|
+
.withIndex("by_chainId_stepId", (q) =>
|
|
562
|
+
q.eq("chainId", args.chainId).eq("stepId", args.stepId)
|
|
563
|
+
)
|
|
564
|
+
.first();
|
|
565
|
+
|
|
566
|
+
if (!execution) {
|
|
567
|
+
throw new Error(`No execution record found for step ${args.stepId}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
await ctx.db.patch(execution._id, {
|
|
571
|
+
status: "completed",
|
|
572
|
+
output: args.output,
|
|
573
|
+
latencyMs: args.latencyMs,
|
|
574
|
+
costCents: args.costCents,
|
|
575
|
+
completedAt: now,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const chain = await ctx.db.get(args.chainId);
|
|
579
|
+
if (chain) {
|
|
580
|
+
const results = { ...(chain.results || {}), [args.stepId]: args.output };
|
|
581
|
+
const totalCost = (chain.totalCostCents || 0) + args.costCents;
|
|
582
|
+
const totalLatency = (chain.totalLatencyMs || 0) + args.latencyMs;
|
|
583
|
+
|
|
584
|
+
await ctx.db.patch(args.chainId, {
|
|
585
|
+
results,
|
|
586
|
+
totalCostCents: totalCost,
|
|
587
|
+
totalLatencyMs: totalLatency,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { success: true };
|
|
592
|
+
},
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Advance chain to next step
|
|
597
|
+
*/
|
|
598
|
+
export const advanceChain = internalMutation({
|
|
599
|
+
args: {
|
|
600
|
+
chainId: v.id("chains"),
|
|
601
|
+
},
|
|
602
|
+
handler: async (ctx, args) => {
|
|
603
|
+
const chain = await ctx.db.get(args.chainId);
|
|
604
|
+
if (!chain) {
|
|
605
|
+
throw new Error("Chain not found");
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const nextStep = chain.currentStep + 1;
|
|
609
|
+
|
|
610
|
+
if (nextStep >= chain.steps.length) {
|
|
611
|
+
return { complete: true, nextStep: null };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
await ctx.db.patch(args.chainId, {
|
|
615
|
+
currentStep: nextStep,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const resumeToken = generateResumeToken(args.chainId, nextStep);
|
|
619
|
+
await ctx.db.patch(args.chainId, {
|
|
620
|
+
resumeToken,
|
|
621
|
+
canResume: true,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
complete: false,
|
|
626
|
+
nextStep,
|
|
627
|
+
nextStepDef: chain.steps[nextStep],
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Handle chain failure
|
|
634
|
+
*/
|
|
635
|
+
export const failChain = internalMutation({
|
|
636
|
+
args: {
|
|
637
|
+
chainId: v.id("chains"),
|
|
638
|
+
stepId: v.string(),
|
|
639
|
+
error: v.object({
|
|
640
|
+
code: v.string(),
|
|
641
|
+
message: v.string(),
|
|
642
|
+
retryAfter: v.optional(v.number()),
|
|
643
|
+
}),
|
|
644
|
+
},
|
|
645
|
+
handler: async (ctx, args) => {
|
|
646
|
+
const now = Date.now();
|
|
647
|
+
|
|
648
|
+
const execution = await ctx.db
|
|
649
|
+
.query("chainExecutions")
|
|
650
|
+
.withIndex("by_chainId_stepId", (q) =>
|
|
651
|
+
q.eq("chainId", args.chainId).eq("stepId", args.stepId)
|
|
652
|
+
)
|
|
653
|
+
.first();
|
|
654
|
+
|
|
655
|
+
if (execution) {
|
|
656
|
+
await ctx.db.patch(execution._id, {
|
|
657
|
+
status: "failed",
|
|
658
|
+
error: {
|
|
659
|
+
code: args.error.code,
|
|
660
|
+
message: args.error.message,
|
|
661
|
+
},
|
|
662
|
+
completedAt: now,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const chain = await ctx.db.get(args.chainId);
|
|
667
|
+
if (!chain) {
|
|
668
|
+
throw new Error("Chain not found");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const resumeToken = generateResumeToken(args.chainId, chain.currentStep);
|
|
672
|
+
|
|
673
|
+
await ctx.db.patch(args.chainId, {
|
|
674
|
+
status: "failed",
|
|
675
|
+
error: {
|
|
676
|
+
stepId: args.stepId,
|
|
677
|
+
...args.error,
|
|
678
|
+
},
|
|
679
|
+
resumeToken,
|
|
680
|
+
canResume: true,
|
|
681
|
+
completedAt: now,
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
resumeToken,
|
|
686
|
+
partialResults: chain.results,
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Resume chain from failed step (public mutation)
|
|
693
|
+
*/
|
|
694
|
+
export const resumeChain = mutation({
|
|
695
|
+
args: {
|
|
696
|
+
resumeToken: v.string(),
|
|
697
|
+
overrides: v.optional(v.any()),
|
|
698
|
+
},
|
|
699
|
+
handler: async (ctx, args) => {
|
|
700
|
+
const chain = await ctx.db
|
|
701
|
+
.query("chains")
|
|
702
|
+
.withIndex("by_resumeToken", (q) => q.eq("resumeToken", args.resumeToken))
|
|
703
|
+
.first();
|
|
704
|
+
|
|
705
|
+
if (!chain) {
|
|
706
|
+
throw new Error("Invalid or expired resume token");
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (!chain.canResume) {
|
|
710
|
+
throw new Error("Chain cannot be resumed");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const executions = await ctx.db
|
|
714
|
+
.query("chainExecutions")
|
|
715
|
+
.withIndex("by_chainId_stepIndex", (q) =>
|
|
716
|
+
q.eq("chainId", chain._id).eq("stepIndex", chain.currentStep)
|
|
717
|
+
)
|
|
718
|
+
.collect();
|
|
719
|
+
|
|
720
|
+
for (const exec of executions) {
|
|
721
|
+
if (exec.status === "failed") {
|
|
722
|
+
await ctx.db.patch(exec._id, {
|
|
723
|
+
status: "pending",
|
|
724
|
+
error: undefined,
|
|
725
|
+
startedAt: undefined,
|
|
726
|
+
completedAt: undefined,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
await ctx.db.patch(chain._id, {
|
|
732
|
+
status: "pending",
|
|
733
|
+
error: undefined,
|
|
734
|
+
resumeToken: undefined,
|
|
735
|
+
canResume: false,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
chainId: chain._id,
|
|
740
|
+
resumeFromStep: chain.currentStep,
|
|
741
|
+
steps: chain.steps,
|
|
742
|
+
results: chain.results,
|
|
743
|
+
overrides: args.overrides,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Mark chain as completed
|
|
750
|
+
*/
|
|
751
|
+
export const completeChain = internalMutation({
|
|
752
|
+
args: {
|
|
753
|
+
chainId: v.id("chains"),
|
|
754
|
+
},
|
|
755
|
+
handler: async (ctx, args) => {
|
|
756
|
+
const now = Date.now();
|
|
757
|
+
const chain = await ctx.db.get(args.chainId);
|
|
758
|
+
|
|
759
|
+
if (!chain) {
|
|
760
|
+
throw new Error("Chain not found");
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
await ctx.db.patch(args.chainId, {
|
|
764
|
+
status: "completed",
|
|
765
|
+
canResume: false,
|
|
766
|
+
resumeToken: undefined,
|
|
767
|
+
completedAt: now,
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
success: true,
|
|
772
|
+
results: chain.results,
|
|
773
|
+
totalCostCents: chain.totalCostCents,
|
|
774
|
+
totalLatencyMs: chain.totalLatencyMs,
|
|
775
|
+
};
|
|
776
|
+
},
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Pause chain execution
|
|
781
|
+
*/
|
|
782
|
+
export const pauseChain = mutation({
|
|
783
|
+
args: {
|
|
784
|
+
chainId: v.id("chains"),
|
|
785
|
+
},
|
|
786
|
+
handler: async (ctx, args) => {
|
|
787
|
+
const chain = await ctx.db.get(args.chainId);
|
|
788
|
+
if (!chain) {
|
|
789
|
+
throw new Error("Chain not found");
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (chain.status !== "running") {
|
|
793
|
+
throw new Error("Can only pause running chains");
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const resumeToken = generateResumeToken(args.chainId, chain.currentStep);
|
|
797
|
+
|
|
798
|
+
await ctx.db.patch(args.chainId, {
|
|
799
|
+
status: "paused",
|
|
800
|
+
resumeToken,
|
|
801
|
+
canResume: true,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return { resumeToken };
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// ============================================
|
|
809
|
+
// CHAIN TEMPLATE MUTATIONS
|
|
810
|
+
// ============================================
|
|
811
|
+
|
|
812
|
+
export const saveChainTemplate = mutation({
|
|
813
|
+
args: {
|
|
814
|
+
id: v.optional(v.id("chainTemplates")),
|
|
815
|
+
workspaceId: v.id("workspaces"),
|
|
816
|
+
name: v.string(),
|
|
817
|
+
description: v.optional(v.string()),
|
|
818
|
+
inputs: v.optional(v.any()),
|
|
819
|
+
chain: v.array(v.any()),
|
|
820
|
+
},
|
|
821
|
+
handler: async (ctx, args) => {
|
|
822
|
+
const now = Date.now();
|
|
823
|
+
|
|
824
|
+
if (args.id) {
|
|
825
|
+
await ctx.db.patch(args.id, {
|
|
826
|
+
name: args.name,
|
|
827
|
+
description: args.description,
|
|
828
|
+
inputs: args.inputs,
|
|
829
|
+
chain: args.chain,
|
|
830
|
+
updatedAt: now,
|
|
831
|
+
});
|
|
832
|
+
return args.id;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return await ctx.db.insert("chainTemplates", {
|
|
836
|
+
workspaceId: args.workspaceId,
|
|
837
|
+
name: args.name,
|
|
838
|
+
description: args.description,
|
|
839
|
+
inputs: args.inputs,
|
|
840
|
+
chain: args.chain,
|
|
841
|
+
useCount: 0,
|
|
842
|
+
createdAt: now,
|
|
843
|
+
updatedAt: now,
|
|
844
|
+
});
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
export const deleteChainTemplate = mutation({
|
|
849
|
+
args: {
|
|
850
|
+
id: v.id("chainTemplates"),
|
|
851
|
+
},
|
|
852
|
+
handler: async (ctx, args) => {
|
|
853
|
+
await ctx.db.delete(args.id);
|
|
854
|
+
return { success: true };
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// ============================================
|
|
859
|
+
// QUERIES
|
|
860
|
+
// ============================================
|
|
861
|
+
|
|
862
|
+
export const getChain = query({
|
|
863
|
+
args: {
|
|
864
|
+
chainId: v.id("chains"),
|
|
865
|
+
},
|
|
866
|
+
handler: async (ctx, args) => {
|
|
867
|
+
const chain = await ctx.db.get(args.chainId);
|
|
868
|
+
if (!chain) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return {
|
|
873
|
+
id: chain._id,
|
|
874
|
+
status: chain.status,
|
|
875
|
+
currentStep: chain.currentStep,
|
|
876
|
+
totalSteps: chain.steps.length,
|
|
877
|
+
results: chain.results,
|
|
878
|
+
error: chain.error,
|
|
879
|
+
canResume: chain.canResume,
|
|
880
|
+
resumeToken: chain.resumeToken,
|
|
881
|
+
totalCostCents: chain.totalCostCents,
|
|
882
|
+
totalLatencyMs: chain.totalLatencyMs,
|
|
883
|
+
createdAt: chain.createdAt,
|
|
884
|
+
startedAt: chain.startedAt,
|
|
885
|
+
completedAt: chain.completedAt,
|
|
886
|
+
};
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
export const getChainTrace = query({
|
|
891
|
+
args: {
|
|
892
|
+
chainId: v.id("chains"),
|
|
893
|
+
},
|
|
894
|
+
handler: async (ctx, args) => {
|
|
895
|
+
const chain = await ctx.db.get(args.chainId);
|
|
896
|
+
if (!chain) {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const executions = await ctx.db
|
|
901
|
+
.query("chainExecutions")
|
|
902
|
+
.withIndex("by_chainId", (q) => q.eq("chainId", args.chainId))
|
|
903
|
+
.collect();
|
|
904
|
+
|
|
905
|
+
executions.sort((a, b) => a.stepIndex - b.stepIndex);
|
|
906
|
+
|
|
907
|
+
const trace = executions.map((exec) => ({
|
|
908
|
+
stepId: exec.stepId,
|
|
909
|
+
stepIndex: exec.stepIndex,
|
|
910
|
+
status: exec.status,
|
|
911
|
+
parallelGroup: exec.parallelGroup,
|
|
912
|
+
input: exec.input,
|
|
913
|
+
output: exec.output,
|
|
914
|
+
latencyMs: exec.latencyMs,
|
|
915
|
+
costCents: exec.costCents,
|
|
916
|
+
error: exec.error,
|
|
917
|
+
startedAt: exec.startedAt,
|
|
918
|
+
completedAt: exec.completedAt,
|
|
919
|
+
}));
|
|
920
|
+
|
|
921
|
+
const completedSteps = executions.filter((e) => e.status === "completed");
|
|
922
|
+
const tokensSaved = completedSteps.length > 1 ? (completedSteps.length - 1) * 400 : 0;
|
|
923
|
+
|
|
924
|
+
return {
|
|
925
|
+
chainId: chain._id,
|
|
926
|
+
workspaceId: chain.workspaceId,
|
|
927
|
+
status: chain.status,
|
|
928
|
+
steps: chain.steps,
|
|
929
|
+
currentStep: chain.currentStep,
|
|
930
|
+
results: chain.results,
|
|
931
|
+
error: chain.error,
|
|
932
|
+
trace,
|
|
933
|
+
totalCostCents: chain.totalCostCents,
|
|
934
|
+
totalLatencyMs: chain.totalLatencyMs,
|
|
935
|
+
tokensSaved,
|
|
936
|
+
canResume: chain.canResume,
|
|
937
|
+
resumeToken: chain.resumeToken,
|
|
938
|
+
createdAt: chain.createdAt,
|
|
939
|
+
startedAt: chain.startedAt,
|
|
940
|
+
completedAt: chain.completedAt,
|
|
941
|
+
};
|
|
942
|
+
},
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
export const listChains = query({
|
|
946
|
+
args: {
|
|
947
|
+
workspaceId: v.id("workspaces"),
|
|
948
|
+
status: v.optional(v.string()),
|
|
949
|
+
limit: v.optional(v.number()),
|
|
950
|
+
},
|
|
951
|
+
handler: async (ctx, args) => {
|
|
952
|
+
const limit = args.limit ?? 50;
|
|
953
|
+
|
|
954
|
+
const chains = await ctx.db
|
|
955
|
+
.query("chains")
|
|
956
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
|
|
957
|
+
.order("desc")
|
|
958
|
+
.take(limit);
|
|
959
|
+
|
|
960
|
+
let filtered = chains;
|
|
961
|
+
if (args.status && args.status !== "all") {
|
|
962
|
+
filtered = chains.filter((c) => c.status === args.status);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return filtered.map((chain) => ({
|
|
966
|
+
id: chain._id,
|
|
967
|
+
status: chain.status,
|
|
968
|
+
stepsCount: chain.steps.length,
|
|
969
|
+
currentStep: chain.currentStep,
|
|
970
|
+
totalCostCents: chain.totalCostCents,
|
|
971
|
+
totalLatencyMs: chain.totalLatencyMs,
|
|
972
|
+
error: chain.error,
|
|
973
|
+
canResume: chain.canResume,
|
|
974
|
+
createdAt: chain.createdAt,
|
|
975
|
+
completedAt: chain.completedAt,
|
|
976
|
+
}));
|
|
977
|
+
},
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
export const listChainTemplates = query({
|
|
981
|
+
args: {
|
|
982
|
+
workspaceId: v.id("workspaces"),
|
|
983
|
+
},
|
|
984
|
+
handler: async (ctx, args) => {
|
|
985
|
+
return await ctx.db
|
|
986
|
+
.query("chainTemplates")
|
|
987
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
|
|
988
|
+
.collect();
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
export const getChainTemplate = query({
|
|
993
|
+
args: {
|
|
994
|
+
workspaceId: v.id("workspaces"),
|
|
995
|
+
name: v.string(),
|
|
996
|
+
},
|
|
997
|
+
handler: async (ctx, args) => {
|
|
998
|
+
return await ctx.db
|
|
999
|
+
.query("chainTemplates")
|
|
1000
|
+
.withIndex("by_name", (q) =>
|
|
1001
|
+
q.eq("workspaceId", args.workspaceId).eq("name", args.name)
|
|
1002
|
+
)
|
|
1003
|
+
.first();
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
export const getChainStats = query({
|
|
1008
|
+
args: {
|
|
1009
|
+
workspaceId: v.id("workspaces"),
|
|
1010
|
+
},
|
|
1011
|
+
handler: async (ctx, args) => {
|
|
1012
|
+
const chains = await ctx.db
|
|
1013
|
+
.query("chains")
|
|
1014
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
|
|
1015
|
+
.collect();
|
|
1016
|
+
|
|
1017
|
+
const total = chains.length;
|
|
1018
|
+
const completed = chains.filter((c) => c.status === "completed").length;
|
|
1019
|
+
const failed = chains.filter((c) => c.status === "failed").length;
|
|
1020
|
+
const running = chains.filter((c) => c.status === "running").length;
|
|
1021
|
+
const paused = chains.filter((c) => c.status === "paused").length;
|
|
1022
|
+
|
|
1023
|
+
const totalCostCents = chains.reduce((acc, c) => acc + (c.totalCostCents || 0), 0);
|
|
1024
|
+
const totalLatencyMs = chains.reduce((acc, c) => acc + (c.totalLatencyMs || 0), 0);
|
|
1025
|
+
|
|
1026
|
+
const allExecutions = await Promise.all(
|
|
1027
|
+
chains.map((c) =>
|
|
1028
|
+
ctx.db
|
|
1029
|
+
.query("chainExecutions")
|
|
1030
|
+
.withIndex("by_chainId", (q) => q.eq("chainId", c._id))
|
|
1031
|
+
.collect()
|
|
1032
|
+
)
|
|
1033
|
+
);
|
|
1034
|
+
const totalSteps = allExecutions.flat().length;
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
total,
|
|
1038
|
+
completed,
|
|
1039
|
+
failed,
|
|
1040
|
+
running,
|
|
1041
|
+
paused,
|
|
1042
|
+
totalCostCents,
|
|
1043
|
+
totalLatencyMs,
|
|
1044
|
+
totalSteps,
|
|
1045
|
+
successRate: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// ============================================
|
|
1051
|
+
// ACTIONS (Orchestration Logic)
|
|
1052
|
+
// ============================================
|
|
1053
|
+
|
|
1054
|
+
export const runChain = action({
|
|
1055
|
+
args: {
|
|
1056
|
+
workspaceId: v.id("workspaces"),
|
|
1057
|
+
steps: v.array(v.any()),
|
|
1058
|
+
continueOnError: v.optional(v.boolean()),
|
|
1059
|
+
timeout: v.optional(v.number()),
|
|
1060
|
+
},
|
|
1061
|
+
handler: async (ctx, args): Promise<{
|
|
1062
|
+
success: boolean;
|
|
1063
|
+
chainId: Id<"chains">;
|
|
1064
|
+
results?: Record<string, unknown>;
|
|
1065
|
+
completedSteps?: string[];
|
|
1066
|
+
failedStep?: { id: string; error: string; code: string };
|
|
1067
|
+
partialResults?: Record<string, unknown>;
|
|
1068
|
+
canResume?: boolean;
|
|
1069
|
+
resumeToken?: string;
|
|
1070
|
+
totalCostCents?: number;
|
|
1071
|
+
totalLatencyMs?: number;
|
|
1072
|
+
}> => {
|
|
1073
|
+
const startTime = Date.now();
|
|
1074
|
+
const timeout = args.timeout || 30000;
|
|
1075
|
+
|
|
1076
|
+
const chainId = await ctx.runMutation(internal.chains.createChainInternal, {
|
|
1077
|
+
workspaceId: args.workspaceId,
|
|
1078
|
+
steps: args.steps,
|
|
1079
|
+
continueOnError: args.continueOnError,
|
|
1080
|
+
timeout: args.timeout,
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const completedSteps: string[] = [];
|
|
1084
|
+
let currentResults: Record<string, unknown> = {};
|
|
1085
|
+
|
|
1086
|
+
try {
|
|
1087
|
+
for (let i = 0; i < args.steps.length; i++) {
|
|
1088
|
+
if (Date.now() - startTime > timeout) {
|
|
1089
|
+
throw new Error("TIMEOUT: Chain execution exceeded timeout");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const step = args.steps[i] as ChainStep;
|
|
1093
|
+
|
|
1094
|
+
if (step.parallel && step.parallel.length > 0) {
|
|
1095
|
+
const parallelResults = await ctx.runAction(internal.chains.runParallelSteps, {
|
|
1096
|
+
chainId,
|
|
1097
|
+
steps: step.parallel,
|
|
1098
|
+
stepIndex: i,
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
for (const [stepId, result] of Object.entries(parallelResults)) {
|
|
1102
|
+
currentResults[stepId] = result;
|
|
1103
|
+
completedSteps.push(stepId);
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
await ctx.runMutation(internal.chains.executeStep, {
|
|
1107
|
+
chainId,
|
|
1108
|
+
stepId: step.id,
|
|
1109
|
+
stepIndex: i,
|
|
1110
|
+
input: step.params,
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const stepStartTime = Date.now();
|
|
1114
|
+
const result = await executeProviderCall(ctx, step);
|
|
1115
|
+
const latencyMs = Date.now() - stepStartTime;
|
|
1116
|
+
|
|
1117
|
+
await ctx.runMutation(internal.chains.completeStep, {
|
|
1118
|
+
chainId,
|
|
1119
|
+
stepId: step.id,
|
|
1120
|
+
output: result,
|
|
1121
|
+
latencyMs,
|
|
1122
|
+
costCents: result.costCents || 0,
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
currentResults[step.id] = result;
|
|
1126
|
+
completedSteps.push(step.id);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
await ctx.runMutation(internal.chains.advanceChain, { chainId });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const finalResult = await ctx.runMutation(internal.chains.completeChain, { chainId });
|
|
1133
|
+
|
|
1134
|
+
return {
|
|
1135
|
+
success: true,
|
|
1136
|
+
chainId,
|
|
1137
|
+
results: currentResults,
|
|
1138
|
+
completedSteps,
|
|
1139
|
+
totalCostCents: finalResult.totalCostCents,
|
|
1140
|
+
totalLatencyMs: finalResult.totalLatencyMs,
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1145
|
+
const errorCode = errorMessage.startsWith("TIMEOUT") ? "TIMEOUT" : "EXECUTION_ERROR";
|
|
1146
|
+
|
|
1147
|
+
const currentStep = args.steps[completedSteps.length] as ChainStep | undefined;
|
|
1148
|
+
const failedStepId = currentStep?.id || "unknown";
|
|
1149
|
+
|
|
1150
|
+
const failureResult = await ctx.runMutation(internal.chains.failChain, {
|
|
1151
|
+
chainId,
|
|
1152
|
+
stepId: failedStepId,
|
|
1153
|
+
error: {
|
|
1154
|
+
code: errorCode,
|
|
1155
|
+
message: errorMessage,
|
|
1156
|
+
},
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
return {
|
|
1160
|
+
success: false,
|
|
1161
|
+
chainId,
|
|
1162
|
+
completedSteps,
|
|
1163
|
+
failedStep: {
|
|
1164
|
+
id: failedStepId,
|
|
1165
|
+
error: errorMessage,
|
|
1166
|
+
code: errorCode,
|
|
1167
|
+
},
|
|
1168
|
+
partialResults: failureResult.partialResults,
|
|
1169
|
+
canResume: true,
|
|
1170
|
+
resumeToken: failureResult.resumeToken,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
export const runParallelSteps = internalAction({
|
|
1177
|
+
args: {
|
|
1178
|
+
chainId: v.id("chains"),
|
|
1179
|
+
steps: v.array(v.any()),
|
|
1180
|
+
stepIndex: v.number(),
|
|
1181
|
+
},
|
|
1182
|
+
handler: async (ctx, args): Promise<Record<string, unknown>> => {
|
|
1183
|
+
const results: Record<string, unknown> = {};
|
|
1184
|
+
|
|
1185
|
+
for (const step of args.steps as ChainStep[]) {
|
|
1186
|
+
await ctx.runMutation(internal.chains.executeStep, {
|
|
1187
|
+
chainId: args.chainId,
|
|
1188
|
+
stepId: step.id,
|
|
1189
|
+
stepIndex: args.stepIndex,
|
|
1190
|
+
input: step.params,
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const promises = (args.steps as ChainStep[]).map(async (step) => {
|
|
1195
|
+
const startTime = Date.now();
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
const result = await executeProviderCall(ctx, step);
|
|
1199
|
+
const latencyMs = Date.now() - startTime;
|
|
1200
|
+
|
|
1201
|
+
await ctx.runMutation(internal.chains.completeStep, {
|
|
1202
|
+
chainId: args.chainId,
|
|
1203
|
+
stepId: step.id,
|
|
1204
|
+
output: result,
|
|
1205
|
+
latencyMs,
|
|
1206
|
+
costCents: result.costCents || 0,
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
return { stepId: step.id, result };
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
throw new Error(`Step ${step.id} failed: ${error instanceof Error ? error.message : "Unknown"}`);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const settledResults = await Promise.all(promises);
|
|
1216
|
+
|
|
1217
|
+
for (const { stepId, result } of settledResults) {
|
|
1218
|
+
results[stepId] = result;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return results;
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// ============================================
|
|
1226
|
+
// HELPER: Execute provider call (placeholder)
|
|
1227
|
+
// ============================================
|
|
1228
|
+
|
|
1229
|
+
async function executeProviderCall(
|
|
1230
|
+
ctx: any,
|
|
1231
|
+
step: ChainStep
|
|
1232
|
+
): Promise<{ success: boolean; data?: unknown; costCents?: number }> {
|
|
1233
|
+
// Simulate latency
|
|
1234
|
+
await new Promise((resolve) => setTimeout(resolve, 50 + Math.random() * 100));
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
success: true,
|
|
1238
|
+
data: {
|
|
1239
|
+
stepId: step.id,
|
|
1240
|
+
provider: step.provider,
|
|
1241
|
+
action: step.action,
|
|
1242
|
+
params: step.params,
|
|
1243
|
+
mockResult: `Executed ${step.action} on ${step.provider}`,
|
|
1244
|
+
timestamp: Date.now(),
|
|
1245
|
+
},
|
|
1246
|
+
costCents: Math.ceil(Math.random() * 5),
|
|
1247
|
+
};
|
|
1248
|
+
}
|