@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.
- package/LICENSE +110 -0
- package/README.md +154 -473
- package/dist/component/_generated/api.d.ts +31 -0
- package/dist/component/_generated/component.d.ts +346 -2
- package/dist/component/convex.config.js +18 -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 +81 -0
- package/dist/component/engine/delegation.d.ts +75 -0
- package/dist/component/engine/delegation.js +327 -0
- package/dist/component/engine/directory.d.ts +9 -0
- package/dist/component/engine/directory.js +45 -0
- package/dist/component/engine/durableJob.js +6 -6
- package/dist/component/engine/habits.d.ts +70 -0
- package/dist/component/engine/habits.js +218 -0
- package/dist/component/engine/mandates.d.ts +74 -0
- package/dist/component/engine/mandates.js +271 -0
- package/dist/component/engine/sandboxCallback.d.ts +36 -0
- package/dist/component/engine/sandboxCallback.js +259 -0
- package/dist/component/engine/sessions.d.ts +6 -2
- package/dist/component/engine/sessions.js +43 -7
- package/dist/component/engine/toolApprovals.js +1 -1
- package/dist/component/engine/vpConnection.d.ts +69 -0
- package/dist/component/engine/vpConnection.js +218 -0
- package/dist/component/engine/wakeup.d.ts +33 -0
- package/dist/component/engine/wakeup.js +228 -0
- package/dist/component/engine/workflows.d.ts +11 -0
- package/dist/component/engine/workflows.js +84 -0
- package/dist/component/schema.d.ts +365 -4
- package/dist/component/schema.js +345 -18
- package/dist/component/testFixtures/callerSteps.js +1 -1
- package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +18 -0
- package/dist/component/testFixtures/delegationDemoWorkflow.js +75 -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 +17 -5
package/dist/component/schema.js
CHANGED
|
@@ -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
|
|
12
|
-
//
|
|
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
|
|
24
|
-
//
|
|
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.
|
|
94
|
-
//
|
|
95
|
-
// run (
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
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
|
-
// "
|
|
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
|
|
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
|
|
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,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
|
+
});
|