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

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 (43) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +154 -473
  3. package/dist/component/_generated/api.d.ts +31 -0
  4. package/dist/component/_generated/component.d.ts +346 -2
  5. package/dist/component/convex.config.js +18 -0
  6. package/dist/component/engine/agents.d.ts +10 -0
  7. package/dist/component/engine/agents.js +21 -0
  8. package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
  9. package/dist/component/engine/awaitEventTimeout.js +81 -0
  10. package/dist/component/engine/delegation.d.ts +75 -0
  11. package/dist/component/engine/delegation.js +327 -0
  12. package/dist/component/engine/directory.d.ts +9 -0
  13. package/dist/component/engine/directory.js +45 -0
  14. package/dist/component/engine/durableJob.js +6 -6
  15. package/dist/component/engine/habits.d.ts +70 -0
  16. package/dist/component/engine/habits.js +218 -0
  17. package/dist/component/engine/mandates.d.ts +74 -0
  18. package/dist/component/engine/mandates.js +271 -0
  19. package/dist/component/engine/sandboxCallback.d.ts +36 -0
  20. package/dist/component/engine/sandboxCallback.js +259 -0
  21. package/dist/component/engine/sessions.d.ts +6 -2
  22. package/dist/component/engine/sessions.js +43 -7
  23. package/dist/component/engine/toolApprovals.js +1 -1
  24. package/dist/component/engine/vpConnection.d.ts +69 -0
  25. package/dist/component/engine/vpConnection.js +218 -0
  26. package/dist/component/engine/wakeup.d.ts +33 -0
  27. package/dist/component/engine/wakeup.js +228 -0
  28. package/dist/component/engine/workflows.d.ts +11 -0
  29. package/dist/component/engine/workflows.js +84 -0
  30. package/dist/component/schema.d.ts +365 -4
  31. package/dist/component/schema.js +345 -18
  32. package/dist/component/testFixtures/callerSteps.js +1 -1
  33. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
  34. package/dist/component/testFixtures/delegationDemoWorkflow.js +75 -0
  35. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
  36. package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
  37. package/dist/component/testFixtures/vpTool.d.ts +4 -0
  38. package/dist/component/testFixtures/vpTool.js +25 -0
  39. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
  40. package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
  41. package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
  42. package/dist/component/testFixtures/wakeupWork.js +21 -0
  43. package/package.json +17 -5
@@ -8,8 +8,8 @@ import { v } from "convex/values";
8
8
  // root, which only backs THIS repo's own dev/test deployment.
9
9
  //
10
10
  // SCOPE (T2-QUATER: durableJob only. T2-QUINQUIES, this pass: WIDENS the
11
- // boundary to the SESSIONS surface Hestia's EveVantage V4 conversation
12
- // thread needs — session start/turn, streamed reasoning/tool-call events,
11
+ // boundary to the SESSIONS surface a downstream conversation-thread
12
+ // consumer needs — session start/turn, streamed reasoning/tool-call events,
13
13
  // toolApprovals response, replay on reconnect, dynamic resolver — see
14
14
  // README "What's callable"). Still DELIBERATELY NARROW: `skills`,
15
15
  // `connections`, `subagents`, `habits`, `sandboxConfig`, `modelKeys`,
@@ -20,10 +20,8 @@ import { v } from "convex/values";
20
20
  // can read (component isolation: this package cannot read a consumer app's
21
21
  // OWN `agents` table across the component boundary).
22
22
  export default defineSchema({
23
- // durableJobs — GENERIC durable step-batch primitive (I1, SYNTHESE.md
24
- // §5/§7). See convex/engine/durableJob.ts (repo root) for the full
25
- // contract description; this table definition is byte-identical to the
26
- // one there.
23
+ // durableJobs — GENERIC durable step-batch primitive. See
24
+ // engine/durableJob.ts for the full contract description.
27
25
  durableJobs: defineTable({
28
26
  orgId: v.string(),
29
27
  kind: v.string(),
@@ -65,8 +63,13 @@ export default defineSchema({
65
63
  agents: defineTable({
66
64
  orgId: v.string(),
67
65
  name: v.string(),
66
+ description: v.optional(v.string()), // eve: agent-definition.ts:291
68
67
  instructions: v.string(),
69
68
  model: v.string(),
69
+ reasoningEffort: v.optional(v.union(v.literal("none"), v.literal("low"), v.literal("medium"), v.literal("high"))), // eve: agent-definition.ts:315-319
70
+ modelProvider: v.optional(v.union(v.literal("gateway"), v.literal("openrouter"))), // intentional divergence from Eve (bring-your-own-key); optional so it stays additive/backwards-compatible with rows created before this field existed
71
+ maxSubagents: v.optional(v.number()), // eve: agent-definition.ts:163-179, default 100
72
+ maxInputTokensPerSession: v.optional(v.union(v.number(), v.literal(false))), // eve: :195, false = unlimited
70
73
  status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
71
74
  createdBy: v.string(),
72
75
  createdAt: v.number(),
@@ -90,20 +93,19 @@ export default defineSchema({
90
93
  .index("by_org", ["orgId"])
91
94
  .index("by_org_agent", ["orgId", "agentId"])
92
95
  .index("by_org_agent_tool", ["orgId", "agentId", "toolName"]),
93
- // sessions — one conversation container per `sessions.start` call. This
94
- // pass deliberately does NOT tie a session to a `@convex-dev/workflow`
95
- // run (unlike the repo-root, app-level `convex/engine/sessions.ts`
96
- // prototype this was ported from) — long-task durability is already
97
- // covered by `engine/durableJob.ts`; a consumer that needs a durable
98
- // workflow driving turns can wire one on top using `durableJob.start`
99
- // with its own step function. This is a deliberate divergence ("notre
100
- // ligne") from the app-level prototype, narrowing the shipped surface to
101
- // what has a working component-level test harness behind it.
96
+ // sessions — one conversation container per `sessions.start` call.
97
+ // "intentional divergence from Eve": a session is piloted by a VERSIONED
98
+ // `@convex-dev/workflow` run (engine/workflows.ts: sessionWorkflowV1), so
99
+ // a turn survives a deploy. `workflowId`/`workflowVersion` are optional:
100
+ // rows written before that workflow existed stay valid without a backfill,
101
+ // and a consumer driving turns itself may leave them unset.
102
102
  sessions: defineTable({
103
103
  orgId: v.string(),
104
104
  agentId: v.id("agents"),
105
105
  status: v.union(v.literal("running"), v.literal("completed"), v.literal("cancelled")),
106
106
  createdBy: v.string(),
107
+ workflowId: v.optional(v.string()),
108
+ workflowVersion: v.optional(v.string()),
107
109
  createdAt: v.number(),
108
110
  updatedAt: v.number(),
109
111
  })
@@ -122,16 +124,341 @@ export default defineSchema({
122
124
  orgId: v.string(),
123
125
  sessionId: v.id("sessions"),
124
126
  name: v.union(v.literal("session.started"), v.literal("turn.started"), v.literal("turn.reasoning"), v.literal("turn.toolCall.requested"), v.literal("turn.toolCall.completed"),
125
- // "notre ligne" (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2):
127
+ // "intentional divergence from Eve" (VP k17ea6x3tht8xa3rxm139t23gx8ajkf3, defaut 2):
126
128
  // explicit terminal state for a one-turn approval REFUSAL, distinct
127
129
  // from "turn.toolCall.completed" (tool actually ran). Without this,
128
130
  // a denied tool call had no event of its own -- a consumer had no
129
131
  // way to tell "refused this turn" from "skipped, turn completed
130
- // anyway", which is the wrong default Hestia had to code around.
131
- v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved")),
132
+ // anyway", which is the wrong default a consumer had to code around.
133
+ v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved"),
134
+ // Blank event auto-inserted by `engine/awaitEventTimeout.fire`
135
+ // (workflow#177: Convex provides no native timeout for
136
+ // `step.awaitEvent`). See engine/awaitEventTimeout.ts.
137
+ v.literal("sandbox.timeout")),
132
138
  payload: v.optional(v.string()), // JSON-serialized, free contract per event name
133
139
  createdAt: v.number(),
134
140
  })
135
141
  .index("by_org", ["orgId"])
136
142
  .index("by_org_session", ["orgId", "sessionId"]),
143
+ // ---------------------------------------------------------------------
144
+ // WAKE-UP WITHOUT POLLING. See engine/wakeup.ts and
145
+ // engine/awaitEventTimeout.ts for the mechanism itself.
146
+ // ---------------------------------------------------------------------
147
+ // wakeableWork — GENERIC wake-up primitive. `enqueue` puts caller-supplied
148
+ // work (an action, referenced by an opaque function-handle string) into a
149
+ // dedicated `@convex-dev/workpool`; that pool's `onComplete` callback
150
+ // calls `workflow.sendEvent(...)` against the CALLER-supplied
151
+ // `workflowId` the instant the work resolves — success, failure, OR
152
+ // cancellation alike. Zero setInterval, zero cron.
153
+ wakeableWork: defineTable({
154
+ orgId: v.string(),
155
+ workflowId: v.string(), // target: the workflow parked on step.awaitEvent
156
+ awaitedEventName: v.string(), // event name that workflow is awaiting
157
+ workId: v.string(), // @convex-dev/workpool WorkId, opaque string
158
+ status: v.union(v.literal("pending"), v.literal("completed"), // work succeeded
159
+ v.literal("failed"), // work threw
160
+ v.literal("cancelled")),
161
+ resultSummary: v.optional(v.string()), // error message when status=="failed"
162
+ createdAt: v.number(),
163
+ updatedAt: v.number(),
164
+ })
165
+ .index("by_org", ["orgId"])
166
+ .index("by_org_workflow", ["orgId", "workflowId"]),
167
+ // awaitEventTimeouts — self-built timeout for `step.awaitEvent`
168
+ // (workflow#177: "Convex does not provide a native timeout for
169
+ // awaitEvent"). A scheduled mutation (`ctx.scheduler.runAfter`) fires a
170
+ // blank `sandbox.timeout` sessionEvent if the awaited event never arrives
171
+ // before `deadlineAt`.
172
+ awaitEventTimeouts: defineTable({
173
+ orgId: v.string(),
174
+ sessionId: v.id("sessions"),
175
+ awaitedEventName: v.string(),
176
+ deadlineAt: v.number(),
177
+ // optional: patched right after insert, once the scheduled function
178
+ // itself has been created (it needs this row's id as its argument).
179
+ scheduledFunctionId: v.optional(v.id("_scheduled_functions")),
180
+ firedAt: v.optional(v.number()),
181
+ cancelledAt: v.optional(v.number()),
182
+ })
183
+ .index("by_org", ["orgId"])
184
+ .index("by_org_session", ["orgId", "sessionId"]),
185
+ // wakeupDemoResults — TEST-ONLY fixture, see
186
+ // component/testFixtures/wakeupDemoWorkflow.ts. Not part of the public
187
+ // contract; a reference recipient workflow parked on `step.awaitEvent`
188
+ // patches `wokenAt`/`observedStatus` here ONLY after
189
+ // `engine/wakeup.onWorkComplete` has called `sendEvent` -- its presence
190
+ // is the test-observable proof that the wake mechanism (not a poll)
191
+ // resumed the workflow. Same discipline as `callerStepInvocations` above.
192
+ wakeupDemoResults: defineTable({
193
+ orgId: v.string(),
194
+ enqueuedAt: v.number(),
195
+ wokenAt: v.optional(v.number()),
196
+ observedStatus: v.optional(v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled"))),
197
+ observedResultSummary: v.optional(v.string()),
198
+ }).index("by_org", ["orgId"]),
199
+ // ---------------------------------------------------------------------
200
+ // MANDATES — "intentional divergence from Eve": bounded authority. See
201
+ // engine/mandates.ts and engine/wakeup.ts, where `enqueue` funds through
202
+ // `mandates.spendInternal` before any work can enter the wakeup pool.
203
+ // ---------------------------------------------------------------------
204
+ // mandates — a cap is a DATA row scoped by orgId + agentId + scope,
205
+ // never a constant read from code. `kind`/`rate`/`period`/`capacity` are
206
+ // the inline contract of @convex-dev/rate-limiter ("dynamic config"): the
207
+ // COST (`count`) is a WEIGHTED count (tokens/euros/calls), never a plain
208
+ // call counter. Changing a cap is a row mutation, never a deployment.
209
+ mandates: defineTable({
210
+ orgId: v.string(),
211
+ agentId: v.id("agents"),
212
+ scope: v.string(), // free-form business name (e.g. "llm-tokens") -- DATA, not a constant
213
+ kind: v.union(v.literal("token bucket"), v.literal("fixed window")),
214
+ rate: v.number(),
215
+ period: v.number(), // ms
216
+ capacity: v.optional(v.number()),
217
+ createdAt: v.number(),
218
+ updatedAt: v.number(),
219
+ })
220
+ .index("by_org", ["orgId"])
221
+ .index("by_org_agent", ["orgId", "agentId"])
222
+ .index("by_org_agent_scope", ["orgId", "agentId", "scope"]),
223
+ // approvalRequests — created whenever a mandate is exceeded. NEVER a
224
+ // silent bypass: an overrun always becomes a persisted HUMAN approval
225
+ // request, never a silent refusal with no trace.
226
+ approvalRequests: defineTable({
227
+ orgId: v.string(),
228
+ agentId: v.id("agents"),
229
+ scope: v.string(),
230
+ requestedCost: v.number(),
231
+ reason: v.string(),
232
+ status: v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")),
233
+ createdAt: v.number(),
234
+ resolvedAt: v.optional(v.number()),
235
+ resolvedBy: v.optional(v.string()),
236
+ })
237
+ .index("by_org", ["orgId"])
238
+ .index("by_org_agent", ["orgId", "agentId"])
239
+ .index("by_org_status", ["orgId", "status"]),
240
+ // ---------------------------------------------------------------------
241
+ // HABITS — "intentional divergence from Eve": a habit is a DYNAMIC cron
242
+ // per agent/org, which is what turns a chat into an employee. See
243
+ // engine/habits.ts; `crons` is a sub-component of THIS component,
244
+ // registered in component/convex.config.ts.
245
+ // ---------------------------------------------------------------------
246
+ // habits — a schedule is a DATA row scoped by orgId + agentId, never a
247
+ // constant read from a deployed `convex/crons.ts`. `cronExpression` is
248
+ // the DATA that drives the runtime cron registered via
249
+ // `@convex-dev/crons`; create/updateSchedule/enable/disable never
250
+ // redeploy anything.
251
+ habits: defineTable({
252
+ orgId: v.string(),
253
+ agentId: v.id("agents"),
254
+ name: v.string(),
255
+ cronExpression: v.string(),
256
+ prompt: v.string(),
257
+ enabled: v.boolean(),
258
+ // id of the runtime-registered @convex-dev/crons job, present only
259
+ // while `enabled` is true (cleared by `disable`, reassigned by
260
+ // `enable` / `updateSchedule` -- no redeploy involved in any transition).
261
+ cronJobId: v.optional(v.string()),
262
+ lastRunAt: v.optional(v.number()),
263
+ createdAt: v.number(),
264
+ updatedAt: v.number(),
265
+ })
266
+ .index("by_org", ["orgId"])
267
+ .index("by_org_agent", ["orgId", "agentId"]),
268
+ // habitRuns — proof of execution of a habit (MUST_PASS/MUST_BLOCK probe,
269
+ // component/habits.componentPackaging.test.ts). Every tick of the runtime
270
+ // cron registered via @convex-dev/crons writes a row here -- this is what
271
+ // makes "fires its first run without a deploy" PROVABLE rather than
272
+ // asserted on trust.
273
+ habitRuns: defineTable({
274
+ orgId: v.string(),
275
+ habitId: v.id("habits"),
276
+ agentId: v.id("agents"),
277
+ ranAt: v.number(),
278
+ })
279
+ .index("by_org", ["orgId"])
280
+ .index("by_org_habit", ["orgId", "habitId"]),
281
+ // ---------------------------------------------------------------------
282
+ // SANDBOX BRIDGE — the same pause/wake mechanism serves both the sandbox
283
+ // and agent-to-agent delegation. See engine/sandboxCallback.ts.
284
+ //
285
+ // BOUNDARY, and why it is where it is: the "use node" action
286
+ // that actually talks to `@vercel/sandbox` CANNOT live inside this
287
+ // component — the
288
+ // Convex CLI's own component bundler HARD-CRASHES on any "use node" file
289
+ // found in a child component directory ("use node directive is not
290
+ // supported in components", node_modules/convex/dist/cjs/cli/lib/
291
+ // components/definition/bundle.js). It stays app-level and calls INTO
292
+ // this component's public `engine.sandboxCallback.startRun` /
293
+ // `attachSandboxId` (and `engine.mandates.spend`) instead. Likewise, no
294
+ // official `@convex-dev/*` component ships an `http.ts` or reads
295
+ // `process.env` directly (measured: zero hits across all 7 installed
296
+ // `@convex-dev/*` component dist trees) — the signed callback's public
297
+ // HTTP route (`/sandbox/callback`) and its `SANDBOX_CALLBACK_SECRET`
298
+ // read + HMAC verification stay in the CONSUMER app's `convex/http.ts`,
299
+ // which delegates to this component's public
300
+ // `engine.sandboxCallback.processCallback` /
301
+ // `engine.sandboxCallback.deadLetterMalformed` ONLY after the signature
302
+ // has already checked out.
303
+ sandboxConfig: defineTable({
304
+ orgId: v.string(),
305
+ agentId: v.id("agents"),
306
+ provider: v.literal("vercel-sandbox"),
307
+ image: v.optional(v.string()),
308
+ timeoutSeconds: v.number(),
309
+ egressPolicy: v.union(v.literal("allow-all"), v.literal("deny-all-allowlist")),
310
+ allowlistDomains: v.array(v.string()),
311
+ createdAt: v.number(),
312
+ updatedAt: v.number(),
313
+ })
314
+ .index("by_org", ["orgId"])
315
+ .index("by_org_agent", ["orgId", "agentId"]),
316
+ sandboxRuns: defineTable({
317
+ orgId: v.string(),
318
+ workflowId: v.string(), // target: the workflow parked on step.awaitEvent
319
+ awaitedEventName: v.string(), // event name that workflow is awaiting
320
+ sandboxId: v.string(), // Vercel Sandbox id (opaque, from Sandbox.create())
321
+ status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("timedOut")),
322
+ resultSummary: v.optional(v.string()),
323
+ createdAt: v.number(),
324
+ updatedAt: v.number(),
325
+ })
326
+ .index("by_org", ["orgId"])
327
+ .index("by_org_workflow", ["orgId", "workflowId"])
328
+ .index("by_org_sandbox", ["orgId", "sandboxId"]),
329
+ sandboxCallbackDeliveries: defineTable({
330
+ orgId: v.string(),
331
+ sandboxRunId: v.id("sandboxRuns"),
332
+ deliveryId: v.string(), // caller-supplied, opaque, unique per attempt-family
333
+ outcome: v.union(v.literal("woke"), // first valid delivery for this id -- sendEvent fired
334
+ v.literal("duplicate")),
335
+ receivedAt: v.number(),
336
+ })
337
+ .index("by_org", ["orgId"])
338
+ .index("by_org_delivery", ["orgId", "deliveryId"])
339
+ .index("by_org_run", ["orgId", "sandboxRunId"]),
340
+ sandboxCallbackDeadLetters: defineTable({
341
+ orgId: v.optional(v.string()), // absent when the payload itself couldn't be parsed
342
+ deliveryId: v.string(),
343
+ reason: v.string(),
344
+ rawBody: v.string(),
345
+ createdAt: v.number(),
346
+ }).index("by_org", ["orgId"]),
347
+ // ---------------------------------------------------------------------
348
+ // DELEGATION — an agent delegating to another agent uses THE SAME
349
+ // pause/wake system as the sandbox bridge: one system to maintain, not
350
+ // two. See engine/delegation.ts and engine/vpConnection.ts: every
351
+ // delegation/clone path terminates in exactly one call to this
352
+ // component's own public `engine/wakeup.enqueue`, never in a second
353
+ // onComplete/sendEvent path.
354
+ // ---------------------------------------------------------------------
355
+ // subagents — declared subagents, RECURSIVE (the child is a full
356
+ // `agents` row with its own aggregate). Isolation is by construction
357
+ // (eve.dev/docs/subagents: "inherits nothing from the root's authored
358
+ // slots") -- this table ONLY declares the parent->child link and exposes
359
+ // it as a named tool; it never copies a single field out of the child's
360
+ // own skills/connections/toolApprovals onto the parent.
361
+ subagents: defineTable({
362
+ orgId: v.string(),
363
+ parentAgentId: v.id("agents"),
364
+ childAgentId: v.id("agents"), // full agent, own aggregate -- strict isolation
365
+ toolName: v.string(),
366
+ description: v.optional(v.string()),
367
+ enabled: v.boolean(),
368
+ createdAt: v.number(),
369
+ })
370
+ .index("by_org", ["orgId"])
371
+ .index("by_org_parent", ["orgId", "parentAgentId"])
372
+ .index("by_org_child", ["orgId", "childAgentId"])
373
+ // Build-time collision guard (engine/delegation.declareSubagent):
374
+ // exact-match lookup of an existing (parent, toolName) pair, no filter().
375
+ .index("by_org_parent_tool", ["orgId", "parentAgentId", "toolName"]),
376
+ // delegationRuns — one row per A->B delegation through a declared
377
+ // subagent (`subagents` table above). `workflowId`/`awaitedEventName` are
378
+ // the SAME two fields `engine/wakeup.enqueue` already threads through to
379
+ // `workflow.sendEvent` -- this table records WHICH delegation a given
380
+ // `wakeableWork` row belongs to; it never duplicates the wake mechanism
381
+ // itself (no second onComplete/sendEvent path exists anywhere in this
382
+ // engine).
383
+ delegationRuns: defineTable({
384
+ orgId: v.string(),
385
+ parentAgentId: v.id("agents"),
386
+ childAgentId: v.id("agents"),
387
+ toolName: v.string(),
388
+ workflowId: v.string(),
389
+ awaitedEventName: v.string(),
390
+ status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
391
+ resultSummary: v.optional(v.string()),
392
+ createdAt: v.number(),
393
+ updatedAt: v.number(),
394
+ })
395
+ .index("by_org", ["orgId"])
396
+ .index("by_org_parent", ["orgId", "parentAgentId"])
397
+ .index("by_org_workflow", ["orgId", "workflowId"])
398
+ .index("by_org_awaitedEvent", ["orgId", "awaitedEventName"]),
399
+ // agentCloneRuns — the built-in `agent` tool (eve.dev/docs/subagents: "the
400
+ // built-in `agent` tool clones the ROOT, root-only, NOT recursive -- it
401
+ // cannot call itself"). `viaAgentTool` is the recursion guard: a clone
402
+ // invocation started BY a clone invocation (viaAgentTool: true passed in)
403
+ // is refused at `engine/delegation.invokeAgentClone` BEFORE any row here
404
+ // is written -- so every row in this table by construction has
405
+ // `viaAgentTool === false` on entry. Wakes through the SAME
406
+ // `engine/wakeup.enqueue` mechanism as `delegationRuns`.
407
+ agentCloneRuns: defineTable({
408
+ orgId: v.string(),
409
+ agentId: v.id("agents"), // the root being cloned
410
+ workflowId: v.string(),
411
+ awaitedEventName: v.string(),
412
+ status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
413
+ resultSummary: v.optional(v.string()),
414
+ createdAt: v.number(),
415
+ updatedAt: v.number(),
416
+ })
417
+ .index("by_org", ["orgId"])
418
+ .index("by_org_agent", ["orgId", "agentId"]),
419
+ // vpConnections — teamwork. The
420
+ // four VantagePeers tools (memory, tasks, messages, notes) wired as a
421
+ // CONNECTION of the agent aggregate, an intentional divergence from Eve. 0..1 row per agent;
422
+ // absence means "use the defaults" (all four ON) -- see
423
+ // `engine/vpConnection.ts:defaultToggles`. DIRECT access: invocation
424
+ // goes through a caller-supplied Convex function handle
425
+ // (`createFunctionHandle`, same discipline as `wakeableWork.workId` /
426
+ // `durableJobs.stepFunctionHandle`), never a `fetch()` call.
427
+ vpConnections: defineTable({
428
+ orgId: v.string(),
429
+ agentId: v.id("agents"),
430
+ memoryEnabled: v.boolean(),
431
+ tasksEnabled: v.boolean(),
432
+ messagesEnabled: v.boolean(),
433
+ notesEnabled: v.boolean(),
434
+ createdAt: v.number(),
435
+ updatedAt: v.number(),
436
+ })
437
+ .index("by_org", ["orgId"])
438
+ .index("by_org_agent", ["orgId", "agentId"]),
439
+ // vpToolInvocations — audit trail of every VantagePeers tool call an
440
+ // agent made, ALWAYS written by `engine/vpConnection.invokeVpTool` in the
441
+ // SAME transaction family as the call itself.
442
+ vpToolInvocations: defineTable({
443
+ orgId: v.string(),
444
+ agentId: v.id("agents"),
445
+ tool: v.union(v.literal("memory"), v.literal("tasks"), v.literal("messages"), v.literal("notes")),
446
+ createdAt: v.number(),
447
+ })
448
+ .index("by_org", ["orgId"])
449
+ .index("by_org_agent", ["orgId", "agentId"]),
450
+ // vpTenantFixture — TEST-ONLY stand-in for "VantagePeers' own tenant
451
+ // table" (component/testFixtures/vpTool.ts). This engine never owns
452
+ // VantagePeers' schema (component isolation) -- this fixture plays the
453
+ // CALLER-side function a real `vpFunctionHandle` would point to, so
454
+ // `component/engine/vpConnection.test.ts` can prove a VP tool write
455
+ // lands in the invoking agent's own org tenant (and that a mismatched
456
+ // orgId is refused) without needing a second real Convex deployment in
457
+ // the test harness. Not part of the public contract.
458
+ vpTenantFixture: defineTable({
459
+ orgId: v.string(),
460
+ tool: v.union(v.literal("memory"), v.literal("tasks"), v.literal("messages"), v.literal("notes")),
461
+ argsJson: v.string(),
462
+ createdAt: v.number(),
463
+ }).index("by_org", ["orgId"]),
137
464
  });
@@ -56,7 +56,7 @@ export const alwaysSucceeds = internalMutation({
56
56
  return null;
57
57
  },
58
58
  });
59
- // Fails on every attempt -- used to prove "abandon after N échecs". Attempt
59
+ // Fails on every attempt -- used to prove abandonment after N failures. Attempt
60
60
  // count is observable via `getAttemptCount` even though every DB write this
61
61
  // function might otherwise make would be rolled back by the throw.
62
62
  export const alwaysFails = internalMutation({
@@ -0,0 +1,18 @@
1
+ import { WorkflowManager } from "@convex-dev/workflow";
2
+ export declare const workflow: WorkflowManager;
3
+ export declare const start: import("convex/server").RegisteredMutation<"public", {
4
+ orgId: string;
5
+ awaitedEventName: string;
6
+ }, Promise<{
7
+ workflowId: string;
8
+ }>>;
9
+ export declare const delegationDemoWorkflowV1: import("convex/server").RegisteredMutation<"internal", import("@convex-dev/workflow").WorkflowArgs<{
10
+ orgId: import("convex/values").VString<string, "required">;
11
+ awaitedEventName: import("convex/values").VString<string, "required">;
12
+ }>, import("@convex-dev/workflow").WorkflowId>;
13
+ export declare const recordDelegationResolvedByEventInternal: import("convex/server").RegisteredMutation<"internal", {
14
+ resultSummary?: string | undefined;
15
+ orgId: string;
16
+ status: "completed" | "cancelled" | "failed";
17
+ awaitedEventName: string;
18
+ }, Promise<null>>;
@@ -0,0 +1,75 @@
1
+ import { WorkflowManager } from "@convex-dev/workflow";
2
+ import { v } from "convex/values";
3
+ import { components, internal } from "../_generated/api.js";
4
+ import { internalMutation, mutation } from "../_generated/server.js";
5
+ import { requireOrgId } from "../lib/auth.js";
6
+ // testFixtures/delegationDemoWorkflow — TEST-ONLY reference PARENT-side
7
+ // recipient for delegation. This is exactly the "bespoke
8
+ // workflow" shape `engine/wakeup.ts`'s own comment describes ("any
9
+ // workflow -- a session, a durable job, or a bespoke one -- can be the wake
10
+ // target"): it parks on `step.awaitEvent` and, once
11
+ // `engine.wakeup.onWorkComplete` fires `sendEvent` (via
12
+ // `engine.delegation.delegateToSubagent` -> `engine.wakeup.enqueue`, the
13
+ // same public action this component ships), it patches the SAME `delegationRuns`
14
+ // row `delegateToSubagent` created -- proving A paused and was woken by
15
+ // B's completion, with zero polling.
16
+ export const workflow = new WorkflowManager(components.workflow);
17
+ export const start = mutation({
18
+ args: { orgId: v.string(), awaitedEventName: v.string() },
19
+ returns: v.object({ workflowId: v.string() }),
20
+ handler: async (ctx, args) => {
21
+ const orgId = requireOrgId(args.orgId);
22
+ const workflowId = await workflow.start(ctx, internal.testFixtures.delegationDemoWorkflow.delegationDemoWorkflowV1, { orgId, awaitedEventName: args.awaitedEventName });
23
+ return { workflowId };
24
+ },
25
+ });
26
+ export const delegationDemoWorkflowV1 = workflow.define({
27
+ args: { orgId: v.string(), awaitedEventName: v.string() },
28
+ returns: v.null(),
29
+ handler: async (step, args) => {
30
+ // A pauses here. Resumes ONLY when engine.wakeup.onWorkComplete calls
31
+ // sendEvent with this exact name -- no loop, no timer.
32
+ const result = await step.awaitEvent({
33
+ name: args.awaitedEventName,
34
+ validator: v.object({
35
+ status: v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
36
+ resultSummary: v.optional(v.string()),
37
+ }),
38
+ });
39
+ await step.runMutation(internal.testFixtures.delegationDemoWorkflow.recordDelegationResolvedByEventInternal, {
40
+ orgId: args.orgId,
41
+ awaitedEventName: args.awaitedEventName,
42
+ status: result.status,
43
+ resultSummary: result.resultSummary,
44
+ });
45
+ return null;
46
+ },
47
+ });
48
+ // recordDelegationResolvedByEventInternal — looks up the delegationRuns row
49
+ // created for THIS (orgId, awaitedEventName) pair via `by_org_awaitedEvent`
50
+ // (no `.filter()`) and patches it. Kept as one mutation (rather than two
51
+ // round trips from the workflow step) so the lookup and patch land in the
52
+ // same transaction.
53
+ export const recordDelegationResolvedByEventInternal = internalMutation({
54
+ args: {
55
+ orgId: v.string(),
56
+ awaitedEventName: v.string(),
57
+ status: v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
58
+ resultSummary: v.optional(v.string()),
59
+ },
60
+ returns: v.null(),
61
+ handler: async (ctx, args) => {
62
+ const row = await ctx.db
63
+ .query("delegationRuns")
64
+ .withIndex("by_org_awaitedEvent", (q) => q.eq("orgId", args.orgId).eq("awaitedEventName", args.awaitedEventName))
65
+ .first();
66
+ if (!row)
67
+ return null;
68
+ await ctx.db.patch(row._id, {
69
+ status: args.status,
70
+ resultSummary: args.resultSummary,
71
+ updatedAt: Date.now(),
72
+ });
73
+ return null;
74
+ },
75
+ });
@@ -0,0 +1,19 @@
1
+ import { WorkflowManager } from "@convex-dev/workflow";
2
+ import type { Id } from "../_generated/dataModel.js";
3
+ export declare const workflow: WorkflowManager;
4
+ export declare const start: import("convex/server").RegisteredMutation<"public", {
5
+ orgId: string;
6
+ awaitedEventName: string;
7
+ }, Promise<{
8
+ workflowId: string;
9
+ resultId: Id<"wakeupDemoResults">;
10
+ }>>;
11
+ export declare const recordWokenInternal: import("convex/server").RegisteredMutation<"internal", {
12
+ resultSummary?: string | undefined;
13
+ status: "completed" | "failed";
14
+ resultId: import("convex/values").GenericId<"wakeupDemoResults">;
15
+ }, Promise<null>>;
16
+ export declare const sandboxDemoWorkflowV1: import("convex/server").RegisteredMutation<"internal", import("@convex-dev/workflow").WorkflowArgs<{
17
+ resultId: import("convex/values").VId<import("convex/values").GenericId<"wakeupDemoResults">, "required">;
18
+ awaitedEventName: import("convex/values").VString<string, "required">;
19
+ }>, import("@convex-dev/workflow").WorkflowId>;
@@ -0,0 +1,69 @@
1
+ import { WorkflowManager } from "@convex-dev/workflow";
2
+ import { v } from "convex/values";
3
+ import { components, internal } from "../_generated/api.js";
4
+ import { internalMutation, mutation } from "../_generated/server.js";
5
+ import { requireOrgId } from "../lib/auth.js";
6
+ // testFixtures/sandboxDemoWorkflow — TEST-ONLY reference recipient for the
7
+ // sandbox bridge, same shape/role as testFixtures/wakeupDemoWorkflow.ts.
8
+ // A real consumer's
9
+ // own workflow parks on `step.awaitEvent({ name: <awaitedEventName passed
10
+ // to engine/sandboxCallback.startRun> })` and does nothing else. Proves
11
+ // (component/sandboxCallback.componentPackaging.test.ts) the ONLY way this
12
+ // resumes is a signed, deduplicated callback reaching
13
+ // `engine/sandboxCallback.processCallback` (or the self-built timeout) --
14
+ // no poll, no interval, no direct call.
15
+ export const workflow = new WorkflowManager(components.workflow);
16
+ export const start = mutation({
17
+ args: { orgId: v.string(), awaitedEventName: v.string() },
18
+ returns: v.object({
19
+ workflowId: v.string(),
20
+ resultId: v.id("wakeupDemoResults"),
21
+ }),
22
+ handler: async (ctx, args) => {
23
+ const orgId = requireOrgId(args.orgId);
24
+ // Reuses wakeupDemoResults -- same test-observation shape (enqueuedAt /
25
+ // wokenAt / observedStatus), decoupled from the wake-up-pool path by
26
+ // definition since nothing here touches wakeableWork.
27
+ const resultId = await ctx.db.insert("wakeupDemoResults", { orgId, enqueuedAt: Date.now() });
28
+ const workflowId = await workflow.start(ctx, internal.testFixtures.sandboxDemoWorkflow.sandboxDemoWorkflowV1, { resultId, awaitedEventName: args.awaitedEventName });
29
+ return { workflowId, resultId };
30
+ },
31
+ });
32
+ export const recordWokenInternal = internalMutation({
33
+ args: {
34
+ resultId: v.id("wakeupDemoResults"),
35
+ status: v.union(v.literal("completed"), v.literal("failed")),
36
+ resultSummary: v.optional(v.string()),
37
+ },
38
+ returns: v.null(),
39
+ handler: async (ctx, args) => {
40
+ await ctx.db.patch(args.resultId, {
41
+ wokenAt: Date.now(),
42
+ observedStatus: args.status,
43
+ observedResultSummary: args.resultSummary,
44
+ });
45
+ return null;
46
+ },
47
+ });
48
+ export const sandboxDemoWorkflowV1 = workflow.define({
49
+ args: { resultId: v.id("wakeupDemoResults"), awaitedEventName: v.string() },
50
+ returns: v.null(),
51
+ handler: async (step, args) => {
52
+ // Blocks here -- resumes ONLY when a signed, non-duplicate sandbox
53
+ // callback (or the self-built timeout) calls `workflow.sendEvent` with
54
+ // this exact name. No loop, no timer polling this workflow's state.
55
+ const result = await step.awaitEvent({
56
+ name: args.awaitedEventName,
57
+ validator: v.object({
58
+ status: v.union(v.literal("completed"), v.literal("failed")),
59
+ resultSummary: v.optional(v.string()),
60
+ }),
61
+ });
62
+ await step.runMutation(internal.testFixtures.sandboxDemoWorkflow.recordWokenInternal, {
63
+ resultId: args.resultId,
64
+ status: result.status,
65
+ resultSummary: result.resultSummary,
66
+ });
67
+ return null;
68
+ },
69
+ });
@@ -0,0 +1,4 @@
1
+ export declare const writeToTenant: import("convex/server").RegisteredMutation<"internal", {
2
+ orgId: string;
3
+ argsJson: string;
4
+ }, Promise<null>>;
@@ -0,0 +1,25 @@
1
+ import { v } from "convex/values";
2
+ import { internalMutation } from "../_generated/server.js";
3
+ // testFixtures/vpTool — stand-in for a VantagePeers-owned mutation reached
4
+ // via `createFunctionHandle` (same discipline as testFixtures/wakeupWork.ts
5
+ // and durableJob's caller-step fixtures). Ported from
6
+ // convex/testFixtures/vpTool.ts. This engine never owns VantagePeers'
7
+ // schema, so its own `vpTenantFixture` table plays the role of "VP's own
8
+ // tenant table" for `component/engine/vpConnection.test.ts`). Writes
9
+ // EXACTLY the orgId it was handed -- it does not derive one from
10
+ // `ctx.auth` -- so the caller (`engine.vpConnection.invokeVpTool`) is what
11
+ // determines tenant isolation, which is precisely the property under test.
12
+ export const writeToTenant = internalMutation({
13
+ args: { orgId: v.string(), argsJson: v.string() },
14
+ returns: v.null(),
15
+ handler: async (ctx, args) => {
16
+ const parsed = JSON.parse(args.argsJson);
17
+ await ctx.db.insert("vpTenantFixture", {
18
+ orgId: args.orgId,
19
+ tool: parsed.tool,
20
+ argsJson: args.argsJson,
21
+ createdAt: Date.now(),
22
+ });
23
+ return null;
24
+ },
25
+ });