@vantageos/agent-engine 0.1.0-alpha.5 → 0.1.0-alpha.6

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.
Files changed (39) hide show
  1. package/README.md +229 -40
  2. package/dist/component/_generated/api.d.ts +31 -0
  3. package/dist/component/_generated/component.d.ts +346 -2
  4. package/dist/component/convex.config.js +19 -0
  5. package/dist/component/engine/agents.d.ts +10 -0
  6. package/dist/component/engine/agents.js +21 -0
  7. package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
  8. package/dist/component/engine/awaitEventTimeout.js +82 -0
  9. package/dist/component/engine/delegation.d.ts +75 -0
  10. package/dist/component/engine/delegation.js +329 -0
  11. package/dist/component/engine/directory.d.ts +9 -0
  12. package/dist/component/engine/directory.js +45 -0
  13. package/dist/component/engine/habits.d.ts +70 -0
  14. package/dist/component/engine/habits.js +219 -0
  15. package/dist/component/engine/mandates.d.ts +74 -0
  16. package/dist/component/engine/mandates.js +272 -0
  17. package/dist/component/engine/sandboxCallback.d.ts +36 -0
  18. package/dist/component/engine/sandboxCallback.js +262 -0
  19. package/dist/component/engine/sessions.d.ts +6 -2
  20. package/dist/component/engine/sessions.js +42 -5
  21. package/dist/component/engine/vpConnection.d.ts +69 -0
  22. package/dist/component/engine/vpConnection.js +221 -0
  23. package/dist/component/engine/wakeup.d.ts +33 -0
  24. package/dist/component/engine/wakeup.js +230 -0
  25. package/dist/component/engine/workflows.d.ts +11 -0
  26. package/dist/component/engine/workflows.js +87 -0
  27. package/dist/component/schema.d.ts +365 -4
  28. package/dist/component/schema.js +353 -1
  29. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
  30. package/dist/component/testFixtures/delegationDemoWorkflow.js +76 -0
  31. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
  32. package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
  33. package/dist/component/testFixtures/vpTool.d.ts +4 -0
  34. package/dist/component/testFixtures/vpTool.js +25 -0
  35. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
  36. package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
  37. package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
  38. package/dist/component/testFixtures/wakeupWork.js +21 -0
  39. package/package.json +2 -1
@@ -23,10 +23,15 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
23
23
  agents: {
24
24
  create: FunctionReference<"mutation", "internal", {
25
25
  createdBy: string;
26
+ description?: string;
26
27
  instructions: string;
28
+ maxInputTokensPerSession?: number | false;
29
+ maxSubagents?: number;
27
30
  model: string;
31
+ modelProvider?: "gateway" | "openrouter";
28
32
  name: string;
29
33
  orgId: string;
34
+ reasoningEffort?: "none" | "low" | "medium" | "high";
30
35
  }, string, Name>;
31
36
  get: FunctionReference<"query", "internal", {
32
37
  agentId: string;
@@ -36,14 +41,88 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
36
41
  _id: string;
37
42
  createdAt: number;
38
43
  createdBy: string;
44
+ description?: string;
39
45
  instructions: string;
46
+ maxInputTokensPerSession?: number | false;
47
+ maxSubagents?: number;
40
48
  model: string;
49
+ modelProvider?: "gateway" | "openrouter";
41
50
  name: string;
42
51
  orgId: string;
52
+ reasoningEffort?: "none" | "low" | "medium" | "high";
43
53
  status: "draft" | "live" | "archived";
44
54
  updatedAt: number;
45
55
  } | null, Name>;
46
56
  };
57
+ awaitEventTimeout: {
58
+ cancel: FunctionReference<"mutation", "internal", {
59
+ orgId: string;
60
+ timeoutRowId: string;
61
+ }, null, Name>;
62
+ schedule: FunctionReference<"mutation", "internal", {
63
+ awaitedEventName: string;
64
+ orgId: string;
65
+ sessionId: string;
66
+ timeoutMs: number;
67
+ }, string, Name>;
68
+ };
69
+ delegation: {
70
+ declareSubagent: FunctionReference<"mutation", "internal", {
71
+ childAgentId: string;
72
+ description?: string;
73
+ orgId: string;
74
+ parentAgentId: string;
75
+ toolName: string;
76
+ }, string, Name>;
77
+ delegateToSubagent: FunctionReference<"action", "internal", {
78
+ awaitedEventName: string;
79
+ cost: number;
80
+ orgId: string;
81
+ payload?: string;
82
+ reason: string;
83
+ scope: string;
84
+ subagentId: string;
85
+ workFunctionHandle: string;
86
+ workflowId: string;
87
+ }, string, Name>;
88
+ invokeAgentClone: FunctionReference<"action", "internal", {
89
+ agentId: string;
90
+ awaitedEventName: string;
91
+ cost: number;
92
+ orgId: string;
93
+ payload?: string;
94
+ reason: string;
95
+ scope: string;
96
+ viaAgentTool: boolean;
97
+ workFunctionHandle: string;
98
+ workflowId: string;
99
+ }, string, Name>;
100
+ listSubagents: FunctionReference<"query", "internal", {
101
+ orgId: string;
102
+ parentAgentId: string;
103
+ }, Array<{
104
+ _creationTime: number;
105
+ _id: string;
106
+ childAgentId: string;
107
+ createdAt: number;
108
+ description?: string;
109
+ enabled: boolean;
110
+ orgId: string;
111
+ parentAgentId: string;
112
+ toolName: string;
113
+ }>, Name>;
114
+ };
115
+ directory: {
116
+ listByOrg: FunctionReference<"query", "internal", {
117
+ orgId: string;
118
+ }, Array<{
119
+ _id: string;
120
+ createdAt: number;
121
+ model: string;
122
+ name: string;
123
+ status: "draft" | "live" | "archived";
124
+ }>, Name>;
125
+ };
47
126
  durableJob: {
48
127
  cancel: FunctionReference<"mutation", "internal", {
49
128
  jobId: string;
@@ -67,6 +146,140 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
67
146
  totalSteps: number;
68
147
  }, string, Name>;
69
148
  };
149
+ habits: {
150
+ create: FunctionReference<"mutation", "internal", {
151
+ agentId: string;
152
+ cronExpression: string;
153
+ name: string;
154
+ orgId: string;
155
+ prompt: string;
156
+ }, string, Name>;
157
+ disable: FunctionReference<"mutation", "internal", {
158
+ habitId: string;
159
+ orgId: string;
160
+ }, null, Name>;
161
+ enable: FunctionReference<"mutation", "internal", {
162
+ habitId: string;
163
+ orgId: string;
164
+ }, null, Name>;
165
+ get: FunctionReference<"query", "internal", {
166
+ habitId: string;
167
+ orgId: string;
168
+ }, {
169
+ _creationTime: number;
170
+ _id: string;
171
+ agentId: string;
172
+ createdAt: number;
173
+ cronExpression: string;
174
+ cronJobId?: string;
175
+ enabled: boolean;
176
+ lastRunAt?: number;
177
+ name: string;
178
+ orgId: string;
179
+ prompt: string;
180
+ updatedAt: number;
181
+ } | null, Name>;
182
+ listForAgent: FunctionReference<"query", "internal", {
183
+ agentId: string;
184
+ orgId: string;
185
+ }, Array<{
186
+ _creationTime: number;
187
+ _id: string;
188
+ agentId: string;
189
+ createdAt: number;
190
+ cronExpression: string;
191
+ cronJobId?: string;
192
+ enabled: boolean;
193
+ lastRunAt?: number;
194
+ name: string;
195
+ orgId: string;
196
+ prompt: string;
197
+ updatedAt: number;
198
+ }>, Name>;
199
+ listRuns: FunctionReference<"query", "internal", {
200
+ habitId: string;
201
+ orgId: string;
202
+ }, Array<{
203
+ _creationTime: number;
204
+ _id: string;
205
+ agentId: string;
206
+ habitId: string;
207
+ orgId: string;
208
+ ranAt: number;
209
+ }>, Name>;
210
+ updateSchedule: FunctionReference<"mutation", "internal", {
211
+ cronExpression?: string;
212
+ habitId: string;
213
+ name?: string;
214
+ orgId: string;
215
+ prompt?: string;
216
+ }, null, Name>;
217
+ };
218
+ mandates: {
219
+ get: FunctionReference<"query", "internal", {
220
+ agentId: string;
221
+ orgId: string;
222
+ scope: string;
223
+ }, {
224
+ _creationTime: number;
225
+ _id: string;
226
+ agentId: string;
227
+ capacity?: number;
228
+ createdAt: number;
229
+ kind: "token bucket" | "fixed window";
230
+ orgId: string;
231
+ period: number;
232
+ rate: number;
233
+ scope: string;
234
+ updatedAt: number;
235
+ } | null, Name>;
236
+ listApprovals: FunctionReference<"query", "internal", {
237
+ agentId?: string;
238
+ orgId: string;
239
+ }, Array<{
240
+ _creationTime: number;
241
+ _id: string;
242
+ agentId: string;
243
+ createdAt: number;
244
+ orgId: string;
245
+ reason: string;
246
+ requestedCost: number;
247
+ resolvedAt?: number;
248
+ resolvedBy?: string;
249
+ scope: string;
250
+ status: "pending" | "approved" | "denied";
251
+ }>, Name>;
252
+ resolveApproval: FunctionReference<"mutation", "internal", {
253
+ approvalId: string;
254
+ decision: "approved" | "denied";
255
+ orgId: string;
256
+ resolvedBy: string;
257
+ }, null, Name>;
258
+ set: FunctionReference<"mutation", "internal", {
259
+ agentId: string;
260
+ capacity?: number;
261
+ kind: "token bucket" | "fixed window";
262
+ orgId: string;
263
+ period: number;
264
+ rate: number;
265
+ scope: string;
266
+ }, string, Name>;
267
+ spend: FunctionReference<"mutation", "internal", {
268
+ agentId: string;
269
+ cost: number;
270
+ orgId: string;
271
+ reason: string;
272
+ scope: string;
273
+ }, {
274
+ approvalId?: string;
275
+ ok: boolean;
276
+ }, Name>;
277
+ usageCount: FunctionReference<"query", "internal", {
278
+ agentId: string;
279
+ orgId: string;
280
+ scope: string;
281
+ }, number, Name>;
282
+ };
70
283
  resolvers: {
71
284
  resolveManifest: FunctionReference<"query", "internal", {
72
285
  orgId: string;
@@ -82,6 +295,41 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
82
295
  }>;
83
296
  }, Name>;
84
297
  };
298
+ sandboxCallback: {
299
+ attachSandboxId: FunctionReference<"mutation", "internal", {
300
+ orgId: string;
301
+ sandboxId: string;
302
+ sandboxRunId: string;
303
+ }, null, Name>;
304
+ deadLetterMalformed: FunctionReference<"mutation", "internal", {
305
+ deliveryId: string;
306
+ orgId?: string;
307
+ rawBody: string;
308
+ reason: string;
309
+ }, null, Name>;
310
+ getStatus: FunctionReference<"query", "internal", {
311
+ orgId: string;
312
+ sandboxRunId: string;
313
+ }, {
314
+ resultSummary?: string;
315
+ status: "pending" | "completed" | "failed" | "timedOut";
316
+ }, Name>;
317
+ processCallback: FunctionReference<"mutation", "internal", {
318
+ deliveryId: string;
319
+ orgId: string;
320
+ resultSummary?: string;
321
+ sandboxRunId: string;
322
+ status: "completed" | "failed";
323
+ }, {
324
+ outcome: "woke" | "duplicate" | "dead-lettered";
325
+ }, Name>;
326
+ startRun: FunctionReference<"mutation", "internal", {
327
+ awaitedEventName: string;
328
+ orgId: string;
329
+ timeoutMs: number;
330
+ workflowId: string;
331
+ }, string, Name>;
332
+ };
85
333
  sessions: {
86
334
  appendReasoning: FunctionReference<"mutation", "internal", {
87
335
  chunk: string;
@@ -105,7 +353,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
105
353
  _creationTime: number;
106
354
  _id: string;
107
355
  createdAt: number;
108
- name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
356
+ name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.toolCall.denied" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved" | "sandbox.timeout";
109
357
  payload?: string;
110
358
  }>, Name>;
111
359
  requestToolCall: FunctionReference<"mutation", "internal", {
@@ -127,7 +375,10 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
127
375
  agentId: string;
128
376
  createdBy: string;
129
377
  orgId: string;
130
- }, string, Name>;
378
+ }, {
379
+ sessionId: string;
380
+ workflowId: string;
381
+ }, Name>;
131
382
  };
132
383
  toolApprovals: {
133
384
  respond: FunctionReference<"mutation", "internal", {
@@ -138,5 +389,98 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
138
389
  updatePolicy?: "always" | "once" | "never";
139
390
  }, null, Name>;
140
391
  };
392
+ vpConnection: {
393
+ get: FunctionReference<"query", "internal", {
394
+ agentId: string;
395
+ orgId: string;
396
+ }, {
397
+ _creationTime: number;
398
+ _id: string;
399
+ agentId: string;
400
+ createdAt: number;
401
+ memoryEnabled: boolean;
402
+ messagesEnabled: boolean;
403
+ notesEnabled: boolean;
404
+ orgId: string;
405
+ tasksEnabled: boolean;
406
+ updatedAt: number;
407
+ } | null, Name>;
408
+ getToggles: FunctionReference<"query", "internal", {
409
+ agentId: string;
410
+ orgId: string;
411
+ }, {
412
+ memoryEnabled: boolean;
413
+ messagesEnabled: boolean;
414
+ notesEnabled: boolean;
415
+ tasksEnabled: boolean;
416
+ }, Name>;
417
+ invokeVpTool: FunctionReference<"action", "internal", {
418
+ agentId: string;
419
+ argsJson: string;
420
+ orgId: string;
421
+ tool: "memory" | "tasks" | "messages" | "notes";
422
+ vpFunctionHandle: string;
423
+ }, null, Name>;
424
+ setToggles: FunctionReference<"mutation", "internal", {
425
+ agentId: string;
426
+ memoryEnabled?: boolean;
427
+ messagesEnabled?: boolean;
428
+ notesEnabled?: boolean;
429
+ orgId: string;
430
+ tasksEnabled?: boolean;
431
+ }, string, Name>;
432
+ };
433
+ wakeup: {
434
+ cancel: FunctionReference<"mutation", "internal", {
435
+ orgId: string;
436
+ wakeableWorkId: string;
437
+ }, null, Name>;
438
+ enqueue: FunctionReference<"action", "internal", {
439
+ agentId: string;
440
+ awaitedEventName: string;
441
+ cost: number;
442
+ orgId: string;
443
+ payload?: string;
444
+ reason: string;
445
+ scope: string;
446
+ workFunctionHandle: string;
447
+ workflowId: string;
448
+ }, string, Name>;
449
+ getStatus: FunctionReference<"query", "internal", {
450
+ orgId: string;
451
+ wakeableWorkId: string;
452
+ }, {
453
+ resultSummary?: string;
454
+ status: "pending" | "completed" | "failed" | "cancelled";
455
+ }, Name>;
456
+ };
457
+ };
458
+ testFixtures: {
459
+ delegationDemoWorkflow: {
460
+ start: FunctionReference<"mutation", "internal", {
461
+ awaitedEventName: string;
462
+ orgId: string;
463
+ }, {
464
+ workflowId: string;
465
+ }, Name>;
466
+ };
467
+ sandboxDemoWorkflow: {
468
+ start: FunctionReference<"mutation", "internal", {
469
+ awaitedEventName: string;
470
+ orgId: string;
471
+ }, {
472
+ resultId: string;
473
+ workflowId: string;
474
+ }, Name>;
475
+ };
476
+ wakeupDemoWorkflow: {
477
+ start: FunctionReference<"mutation", "internal", {
478
+ awaitedEventName: string;
479
+ orgId: string;
480
+ }, {
481
+ resultId: string;
482
+ workflowId: string;
483
+ }, Name>;
484
+ };
141
485
  };
142
486
  };
@@ -2,6 +2,9 @@ import { defineComponent } from "convex/server";
2
2
  import workflow from "@convex-dev/workflow/convex.config";
3
3
  import workpool from "@convex-dev/workpool/convex.config";
4
4
  import agent from "@convex-dev/agent/convex.config";
5
+ import rateLimiter from "@convex-dev/rate-limiter/convex.config";
6
+ import shardedCounter from "@convex-dev/sharded-counter/convex.config";
7
+ import crons from "@convex-dev/crons/convex.config";
5
8
  // @vantageos/agent-engine — Convex COMPONENT definition (pattern
6
9
  // `@convex-dev/*`, verified against the published shape of
7
10
  // `@convex-dev/workflow` — `defineComponent(name)` + a `convex.config.ts`
@@ -32,9 +35,25 @@ import agent from "@convex-dev/agent/convex.config";
32
35
  // awaitEventTimeouts, wakeableWork) is deliberately NOT yet re-hosted
33
36
  // here — porting those without a working component-level test harness
34
37
  // behind each of them would violate Day 128 (never ship unverified).
38
+ // C3 (this pass): mandates ("notre ligne", autorité bornée) are now hosted
39
+ // under this component boundary, following the exact same trio the
40
+ // repo-root prototype used (I0 §3, `convex/convex.config.ts`) -- official
41
+ // `@convex-dev/*` libraries only, NO canary/alpha dependency for a
42
+ // guardrail: `rate-limiter` for the fails-closed spend gate itself,
43
+ // `sharded-counter` for the usage rollup. See component/engine/mandates.ts.
44
+ // C4 (this pass): habits ("notre ligne", un cron DYNAMIQUE par agent/org)
45
+ // are now hosted under this component boundary, ported byte-identical from
46
+ // the repo-root prototype (convex/habits.ts). `crons` is a sub-component of
47
+ // THIS component -- create/updateSchedule/enable/disable register or tear
48
+ // down a runtime cron job via `@convex-dev/crons` inside the SAME Convex
49
+ // mutation transaction that writes the `habits` row, so no habit schedule
50
+ // change ever requires a redeploy. See component/engine/habits.ts.
35
51
  const component = defineComponent("agentEngine");
36
52
  component.use(workflow);
37
53
  component.use(workpool, { name: "agentTurnPool" });
38
54
  component.use(workpool, { name: "wakeupPool" });
39
55
  component.use(agent);
56
+ component.use(rateLimiter);
57
+ component.use(shardedCounter);
58
+ component.use(crons);
40
59
  export default component;
@@ -1,4 +1,9 @@
1
1
  export declare const create: import("convex/server").RegisteredMutation<"public", {
2
+ description?: string | undefined;
3
+ reasoningEffort?: "none" | "low" | "medium" | "high" | undefined;
4
+ modelProvider?: "gateway" | "openrouter" | undefined;
5
+ maxSubagents?: number | undefined;
6
+ maxInputTokensPerSession?: number | false | undefined;
2
7
  name: string;
3
8
  orgId: string;
4
9
  instructions: string;
@@ -11,6 +16,11 @@ export declare const get: import("convex/server").RegisteredQuery<"public", {
11
16
  }, Promise<{
12
17
  _id: import("convex/values").GenericId<"agents">;
13
18
  _creationTime: number;
19
+ description?: string | undefined;
20
+ reasoningEffort?: "none" | "low" | "medium" | "high" | undefined;
21
+ modelProvider?: "gateway" | "openrouter" | undefined;
22
+ maxSubagents?: number | undefined;
23
+ maxInputTokensPerSession?: number | false | undefined;
14
24
  name: string;
15
25
  orgId: string;
16
26
  status: "draft" | "live" | "archived";
@@ -8,13 +8,21 @@ import { requireOrgId } from "../lib/auth.js";
8
8
  // `create`/`get`, no `update`/`list`/skills/connections — the full agent
9
9
  // aggregate CRUD stays out of this component boundary for this pass (see
10
10
  // component/schema.ts SCOPE note).
11
+ const reasoningEffort = v.union(v.literal("none"), v.literal("low"), v.literal("medium"), v.literal("high"));
12
+ const modelProvider = v.union(v.literal("gateway"), v.literal("openrouter"));
13
+ const maxInputTokensPerSession = v.union(v.number(), v.literal(false));
11
14
  const agentDoc = v.object({
12
15
  _id: v.id("agents"),
13
16
  _creationTime: v.number(),
14
17
  orgId: v.string(),
15
18
  name: v.string(),
19
+ description: v.optional(v.string()),
16
20
  instructions: v.string(),
17
21
  model: v.string(),
22
+ reasoningEffort: v.optional(reasoningEffort),
23
+ modelProvider: v.optional(modelProvider),
24
+ maxSubagents: v.optional(v.number()),
25
+ maxInputTokensPerSession: v.optional(maxInputTokensPerSession),
18
26
  status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
19
27
  createdBy: v.string(),
20
28
  createdAt: v.number(),
@@ -25,8 +33,13 @@ export const create = mutation({
25
33
  orgId: v.string(),
26
34
  createdBy: v.string(),
27
35
  name: v.string(),
36
+ description: v.optional(v.string()),
28
37
  instructions: v.string(),
29
38
  model: v.string(),
39
+ reasoningEffort: v.optional(reasoningEffort),
40
+ modelProvider: v.optional(modelProvider),
41
+ maxSubagents: v.optional(v.number()),
42
+ maxInputTokensPerSession: v.optional(maxInputTokensPerSession),
30
43
  },
31
44
  returns: v.id("agents"),
32
45
  handler: async (ctx, args) => {
@@ -43,12 +56,20 @@ export const create = mutation({
43
56
  if (args.model.trim().length === 0) {
44
57
  throw new Error("Validation: model must not be empty");
45
58
  }
59
+ if (args.maxSubagents !== undefined && args.maxSubagents < 0) {
60
+ throw new Error("Validation: maxSubagents must not be negative");
61
+ }
46
62
  const now = Date.now();
47
63
  return await ctx.db.insert("agents", {
48
64
  orgId,
49
65
  name: args.name,
66
+ description: args.description,
50
67
  instructions: args.instructions,
51
68
  model: args.model,
69
+ reasoningEffort: args.reasoningEffort,
70
+ modelProvider: args.modelProvider,
71
+ maxSubagents: args.maxSubagents,
72
+ maxInputTokensPerSession: args.maxInputTokensPerSession,
52
73
  status: "live",
53
74
  createdBy: args.createdBy,
54
75
  createdAt: now,
@@ -0,0 +1,13 @@
1
+ export declare const schedule: import("convex/server").RegisteredMutation<"public", {
2
+ orgId: string;
3
+ sessionId: import("convex/values").GenericId<"sessions">;
4
+ awaitedEventName: string;
5
+ timeoutMs: number;
6
+ }, Promise<import("convex/values").GenericId<"awaitEventTimeouts">>>;
7
+ export declare const cancel: import("convex/server").RegisteredMutation<"public", {
8
+ orgId: string;
9
+ timeoutRowId: import("convex/values").GenericId<"awaitEventTimeouts">;
10
+ }, Promise<null>>;
11
+ export declare const fire: import("convex/server").RegisteredMutation<"internal", {
12
+ timeoutRowId: import("convex/values").GenericId<"awaitEventTimeouts">;
13
+ }, Promise<null>>;
@@ -0,0 +1,82 @@
1
+ import { v } from "convex/values";
2
+ import { internal } from "../_generated/api.js";
3
+ import { internalMutation, mutation } from "../_generated/server.js";
4
+ import { assertSameOrg, requireOrgId } from "../lib/auth.js";
5
+ // engine/awaitEventTimeout — self-built timeout for step.awaitEvent
6
+ // (workflow#177: "Convex does not provide a native timeout for
7
+ // awaitEvent"). PORTED from convex/engine/awaitEventTimeout.ts (repo-root
8
+ // prototype) into the component boundary (C2). We schedule a mutation that
9
+ // fires a "blank" `sandbox.timeout` sessionEvent at the deadline; the
10
+ // workflow's `awaitEvent` call is written to also accept this event name so
11
+ // it wakes up even if the awaited event never arrives. Pattern:
12
+ // maintainer's posted code in workflow#177, adapted to our sessionEvents
13
+ // table.
14
+ //
15
+ // C2 MIGRATION NOTE ("notre ligne"): orgId is an EXPLICIT ARGUMENT on
16
+ // every public function, never derived from `ctx.auth` -- auth does NOT
17
+ // propagate across a component boundary (component/lib/auth.ts). Also:
18
+ // `sessionEvents.name`'s union in component/schema.ts did not previously
19
+ // include `sandbox.timeout` -- it has been added there (and mirrored in
20
+ // `engine/sessions.listEvents`'s return validator) as part of this
21
+ // migration, otherwise `fire`'s insert below would not compile under the
22
+ // strict build.
23
+ export const schedule = mutation({
24
+ args: {
25
+ orgId: v.string(),
26
+ sessionId: v.id("sessions"),
27
+ awaitedEventName: v.string(),
28
+ timeoutMs: v.number(),
29
+ },
30
+ returns: v.id("awaitEventTimeouts"),
31
+ handler: async (ctx, args) => {
32
+ const orgId = requireOrgId(args.orgId);
33
+ const session = await ctx.db.get(args.sessionId);
34
+ assertSameOrg(orgId, session, "Session not found");
35
+ if (args.timeoutMs <= 0) {
36
+ throw new Error("Validation: timeoutMs must be > 0");
37
+ }
38
+ const timeoutRowId = await ctx.db.insert("awaitEventTimeouts", {
39
+ orgId,
40
+ sessionId: args.sessionId,
41
+ awaitedEventName: args.awaitedEventName,
42
+ deadlineAt: Date.now() + args.timeoutMs,
43
+ });
44
+ const scheduledFunctionId = await ctx.scheduler.runAfter(args.timeoutMs, internal.engine.awaitEventTimeout.fire, { timeoutRowId });
45
+ await ctx.db.patch(timeoutRowId, { scheduledFunctionId });
46
+ return timeoutRowId;
47
+ },
48
+ });
49
+ export const cancel = mutation({
50
+ args: { orgId: v.string(), timeoutRowId: v.id("awaitEventTimeouts") },
51
+ returns: v.null(),
52
+ handler: async (ctx, args) => {
53
+ const orgId = requireOrgId(args.orgId);
54
+ const row = await ctx.db.get(args.timeoutRowId);
55
+ assertSameOrg(orgId, row, "Timeout not found");
56
+ if (!row.firedAt && !row.cancelledAt && row.scheduledFunctionId) {
57
+ await ctx.scheduler.cancel(row.scheduledFunctionId);
58
+ await ctx.db.patch(args.timeoutRowId, { cancelledAt: Date.now() });
59
+ }
60
+ return null;
61
+ },
62
+ });
63
+ // internal — invoked only by the scheduler, never by a client.
64
+ export const fire = internalMutation({
65
+ args: { timeoutRowId: v.id("awaitEventTimeouts") },
66
+ returns: v.null(),
67
+ handler: async (ctx, args) => {
68
+ const row = await ctx.db.get(args.timeoutRowId);
69
+ if (!row || row.firedAt || row.cancelledAt) {
70
+ return null; // already resolved by the real event or by a prior fire
71
+ }
72
+ await ctx.db.patch(args.timeoutRowId, { firedAt: Date.now() });
73
+ await ctx.db.insert("sessionEvents", {
74
+ orgId: row.orgId,
75
+ sessionId: row.sessionId,
76
+ name: "sandbox.timeout",
77
+ payload: JSON.stringify({ awaitedEventName: row.awaitedEventName }),
78
+ createdAt: Date.now(),
79
+ });
80
+ return null;
81
+ },
82
+ });