@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/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, api } from "./_generated/api";
4
- import { Id, Doc } from "./_generated/dataModel";
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 have required fields
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 for each step
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 or parallel batch
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()), // Optional param overrides per step
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 (for long-running chains)
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
- let chainsQuery = ctx.db
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; // Default 30s
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
- // For MVP/testing, return mock success
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), // Mock 1-5 cents cost
1246
+ costCents: Math.ceil(Math.random() * 5),
1094
1247
  };
1095
1248
  }