@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.
- package/README.md +229 -40
- package/dist/component/_generated/api.d.ts +31 -0
- package/dist/component/_generated/component.d.ts +346 -2
- package/dist/component/convex.config.js +19 -0
- package/dist/component/engine/agents.d.ts +10 -0
- package/dist/component/engine/agents.js +21 -0
- package/dist/component/engine/awaitEventTimeout.d.ts +13 -0
- package/dist/component/engine/awaitEventTimeout.js +82 -0
- package/dist/component/engine/delegation.d.ts +75 -0
- package/dist/component/engine/delegation.js +329 -0
- package/dist/component/engine/directory.d.ts +9 -0
- package/dist/component/engine/directory.js +45 -0
- package/dist/component/engine/habits.d.ts +70 -0
- package/dist/component/engine/habits.js +219 -0
- package/dist/component/engine/mandates.d.ts +74 -0
- package/dist/component/engine/mandates.js +272 -0
- package/dist/component/engine/sandboxCallback.d.ts +36 -0
- package/dist/component/engine/sandboxCallback.js +262 -0
- package/dist/component/engine/sessions.d.ts +6 -2
- package/dist/component/engine/sessions.js +42 -5
- package/dist/component/engine/vpConnection.d.ts +69 -0
- package/dist/component/engine/vpConnection.js +221 -0
- package/dist/component/engine/wakeup.d.ts +33 -0
- package/dist/component/engine/wakeup.js +230 -0
- package/dist/component/engine/workflows.d.ts +11 -0
- package/dist/component/engine/workflows.js +87 -0
- package/dist/component/schema.d.ts +365 -4
- package/dist/component/schema.js +353 -1
- package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
- package/dist/component/testFixtures/delegationDemoWorkflow.js +76 -0
- package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +19 -0
- package/dist/component/testFixtures/sandboxDemoWorkflow.js +69 -0
- package/dist/component/testFixtures/vpTool.d.ts +4 -0
- package/dist/component/testFixtures/vpTool.js +25 -0
- package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +19 -0
- package/dist/component/testFixtures/wakeupDemoWorkflow.js +77 -0
- package/dist/component/testFixtures/wakeupWork.d.ts +8 -0
- package/dist/component/testFixtures/wakeupWork.js +21 -0
- package/package.json +2 -1
package/dist/component/schema.js
CHANGED
|
@@ -65,8 +65,13 @@ export default defineSchema({
|
|
|
65
65
|
agents: defineTable({
|
|
66
66
|
orgId: v.string(),
|
|
67
67
|
name: v.string(),
|
|
68
|
+
description: v.optional(v.string()), // eve: agent-definition.ts:291
|
|
68
69
|
instructions: v.string(),
|
|
69
70
|
model: v.string(),
|
|
71
|
+
reasoningEffort: v.optional(v.union(v.literal("none"), v.literal("low"), v.literal("medium"), v.literal("high"))), // eve: agent-definition.ts:315-319
|
|
72
|
+
modelProvider: v.optional(v.union(v.literal("gateway"), v.literal("openrouter"))), // notre ligne (BYOK, mvp §2.3); optional here to stay additive/backwards-compatible with rows created before this field existed
|
|
73
|
+
maxSubagents: v.optional(v.number()), // eve: agent-definition.ts:163-179, défaut 100
|
|
74
|
+
maxInputTokensPerSession: v.optional(v.union(v.number(), v.literal(false))), // eve: :195, false = illimité
|
|
70
75
|
status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
|
|
71
76
|
createdBy: v.string(),
|
|
72
77
|
createdAt: v.number(),
|
|
@@ -99,11 +104,19 @@ export default defineSchema({
|
|
|
99
104
|
// with its own step function. This is a deliberate divergence ("notre
|
|
100
105
|
// ligne") from the app-level prototype, narrowing the shipped surface to
|
|
101
106
|
// what has a working component-level test harness behind it.
|
|
107
|
+
// C1.5 ("notre ligne", measured trou fixed): sessions ARE now piloted by
|
|
108
|
+
// a versioned `@convex-dev/workflow` run (component/engine/workflows.ts:
|
|
109
|
+
// sessionWorkflowV1) -- the divergence note above (T2-QUINQUIES,
|
|
110
|
+
// "deliberately does NOT tie a session to a workflow run") is retired.
|
|
111
|
+
// `workflowId`/`workflowVersion` are optional so pre-C1.5 rows (written
|
|
112
|
+
// before this migration) remain valid without a backfill.
|
|
102
113
|
sessions: defineTable({
|
|
103
114
|
orgId: v.string(),
|
|
104
115
|
agentId: v.id("agents"),
|
|
105
116
|
status: v.union(v.literal("running"), v.literal("completed"), v.literal("cancelled")),
|
|
106
117
|
createdBy: v.string(),
|
|
118
|
+
workflowId: v.optional(v.string()),
|
|
119
|
+
workflowVersion: v.optional(v.string()),
|
|
107
120
|
createdAt: v.number(),
|
|
108
121
|
updatedAt: v.number(),
|
|
109
122
|
})
|
|
@@ -128,10 +141,349 @@ export default defineSchema({
|
|
|
128
141
|
// a denied tool call had no event of its own -- a consumer had no
|
|
129
142
|
// way to tell "refused this turn" from "skipped, turn completed
|
|
130
143
|
// 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")
|
|
144
|
+
v.literal("turn.toolCall.denied"), v.literal("turn.completed"), v.literal("tool.approval.requested"), v.literal("tool.approval.resolved"),
|
|
145
|
+
// C2 — blank event auto-inserted by `engine/awaitEventTimeout.fire`
|
|
146
|
+
// (workflow#177: Convex provides no native timeout for
|
|
147
|
+
// `step.awaitEvent`). See component/engine/awaitEventTimeout.ts.
|
|
148
|
+
v.literal("sandbox.timeout")),
|
|
132
149
|
payload: v.optional(v.string()), // JSON-serialized, free contract per event name
|
|
133
150
|
createdAt: v.number(),
|
|
134
151
|
})
|
|
135
152
|
.index("by_org", ["orgId"])
|
|
136
153
|
.index("by_org_session", ["orgId", "sessionId"]),
|
|
154
|
+
// ---------------------------------------------------------------------
|
|
155
|
+
// C2 — WAKE-UP WITHOUT POLLING (I2, SYNTHESE.md §5). Ported from
|
|
156
|
+
// convex/schema.ts (repo-root prototype); table shapes are byte-identical
|
|
157
|
+
// to their app-level source. See component/engine/wakeup.ts and
|
|
158
|
+
// component/engine/awaitEventTimeout.ts for the mechanism itself.
|
|
159
|
+
// ---------------------------------------------------------------------
|
|
160
|
+
// wakeableWork — GENERIC wake-up primitive. `enqueue` puts caller-supplied
|
|
161
|
+
// work (an action, referenced by an opaque function-handle string) into a
|
|
162
|
+
// dedicated `@convex-dev/workpool`; that pool's `onComplete` callback
|
|
163
|
+
// calls `workflow.sendEvent(...)` against the CALLER-supplied
|
|
164
|
+
// `workflowId` the instant the work resolves — success, failure, OR
|
|
165
|
+
// cancellation alike. Zero setInterval, zero cron.
|
|
166
|
+
wakeableWork: defineTable({
|
|
167
|
+
orgId: v.string(),
|
|
168
|
+
workflowId: v.string(), // target: the workflow parked on step.awaitEvent
|
|
169
|
+
awaitedEventName: v.string(), // event name that workflow is awaiting
|
|
170
|
+
workId: v.string(), // @convex-dev/workpool WorkId, opaque string
|
|
171
|
+
status: v.union(v.literal("pending"), v.literal("completed"), // work succeeded
|
|
172
|
+
v.literal("failed"), // work threw
|
|
173
|
+
v.literal("cancelled")),
|
|
174
|
+
resultSummary: v.optional(v.string()), // error message when status=="failed"
|
|
175
|
+
createdAt: v.number(),
|
|
176
|
+
updatedAt: v.number(),
|
|
177
|
+
})
|
|
178
|
+
.index("by_org", ["orgId"])
|
|
179
|
+
.index("by_org_workflow", ["orgId", "workflowId"]),
|
|
180
|
+
// awaitEventTimeouts — self-built timeout for `step.awaitEvent`
|
|
181
|
+
// (workflow#177: "Convex does not provide a native timeout for
|
|
182
|
+
// awaitEvent"). A scheduled mutation (`ctx.scheduler.runAfter`) fires a
|
|
183
|
+
// blank `sandbox.timeout` sessionEvent if the awaited event never arrives
|
|
184
|
+
// before `deadlineAt`.
|
|
185
|
+
awaitEventTimeouts: defineTable({
|
|
186
|
+
orgId: v.string(),
|
|
187
|
+
sessionId: v.id("sessions"),
|
|
188
|
+
awaitedEventName: v.string(),
|
|
189
|
+
deadlineAt: v.number(),
|
|
190
|
+
// optional: patched right after insert, once the scheduled function
|
|
191
|
+
// itself has been created (it needs this row's id as its argument).
|
|
192
|
+
scheduledFunctionId: v.optional(v.id("_scheduled_functions")),
|
|
193
|
+
firedAt: v.optional(v.number()),
|
|
194
|
+
cancelledAt: v.optional(v.number()),
|
|
195
|
+
})
|
|
196
|
+
.index("by_org", ["orgId"])
|
|
197
|
+
.index("by_org_session", ["orgId", "sessionId"]),
|
|
198
|
+
// wakeupDemoResults — TEST-ONLY fixture, see
|
|
199
|
+
// component/testFixtures/wakeupDemoWorkflow.ts. Not part of the public
|
|
200
|
+
// contract; a reference recipient workflow parked on `step.awaitEvent`
|
|
201
|
+
// patches `wokenAt`/`observedStatus` here ONLY after
|
|
202
|
+
// `engine/wakeup.onWorkComplete` has called `sendEvent` -- its presence
|
|
203
|
+
// is the test-observable proof that the wake mechanism (not a poll)
|
|
204
|
+
// resumed the workflow. Same discipline as `callerStepInvocations` above.
|
|
205
|
+
wakeupDemoResults: defineTable({
|
|
206
|
+
orgId: v.string(),
|
|
207
|
+
enqueuedAt: v.number(),
|
|
208
|
+
wokenAt: v.optional(v.number()),
|
|
209
|
+
observedStatus: v.optional(v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled"))),
|
|
210
|
+
observedResultSummary: v.optional(v.string()),
|
|
211
|
+
}).index("by_org", ["orgId"]),
|
|
212
|
+
// ---------------------------------------------------------------------
|
|
213
|
+
// C3 — MANDATES: "notre ligne" (autorité bornée, SYNTHESE §2/§4.e-ter).
|
|
214
|
+
// Ported from convex/schema.ts (repo-root prototype, lines 168-214);
|
|
215
|
+
// table shapes are byte-identical to their app-level source. See
|
|
216
|
+
// component/engine/mandates.ts and component/engine/wakeup.ts (the C3
|
|
217
|
+
// seam: `enqueue` now funds through `mandates.spendInternal` before any
|
|
218
|
+
// work can enter the wakeup pool).
|
|
219
|
+
// ---------------------------------------------------------------------
|
|
220
|
+
// mandates — a plafond is a DATA row scoped by orgId + agentId + scope,
|
|
221
|
+
// never a constant read from code. `kind`/`rate`/`period`/`capacity` are
|
|
222
|
+
// the inline contract of @convex-dev/rate-limiter ("dynamic config"): the
|
|
223
|
+
// COST (`count`) is a WEIGHTED count (tokens/euros/calls), never a plain
|
|
224
|
+
// call counter. Changing a cap is a row mutation, never a deployment.
|
|
225
|
+
mandates: defineTable({
|
|
226
|
+
orgId: v.string(),
|
|
227
|
+
agentId: v.id("agents"),
|
|
228
|
+
scope: v.string(), // free-form business name (e.g. "llm-tokens") -- DATA, not a constant
|
|
229
|
+
kind: v.union(v.literal("token bucket"), v.literal("fixed window")),
|
|
230
|
+
rate: v.number(),
|
|
231
|
+
period: v.number(), // ms
|
|
232
|
+
capacity: v.optional(v.number()),
|
|
233
|
+
createdAt: v.number(),
|
|
234
|
+
updatedAt: v.number(),
|
|
235
|
+
})
|
|
236
|
+
.index("by_org", ["orgId"])
|
|
237
|
+
.index("by_org_agent", ["orgId", "agentId"])
|
|
238
|
+
.index("by_org_agent_scope", ["orgId", "agentId", "scope"]),
|
|
239
|
+
// approvalRequests — created whenever a mandate is exceeded. NEVER a
|
|
240
|
+
// silent bypass: an overrun always becomes a persisted HUMAN approval
|
|
241
|
+
// request, never a silent refusal with no trace.
|
|
242
|
+
approvalRequests: defineTable({
|
|
243
|
+
orgId: v.string(),
|
|
244
|
+
agentId: v.id("agents"),
|
|
245
|
+
scope: v.string(),
|
|
246
|
+
requestedCost: v.number(),
|
|
247
|
+
reason: v.string(),
|
|
248
|
+
status: v.union(v.literal("pending"), v.literal("approved"), v.literal("denied")),
|
|
249
|
+
createdAt: v.number(),
|
|
250
|
+
resolvedAt: v.optional(v.number()),
|
|
251
|
+
resolvedBy: v.optional(v.string()),
|
|
252
|
+
})
|
|
253
|
+
.index("by_org", ["orgId"])
|
|
254
|
+
.index("by_org_agent", ["orgId", "agentId"])
|
|
255
|
+
.index("by_org_status", ["orgId", "status"]),
|
|
256
|
+
// ---------------------------------------------------------------------
|
|
257
|
+
// C4 — HABITS: "notre ligne" (une habitude = un cron DYNAMIQUE par
|
|
258
|
+
// agent/org, SYNTHESE §2/§4.e-ter: "ce qui transforme un chat en
|
|
259
|
+
// employé"). Ported from convex/schema.ts (repo-root prototype, lines
|
|
260
|
+
// 108-124 / 210-217); table shapes are byte-identical to their app-level
|
|
261
|
+
// source. See component/engine/habits.ts (the C4 seam: `crons` is a new
|
|
262
|
+
// sub-component of THIS component, registered in component/convex.config.ts).
|
|
263
|
+
// ---------------------------------------------------------------------
|
|
264
|
+
// habits — a schedule is a DATA row scoped by orgId + agentId, never a
|
|
265
|
+
// constant read from a deployed `convex/crons.ts`. `cronExpression` is
|
|
266
|
+
// the DATA that drives the runtime cron registered via
|
|
267
|
+
// `@convex-dev/crons`; create/updateSchedule/enable/disable never
|
|
268
|
+
// redeploy anything.
|
|
269
|
+
habits: defineTable({
|
|
270
|
+
orgId: v.string(),
|
|
271
|
+
agentId: v.id("agents"),
|
|
272
|
+
name: v.string(),
|
|
273
|
+
cronExpression: v.string(),
|
|
274
|
+
prompt: v.string(),
|
|
275
|
+
enabled: v.boolean(),
|
|
276
|
+
// id of the runtime-registered @convex-dev/crons job, present only
|
|
277
|
+
// while `enabled` is true (cleared by `disable`, reassigned by
|
|
278
|
+
// `enable` / `updateSchedule` -- no redeploy involved in any transition).
|
|
279
|
+
cronJobId: v.optional(v.string()),
|
|
280
|
+
lastRunAt: v.optional(v.number()),
|
|
281
|
+
createdAt: v.number(),
|
|
282
|
+
updatedAt: v.number(),
|
|
283
|
+
})
|
|
284
|
+
.index("by_org", ["orgId"])
|
|
285
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
286
|
+
// habitRuns — proof of execution of a habit (MUST_PASS/MUST_BLOCK probe,
|
|
287
|
+
// component/habits.componentPackaging.test.ts). Every tick of the runtime
|
|
288
|
+
// cron registered via @convex-dev/crons writes a row here -- this is what
|
|
289
|
+
// makes "fires its first run without a deploy" PROVABLE rather than
|
|
290
|
+
// asserted on trust.
|
|
291
|
+
habitRuns: defineTable({
|
|
292
|
+
orgId: v.string(),
|
|
293
|
+
habitId: v.id("habits"),
|
|
294
|
+
agentId: v.id("agents"),
|
|
295
|
+
ranAt: v.number(),
|
|
296
|
+
})
|
|
297
|
+
.index("by_org", ["orgId"])
|
|
298
|
+
.index("by_org_habit", ["orgId", "habitId"]),
|
|
299
|
+
// ---------------------------------------------------------------------
|
|
300
|
+
// C5 — SANDBOX BRIDGE (I5, SYNTHESE.md §4.c-d: "le pont pause/réveil vers
|
|
301
|
+
// le bac à sable ET vers la délégation entre agents, même mécanisme").
|
|
302
|
+
// Ported from convex/schema.ts (repo-root prototype, sandboxRuns /
|
|
303
|
+
// sandboxCallbackDeliveries / sandboxCallbackDeadLetters); table shapes
|
|
304
|
+
// are byte-identical to their app-level source. See
|
|
305
|
+
// component/engine/sandboxCallback.ts.
|
|
306
|
+
//
|
|
307
|
+
// NOTRE LIGNE (frontière, C5 recherche-d'abord): the "use node" action
|
|
308
|
+
// that actually talks to `@vercel/sandbox` (convex/engine/
|
|
309
|
+
// sandboxBridgeActions.ts) CANNOT be ported into this component — the
|
|
310
|
+
// Convex CLI's own component bundler HARD-CRASHES on any "use node" file
|
|
311
|
+
// found in a child component directory ("use node directive is not
|
|
312
|
+
// supported in components", node_modules/convex/dist/cjs/cli/lib/
|
|
313
|
+
// components/definition/bundle.js). It stays app-level and calls INTO
|
|
314
|
+
// this component's public `engine.sandboxCallback.startRun` /
|
|
315
|
+
// `attachSandboxId` (and `engine.mandates.spend`) instead. Likewise, no
|
|
316
|
+
// official `@convex-dev/*` component ships an `http.ts` or reads
|
|
317
|
+
// `process.env` directly (measured: zero hits across all 7 installed
|
|
318
|
+
// `@convex-dev/*` component dist trees) — the signed callback's public
|
|
319
|
+
// HTTP route (`/sandbox/callback`) and its `SANDBOX_CALLBACK_SECRET`
|
|
320
|
+
// read + HMAC verification stay in the CONSUMER app's `convex/http.ts`,
|
|
321
|
+
// which delegates to this component's public
|
|
322
|
+
// `engine.sandboxCallback.processCallback` /
|
|
323
|
+
// `engine.sandboxCallback.deadLetterMalformed` ONLY after the signature
|
|
324
|
+
// has already checked out.
|
|
325
|
+
sandboxConfig: defineTable({
|
|
326
|
+
orgId: v.string(),
|
|
327
|
+
agentId: v.id("agents"),
|
|
328
|
+
provider: v.literal("vercel-sandbox"),
|
|
329
|
+
image: v.optional(v.string()),
|
|
330
|
+
timeoutSeconds: v.number(),
|
|
331
|
+
egressPolicy: v.union(v.literal("allow-all"), v.literal("deny-all-allowlist")),
|
|
332
|
+
allowlistDomains: v.array(v.string()),
|
|
333
|
+
createdAt: v.number(),
|
|
334
|
+
updatedAt: v.number(),
|
|
335
|
+
})
|
|
336
|
+
.index("by_org", ["orgId"])
|
|
337
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
338
|
+
sandboxRuns: defineTable({
|
|
339
|
+
orgId: v.string(),
|
|
340
|
+
workflowId: v.string(), // target: the workflow parked on step.awaitEvent
|
|
341
|
+
awaitedEventName: v.string(), // event name that workflow is awaiting
|
|
342
|
+
sandboxId: v.string(), // Vercel Sandbox id (opaque, from Sandbox.create())
|
|
343
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("timedOut")),
|
|
344
|
+
resultSummary: v.optional(v.string()),
|
|
345
|
+
createdAt: v.number(),
|
|
346
|
+
updatedAt: v.number(),
|
|
347
|
+
})
|
|
348
|
+
.index("by_org", ["orgId"])
|
|
349
|
+
.index("by_org_workflow", ["orgId", "workflowId"])
|
|
350
|
+
.index("by_org_sandbox", ["orgId", "sandboxId"]),
|
|
351
|
+
sandboxCallbackDeliveries: defineTable({
|
|
352
|
+
orgId: v.string(),
|
|
353
|
+
sandboxRunId: v.id("sandboxRuns"),
|
|
354
|
+
deliveryId: v.string(), // caller-supplied, opaque, unique per attempt-family
|
|
355
|
+
outcome: v.union(v.literal("woke"), // first valid delivery for this id -- sendEvent fired
|
|
356
|
+
v.literal("duplicate")),
|
|
357
|
+
receivedAt: v.number(),
|
|
358
|
+
})
|
|
359
|
+
.index("by_org", ["orgId"])
|
|
360
|
+
.index("by_org_delivery", ["orgId", "deliveryId"])
|
|
361
|
+
.index("by_org_run", ["orgId", "sandboxRunId"]),
|
|
362
|
+
sandboxCallbackDeadLetters: defineTable({
|
|
363
|
+
orgId: v.optional(v.string()), // absent when the payload itself couldn't be parsed
|
|
364
|
+
deliveryId: v.string(),
|
|
365
|
+
reason: v.string(),
|
|
366
|
+
rawBody: v.string(),
|
|
367
|
+
createdAt: v.number(),
|
|
368
|
+
}).index("by_org", ["orgId"]),
|
|
369
|
+
// ---------------------------------------------------------------------
|
|
370
|
+
// C6 — DELEGATION + VP CONNECTION (I6, SYNTHESE.md §4.d: "un agent qui
|
|
371
|
+
// délègue à un autre agent = LE MÊME système pause/réveil que le pont
|
|
372
|
+
// sandbox -- un seul système à écrire, pas deux"). Ported from
|
|
373
|
+
// convex/schema.ts (repo-root prototype, lines 74-91 / 377-467); table
|
|
374
|
+
// shapes are byte-identical to their app-level source. See
|
|
375
|
+
// component/engine/delegation.ts and component/engine/vpConnection.ts
|
|
376
|
+
// (the C6 seam: both terminate delegation/clone paths in exactly one call
|
|
377
|
+
// to THIS component's own `engine/wakeup.enqueue`, C2/C3's public action,
|
|
378
|
+
// never a second onComplete/sendEvent path).
|
|
379
|
+
// ---------------------------------------------------------------------
|
|
380
|
+
// subagents — declared subagents, RECURSIVE (the child is a full
|
|
381
|
+
// `agents` row with its own aggregate). Isolation is by construction
|
|
382
|
+
// (eve.dev/docs/subagents: "inherits nothing from the root's authored
|
|
383
|
+
// slots") -- this table ONLY declares the parent->child link and exposes
|
|
384
|
+
// it as a named tool; it never copies a single field out of the child's
|
|
385
|
+
// own skills/connections/toolApprovals onto the parent.
|
|
386
|
+
subagents: defineTable({
|
|
387
|
+
orgId: v.string(),
|
|
388
|
+
parentAgentId: v.id("agents"),
|
|
389
|
+
childAgentId: v.id("agents"), // full agent, own aggregate -- strict isolation
|
|
390
|
+
toolName: v.string(),
|
|
391
|
+
description: v.optional(v.string()),
|
|
392
|
+
enabled: v.boolean(),
|
|
393
|
+
createdAt: v.number(),
|
|
394
|
+
})
|
|
395
|
+
.index("by_org", ["orgId"])
|
|
396
|
+
.index("by_org_parent", ["orgId", "parentAgentId"])
|
|
397
|
+
.index("by_org_child", ["orgId", "childAgentId"])
|
|
398
|
+
// I6 build-time collision guard (engine/delegation.declareSubagent):
|
|
399
|
+
// exact-match lookup of an existing (parent, toolName) pair, no filter().
|
|
400
|
+
.index("by_org_parent_tool", ["orgId", "parentAgentId", "toolName"]),
|
|
401
|
+
// delegationRuns — one row per A->B delegation through a declared
|
|
402
|
+
// subagent (`subagents` table above). `workflowId`/`awaitedEventName` are
|
|
403
|
+
// the SAME two fields `engine/wakeup.enqueue` already threads through to
|
|
404
|
+
// `workflow.sendEvent` -- this table records WHICH delegation a given
|
|
405
|
+
// `wakeableWork` row belongs to; it never duplicates the wake mechanism
|
|
406
|
+
// itself (no second onComplete/sendEvent path exists anywhere in this
|
|
407
|
+
// engine).
|
|
408
|
+
delegationRuns: defineTable({
|
|
409
|
+
orgId: v.string(),
|
|
410
|
+
parentAgentId: v.id("agents"),
|
|
411
|
+
childAgentId: v.id("agents"),
|
|
412
|
+
toolName: v.string(),
|
|
413
|
+
workflowId: v.string(),
|
|
414
|
+
awaitedEventName: v.string(),
|
|
415
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
|
|
416
|
+
resultSummary: v.optional(v.string()),
|
|
417
|
+
createdAt: v.number(),
|
|
418
|
+
updatedAt: v.number(),
|
|
419
|
+
})
|
|
420
|
+
.index("by_org", ["orgId"])
|
|
421
|
+
.index("by_org_parent", ["orgId", "parentAgentId"])
|
|
422
|
+
.index("by_org_workflow", ["orgId", "workflowId"])
|
|
423
|
+
.index("by_org_awaitedEvent", ["orgId", "awaitedEventName"]),
|
|
424
|
+
// agentCloneRuns — I6 built-in `agent` tool (eve.dev/docs/subagents: "the
|
|
425
|
+
// built-in `agent` tool clones the ROOT, root-only, NOT recursive -- it
|
|
426
|
+
// cannot call itself"). `viaAgentTool` is the recursion guard: a clone
|
|
427
|
+
// invocation started BY a clone invocation (viaAgentTool: true passed in)
|
|
428
|
+
// is refused at `engine/delegation.invokeAgentClone` BEFORE any row here
|
|
429
|
+
// is written -- so every row in this table by construction has
|
|
430
|
+
// `viaAgentTool === false` on entry. Wakes through the SAME
|
|
431
|
+
// `engine/wakeup.enqueue` mechanism as `delegationRuns`.
|
|
432
|
+
agentCloneRuns: defineTable({
|
|
433
|
+
orgId: v.string(),
|
|
434
|
+
agentId: v.id("agents"), // the root being cloned
|
|
435
|
+
workflowId: v.string(),
|
|
436
|
+
awaitedEventName: v.string(),
|
|
437
|
+
status: v.union(v.literal("pending"), v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
|
|
438
|
+
resultSummary: v.optional(v.string()),
|
|
439
|
+
createdAt: v.number(),
|
|
440
|
+
updatedAt: v.number(),
|
|
441
|
+
})
|
|
442
|
+
.index("by_org", ["orgId"])
|
|
443
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
444
|
+
// vpConnections — I6 (mvp-evevantage.md §2.2 "Travail d'équipe"). The
|
|
445
|
+
// four VantagePeers tools (memory, tasks, messages, notes) wired as a
|
|
446
|
+
// CONNECTION of the agent aggregate, notre ligne. 0..1 row per agent;
|
|
447
|
+
// absence means "use the defaults" (all four ON) -- see
|
|
448
|
+
// `engine/vpConnection.ts:defaultToggles`. DIRECT access: invocation
|
|
449
|
+
// goes through a caller-supplied Convex function handle
|
|
450
|
+
// (`createFunctionHandle`, same discipline as `wakeableWork.workId` /
|
|
451
|
+
// `durableJobs.stepFunctionHandle`), never a `fetch()` call.
|
|
452
|
+
vpConnections: defineTable({
|
|
453
|
+
orgId: v.string(),
|
|
454
|
+
agentId: v.id("agents"),
|
|
455
|
+
memoryEnabled: v.boolean(),
|
|
456
|
+
tasksEnabled: v.boolean(),
|
|
457
|
+
messagesEnabled: v.boolean(),
|
|
458
|
+
notesEnabled: v.boolean(),
|
|
459
|
+
createdAt: v.number(),
|
|
460
|
+
updatedAt: v.number(),
|
|
461
|
+
})
|
|
462
|
+
.index("by_org", ["orgId"])
|
|
463
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
464
|
+
// vpToolInvocations — audit trail of every VantagePeers tool call an
|
|
465
|
+
// agent made, ALWAYS written by `engine/vpConnection.invokeVpTool` in the
|
|
466
|
+
// SAME transaction family as the call itself.
|
|
467
|
+
vpToolInvocations: defineTable({
|
|
468
|
+
orgId: v.string(),
|
|
469
|
+
agentId: v.id("agents"),
|
|
470
|
+
tool: v.union(v.literal("memory"), v.literal("tasks"), v.literal("messages"), v.literal("notes")),
|
|
471
|
+
createdAt: v.number(),
|
|
472
|
+
})
|
|
473
|
+
.index("by_org", ["orgId"])
|
|
474
|
+
.index("by_org_agent", ["orgId", "agentId"]),
|
|
475
|
+
// vpTenantFixture — TEST-ONLY stand-in for "VantagePeers' own tenant
|
|
476
|
+
// table" (component/testFixtures/vpTool.ts). This engine never owns
|
|
477
|
+
// VantagePeers' schema (component isolation) -- this fixture plays the
|
|
478
|
+
// CALLER-side function a real `vpFunctionHandle` would point to, so
|
|
479
|
+
// `component/engine/vpConnection.test.ts` can prove a VP tool write
|
|
480
|
+
// lands in the invoking agent's own org tenant (and that a mismatched
|
|
481
|
+
// orgId is refused) without needing a second real Convex deployment in
|
|
482
|
+
// the test harness. Not part of the public contract.
|
|
483
|
+
vpTenantFixture: defineTable({
|
|
484
|
+
orgId: v.string(),
|
|
485
|
+
tool: v.union(v.literal("memory"), v.literal("tasks"), v.literal("messages"), v.literal("notes")),
|
|
486
|
+
argsJson: v.string(),
|
|
487
|
+
createdAt: v.number(),
|
|
488
|
+
}).index("by_org", ["orgId"]),
|
|
137
489
|
});
|
|
@@ -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,76 @@
|
|
|
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 (C6), ported from
|
|
8
|
+
// convex/engine/delegationDemoWorkflow.ts. This is exactly the "bespoke
|
|
9
|
+
// workflow" shape `engine/wakeup.ts`'s own comment describes ("any
|
|
10
|
+
// workflow -- a session, a durable job, or a bespoke one -- can be the wake
|
|
11
|
+
// target"): it parks on `step.awaitEvent` and, once
|
|
12
|
+
// `engine.wakeup.onWorkComplete` fires `sendEvent` (via
|
|
13
|
+
// `engine.delegation.delegateToSubagent` -> `engine.wakeup.enqueue`, the
|
|
14
|
+
// SAME public action C2/C3 ships), it patches the SAME `delegationRuns`
|
|
15
|
+
// row `delegateToSubagent` created -- proving A paused and was woken by
|
|
16
|
+
// B's completion, with zero polling.
|
|
17
|
+
export const workflow = new WorkflowManager(components.workflow);
|
|
18
|
+
export const start = mutation({
|
|
19
|
+
args: { orgId: v.string(), awaitedEventName: v.string() },
|
|
20
|
+
returns: v.object({ workflowId: v.string() }),
|
|
21
|
+
handler: async (ctx, args) => {
|
|
22
|
+
const orgId = requireOrgId(args.orgId);
|
|
23
|
+
const workflowId = await workflow.start(ctx, internal.testFixtures.delegationDemoWorkflow.delegationDemoWorkflowV1, { orgId, awaitedEventName: args.awaitedEventName });
|
|
24
|
+
return { workflowId };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
export const delegationDemoWorkflowV1 = workflow.define({
|
|
28
|
+
args: { orgId: v.string(), awaitedEventName: v.string() },
|
|
29
|
+
returns: v.null(),
|
|
30
|
+
handler: async (step, args) => {
|
|
31
|
+
// A pauses here. Resumes ONLY when engine.wakeup.onWorkComplete calls
|
|
32
|
+
// sendEvent with this exact name -- no loop, no timer.
|
|
33
|
+
const result = await step.awaitEvent({
|
|
34
|
+
name: args.awaitedEventName,
|
|
35
|
+
validator: v.object({
|
|
36
|
+
status: v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
|
|
37
|
+
resultSummary: v.optional(v.string()),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
await step.runMutation(internal.testFixtures.delegationDemoWorkflow.recordDelegationResolvedByEventInternal, {
|
|
41
|
+
orgId: args.orgId,
|
|
42
|
+
awaitedEventName: args.awaitedEventName,
|
|
43
|
+
status: result.status,
|
|
44
|
+
resultSummary: result.resultSummary,
|
|
45
|
+
});
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// recordDelegationResolvedByEventInternal — looks up the delegationRuns row
|
|
50
|
+
// created for THIS (orgId, awaitedEventName) pair via `by_org_awaitedEvent`
|
|
51
|
+
// (no `.filter()`) and patches it. Kept as one mutation (rather than two
|
|
52
|
+
// round trips from the workflow step) so the lookup and patch land in the
|
|
53
|
+
// same transaction.
|
|
54
|
+
export const recordDelegationResolvedByEventInternal = internalMutation({
|
|
55
|
+
args: {
|
|
56
|
+
orgId: v.string(),
|
|
57
|
+
awaitedEventName: v.string(),
|
|
58
|
+
status: v.union(v.literal("completed"), v.literal("failed"), v.literal("cancelled")),
|
|
59
|
+
resultSummary: v.optional(v.string()),
|
|
60
|
+
},
|
|
61
|
+
returns: v.null(),
|
|
62
|
+
handler: async (ctx, args) => {
|
|
63
|
+
const row = await ctx.db
|
|
64
|
+
.query("delegationRuns")
|
|
65
|
+
.withIndex("by_org_awaitedEvent", (q) => q.eq("orgId", args.orgId).eq("awaitedEventName", args.awaitedEventName))
|
|
66
|
+
.first();
|
|
67
|
+
if (!row)
|
|
68
|
+
return null;
|
|
69
|
+
await ctx.db.patch(row._id, {
|
|
70
|
+
status: args.status,
|
|
71
|
+
resultSummary: args.resultSummary,
|
|
72
|
+
updatedAt: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -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
|
+
// C5 sandbox bridge, ported from convex/engine/sandboxDemoWorkflow.ts, same
|
|
8
|
+
// shape/role as testFixtures/wakeupDemoWorkflow.ts (C2). 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,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
|
+
});
|
|
@@ -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" | "cancelled" | "failed";
|
|
14
|
+
resultId: import("convex/values").GenericId<"wakeupDemoResults">;
|
|
15
|
+
}, Promise<null>>;
|
|
16
|
+
export declare const wakeupDemoWorkflowV1: 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>;
|