@vantageos/agent-engine 0.1.0-alpha.8 → 0.3.0-alpha.1

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 (62) hide show
  1. package/README.md +33 -8
  2. package/dist/component/_generated/api.d.ts +14 -3
  3. package/dist/component/_generated/api.js +0 -25
  4. package/dist/component/_generated/component.d.ts +369 -73
  5. package/dist/component/_generated/component.js +0 -9
  6. package/dist/component/_generated/dataModel.js +0 -9
  7. package/dist/component/_generated/server.js +0 -69
  8. package/dist/component/convex.config.js +0 -44
  9. package/dist/component/engine/agents.d.ts +3 -3
  10. package/dist/component/engine/agents.js +0 -7
  11. package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
  12. package/dist/component/engine/awaitEventTimeout.js +16 -26
  13. package/dist/component/engine/connections.d.ts +74 -0
  14. package/dist/component/engine/connections.js +254 -0
  15. package/dist/component/engine/delegation.d.ts +5 -5
  16. package/dist/component/engine/delegation.js +0 -74
  17. package/dist/component/engine/directory.js +0 -18
  18. package/dist/component/engine/durableJob.d.ts +24 -17
  19. package/dist/component/engine/durableJob.js +182 -138
  20. package/dist/component/engine/mandates.d.ts +2 -2
  21. package/dist/component/engine/mandates.js +0 -59
  22. package/dist/component/engine/queue.d.ts +97 -0
  23. package/dist/component/engine/queue.js +201 -0
  24. package/dist/component/engine/resolvers.d.ts +1 -1
  25. package/dist/component/engine/resolvers.js +6 -18
  26. package/dist/component/engine/sandboxCallback.d.ts +2 -2
  27. package/dist/component/engine/sandboxCallback.js +2 -79
  28. package/dist/component/engine/sandboxConfigProvider.d.ts +9 -0
  29. package/dist/component/engine/sandboxConfigProvider.js +32 -0
  30. package/dist/component/engine/sessions.d.ts +28 -10
  31. package/dist/component/engine/sessions.js +130 -170
  32. package/dist/component/engine/streams.d.ts +47 -0
  33. package/dist/component/engine/streams.js +162 -0
  34. package/dist/component/engine/toolApprovals.d.ts +1 -1
  35. package/dist/component/engine/toolApprovals.js +12 -37
  36. package/dist/component/engine/vpConnection.js +0 -40
  37. package/dist/component/engine/wakeup.d.ts +7 -9
  38. package/dist/component/engine/wakeup.js +54 -127
  39. package/dist/component/engine/workflows.d.ts +26 -8
  40. package/dist/component/engine/workflows.js +59 -64
  41. package/dist/component/engine/world.d.ts +250 -0
  42. package/dist/component/engine/world.js +689 -0
  43. package/dist/component/lib/auth.js +0 -26
  44. package/dist/component/lib/connectionSecret.d.ts +5 -0
  45. package/dist/component/lib/connectionSecret.js +32 -0
  46. package/dist/component/schema.d.ts +240 -175
  47. package/dist/component/schema.js +134 -294
  48. package/package.json +21 -6
  49. package/dist/component/engine/habits.d.ts +0 -70
  50. package/dist/component/engine/habits.js +0 -218
  51. package/dist/component/testFixtures/callerSteps.d.ts +0 -20
  52. package/dist/component/testFixtures/callerSteps.js +0 -90
  53. package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +0 -18
  54. package/dist/component/testFixtures/delegationDemoWorkflow.js +0 -75
  55. package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +0 -19
  56. package/dist/component/testFixtures/sandboxDemoWorkflow.js +0 -69
  57. package/dist/component/testFixtures/vpTool.d.ts +0 -4
  58. package/dist/component/testFixtures/vpTool.js +0 -25
  59. package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +0 -19
  60. package/dist/component/testFixtures/wakeupDemoWorkflow.js +0 -77
  61. package/dist/component/testFixtures/wakeupWork.d.ts +0 -8
  62. package/dist/component/testFixtures/wakeupWork.js +0 -21
@@ -2,11 +2,11 @@ import type { Id } from "../_generated/dataModel.js";
2
2
  export declare const set: import("convex/server").RegisteredMutation<"public", {
3
3
  capacity?: number | undefined;
4
4
  orgId: string;
5
- kind: "token bucket" | "fixed window";
6
5
  agentId: import("convex/values").GenericId<"agents">;
7
6
  scope: string;
8
7
  rate: number;
9
8
  period: number;
9
+ kind: "token bucket" | "fixed window";
10
10
  }, Promise<import("convex/values").GenericId<"mandates">>>;
11
11
  export declare const get: import("convex/server").RegisteredQuery<"public", {
12
12
  orgId: string;
@@ -19,11 +19,11 @@ export declare const get: import("convex/server").RegisteredQuery<"public", {
19
19
  orgId: string;
20
20
  createdAt: number;
21
21
  updatedAt: number;
22
- kind: "token bucket" | "fixed window";
23
22
  agentId: import("convex/values").GenericId<"agents">;
24
23
  scope: string;
25
24
  rate: number;
26
25
  period: number;
26
+ kind: "token bucket" | "fixed window";
27
27
  } | null>>;
28
28
  export declare const spend: import("convex/server").RegisteredMutation<"public", {
29
29
  orgId: string;
@@ -4,26 +4,7 @@ import { ShardedCounter } from "@convex-dev/sharded-counter";
4
4
  import { internalMutation, mutation, query } from "../_generated/server.js";
5
5
  import { components } from "../_generated/api.js";
6
6
  import { assertSameOrg, requireOrgId } from "../lib/auth.js";
7
- // engine/mandates — "intentional divergence from Eve": BOUNDED AUTHORITY.
8
- // A mandate is a LINE OF DATA scoped
9
- // orgId+agentId+scope, never a constant read from code (litmus test: a
10
- // client changes their mind => zero deployment). The rate-limiter has NO
11
- // static named config in this module -- every call to `spend` builds its
12
- // `config` inline, dynamically, from the `mandates` row loaded from the
13
- // database (@convex-dev/rate-limiter README §"Define a rate limit inline /
14
- // dynamically"). Fails-closed: no mandate row => refusal, never a silent
15
- // bypass.
16
- //
17
- // orgId discipline (component/lib/auth.ts): every PUBLIC function takes
18
- // `orgId` as an explicit ARGUMENT (auth does not propagate across a
19
- // component boundary), same as the rest of this component (agents,
20
- // wakeup, sessions). `spendInternal` also takes `orgId` as an argument, but
21
- // is an `internalMutation` -- the ONLY caller reachable from any client is
22
- // `engine/wakeup.enqueue` (an action), which threads its own
23
- // caller-supplied `orgId` argument through after validating it.
24
7
  const rateLimiter = new RateLimiter(components.rateLimiter);
25
- // Usage rollup, keyed identically to the rate-limiter key (org+agent+scope).
26
- // internal research notes: counters via sharded-counter, never a canary/alpha dependency.
27
8
  const usageCounter = new ShardedCounter(components.shardedCounter);
28
9
  const mandateDoc = v.object({
29
10
  _id: v.id("mandates"),
@@ -38,9 +19,6 @@ const mandateDoc = v.object({
38
19
  createdAt: v.number(),
39
20
  updatedAt: v.number(),
40
21
  });
41
- // set — upserts the DATA row that bounds an agent's authority for a given
42
- // scope. This is the ONLY place a cap value is ever written; there is no
43
- // constant anywhere in the engine that plays this role.
44
22
  export const set = mutation({
45
23
  args: {
46
24
  orgId: v.string(),
@@ -101,25 +79,6 @@ export const get = query({
101
79
  .unique();
102
80
  },
103
81
  });
104
- // spend — the single choke point through which every costed action an
105
- // agent takes must pass. `cost` is a WEIGHTED count (tokens, euros, calls —
106
- // whatever the mandate's scope means), never a plain call counter.
107
- //
108
- // Deny by default, twice over:
109
- // 1. No mandate row for this org+agent+scope => throw immediately. We do
110
- // NOT fall back to "unlimited"; an unconfigured mandate is a refusal,
111
- // not an escape hatch. Nothing needs to be persisted for this case, so
112
- // throwing (and rolling back the whole transaction) is safe.
113
- // 2. Mandate exists but is exhausted => an approvalRequests row MUST be
114
- // committed. Convex mutations are single transactions: throwing after
115
- // `ctx.db.insert` would roll the insert back along with everything
116
- // else, silently destroying the very approval request the guard is
117
- // supposed to leave behind. So this path returns `{ ok: false }`
118
- // instead of throwing -- the approval row survives, and the caller
119
- // (engine/wakeup) is REQUIRED to check `.ok` and treat `false` as a
120
- // hard refusal. There is no code path that lets the action proceed
121
- // past an exhausted mandate; the only way authority is restored is an
122
- // explicit human `resolveApproval("approved")`.
123
82
  export const spend = mutation({
124
83
  args: {
125
84
  orgId: v.string(),
@@ -139,14 +98,6 @@ export const spend = mutation({
139
98
  return await spendCore(ctx, orgId, args);
140
99
  },
141
100
  });
142
- // spendInternal — SAME choke point as `spend`, INTERNAL ONLY (unreachable
143
- // from any client). This is the ONE function `engine/wakeup.enqueue` (an
144
- // `action`) calls, in its OWN transaction, before it may ever call
145
- // `enqueueInternal` to put work in the pool -- see the wiring note in
146
- // component/engine/wakeup.ts. Trusting an `orgId` argument here is exactly
147
- // as safe as `engine/wakeup.enqueueInternal` trusting one: the only code
148
- // that can reach an `internal.*` function is other server code, never a
149
- // client SDK. The public `spend` above is completely untouched.
150
101
  export const spendInternal = internalMutation({
151
102
  args: {
152
103
  orgId: v.string(),
@@ -174,7 +125,6 @@ async function spendCore(ctx, orgId, args) {
174
125
  .withIndex("by_org_agent_scope", (q) => q.eq("orgId", orgId).eq("agentId", args.agentId).eq("scope", args.scope))
175
126
  .unique();
176
127
  if (!mandate) {
177
- // fails-closed: no data => no authority. Never silently unlimited.
178
128
  throw new Error(`Mandate refused: no mandate configured for scope "${args.scope}"`);
179
129
  }
180
130
  const key = `${orgId}:${args.agentId}:${args.scope}`;
@@ -189,10 +139,6 @@ async function spendCore(ctx, orgId, args) {
189
139
  },
190
140
  });
191
141
  if (!status.ok) {
192
- // Overrun => a human approval request is created and PERSISTED
193
- // (returning instead of throwing so the transaction commits it). This
194
- // is the ONLY consequence of an overrun -- there is no code path that
195
- // spends past an exhausted mandate.
196
142
  const approvalId = await ctx.db.insert("approvalRequests", {
197
143
  orgId,
198
144
  agentId: args.agentId,
@@ -204,8 +150,6 @@ async function spendCore(ctx, orgId, args) {
204
150
  });
205
151
  return { ok: false, approvalId };
206
152
  }
207
- // Rollup via sharded-counter (internal research notes: no canary dependency for a
208
- // guardrail). One shard bucket per org+agent+scope key.
209
153
  await usageCounter.add(ctx, key, args.cost);
210
154
  return { ok: true };
211
155
  }
@@ -235,8 +179,6 @@ export const listApprovals = query({
235
179
  : rows;
236
180
  },
237
181
  });
238
- // resolveApproval — the ONLY way an overrun ever becomes usable authority
239
- // again: an explicit human decision, recorded, never inferred.
240
182
  export const resolveApproval = mutation({
241
183
  args: {
242
184
  orgId: v.string(),
@@ -257,7 +199,6 @@ export const resolveApproval = mutation({
257
199
  return null;
258
200
  },
259
201
  });
260
- // usageCount — the rollup readout (sharded-counter), for dashboards/audits.
261
202
  export const usageCount = query({
262
203
  args: { orgId: v.string(), agentId: v.id("agents"), scope: v.string() },
263
204
  returns: v.number(),
@@ -0,0 +1,97 @@
1
+ import type { MutationCtx, QueryCtx } from "../_generated/server.js";
2
+ export declare function enqueueMessage(ctx: MutationCtx, args: {
3
+ orgId: string;
4
+ queueName: string;
5
+ payloadJson: string;
6
+ idempotencyKey?: string;
7
+ delaySeconds?: number;
8
+ }): Promise<{
9
+ messageId: string;
10
+ }>;
11
+ export declare function getMessageById(ctx: MutationCtx | QueryCtx, args: {
12
+ orgId: string;
13
+ messageId: string;
14
+ }): Promise<{
15
+ _id: import("convex/values").GenericId<"worldQueueMessages">;
16
+ _creationTime: number;
17
+ idempotencyKey?: string | undefined;
18
+ orgId: string;
19
+ status: "failed" | "pending" | "claimed" | "delivered";
20
+ createdAt: number;
21
+ updatedAt: number;
22
+ queueName: string;
23
+ messageId: string;
24
+ payloadJson: string;
25
+ attempt: number;
26
+ deliverAt: number;
27
+ } | null>;
28
+ export declare function settleMessageById(ctx: MutationCtx, args: {
29
+ orgId: string;
30
+ messageId: string;
31
+ status: "delivered" | "failed";
32
+ }): Promise<boolean>;
33
+ export declare function setMessagePayload(ctx: MutationCtx, args: {
34
+ orgId: string;
35
+ messageId: string;
36
+ payloadJson: string;
37
+ }): Promise<boolean>;
38
+ export declare const enqueue: import("convex/server").RegisteredMutation<"public", {
39
+ idempotencyKey?: string | undefined;
40
+ delaySeconds?: number | undefined;
41
+ orgId: string;
42
+ queueName: string;
43
+ payloadJson: string;
44
+ }, Promise<{
45
+ messageId: string;
46
+ }>>;
47
+ export declare const claimNextForDelivery: import("convex/server").RegisteredMutation<"public", {
48
+ orgId: string;
49
+ queueNamePrefix: string;
50
+ }, Promise<{
51
+ messageId: string;
52
+ queueName: string;
53
+ payloadJson: string;
54
+ idempotencyKey: string | undefined;
55
+ attempt: number;
56
+ status: "claimed";
57
+ deliverAt: number;
58
+ createdAt: number;
59
+ updatedAt: number;
60
+ } | null>>;
61
+ export declare const markDelivered: import("convex/server").RegisteredMutation<"public", {
62
+ orgId: string;
63
+ messageId: string;
64
+ }, Promise<null>>;
65
+ export declare const markFailedForRetry: import("convex/server").RegisteredMutation<"public", {
66
+ orgId: string;
67
+ messageId: string;
68
+ delaySeconds: number;
69
+ }, Promise<null>>;
70
+ export declare const getMessage: import("convex/server").RegisteredQuery<"public", {
71
+ orgId: string;
72
+ messageId: string;
73
+ }, Promise<{
74
+ messageId: string;
75
+ queueName: string;
76
+ payloadJson: string;
77
+ idempotencyKey: string | undefined;
78
+ attempt: number;
79
+ status: "failed" | "pending" | "claimed" | "delivered";
80
+ deliverAt: number;
81
+ createdAt: number;
82
+ updatedAt: number;
83
+ } | null>>;
84
+ export declare const listPending: import("convex/server").RegisteredQuery<"public", {
85
+ orgId: string;
86
+ queueNamePrefix: string;
87
+ }, Promise<{
88
+ messageId: string;
89
+ queueName: string;
90
+ payloadJson: string;
91
+ idempotencyKey: string | undefined;
92
+ attempt: number;
93
+ status: "failed" | "pending" | "claimed" | "delivered";
94
+ deliverAt: number;
95
+ createdAt: number;
96
+ updatedAt: number;
97
+ }[]>>;
@@ -0,0 +1,201 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "../_generated/server.js";
3
+ import { requireOrgId } from "../lib/auth.js";
4
+ const queueMessageResult = v.object({
5
+ messageId: v.string(),
6
+ queueName: v.string(),
7
+ payloadJson: v.string(),
8
+ idempotencyKey: v.optional(v.string()),
9
+ attempt: v.number(),
10
+ status: v.union(v.literal("pending"), v.literal("claimed"), v.literal("delivered"), v.literal("failed")),
11
+ deliverAt: v.number(),
12
+ createdAt: v.number(),
13
+ updatedAt: v.number(),
14
+ });
15
+ function newMessageId() {
16
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 14)}`;
17
+ }
18
+ export async function enqueueMessage(ctx, args) {
19
+ const { orgId } = args;
20
+ const now = Date.now();
21
+ if (args.idempotencyKey) {
22
+ const existing = await ctx.db
23
+ .query("worldQueueMessages")
24
+ .withIndex("by_org_queue_idempotencyKey", (q) => q
25
+ .eq("orgId", orgId)
26
+ .eq("queueName", args.queueName)
27
+ .eq("idempotencyKey", args.idempotencyKey))
28
+ .unique();
29
+ if (existing) {
30
+ return { messageId: existing.messageId };
31
+ }
32
+ }
33
+ const messageId = newMessageId();
34
+ const deliverAt = now + (args.delaySeconds ?? 0) * 1000;
35
+ await ctx.db.insert("worldQueueMessages", {
36
+ orgId,
37
+ queueName: args.queueName,
38
+ messageId,
39
+ payloadJson: args.payloadJson,
40
+ idempotencyKey: args.idempotencyKey,
41
+ attempt: 0,
42
+ status: "pending",
43
+ deliverAt,
44
+ createdAt: now,
45
+ updatedAt: now,
46
+ });
47
+ return { messageId };
48
+ }
49
+ export async function getMessageById(ctx, args) {
50
+ return await ctx.db
51
+ .query("worldQueueMessages")
52
+ .withIndex("by_org_message", (q) => q.eq("orgId", args.orgId).eq("messageId", args.messageId))
53
+ .unique();
54
+ }
55
+ export async function settleMessageById(ctx, args) {
56
+ const doc = await getMessageById(ctx, args);
57
+ if (!doc)
58
+ return false;
59
+ await ctx.db.patch(doc._id, { status: args.status, updatedAt: Date.now() });
60
+ return true;
61
+ }
62
+ export async function setMessagePayload(ctx, args) {
63
+ const doc = await getMessageById(ctx, args);
64
+ if (!doc)
65
+ return false;
66
+ await ctx.db.patch(doc._id, { payloadJson: args.payloadJson, updatedAt: Date.now() });
67
+ return true;
68
+ }
69
+ export const enqueue = mutation({
70
+ args: {
71
+ orgId: v.string(),
72
+ queueName: v.string(),
73
+ payloadJson: v.string(),
74
+ idempotencyKey: v.optional(v.string()),
75
+ delaySeconds: v.optional(v.number()),
76
+ },
77
+ returns: v.object({ messageId: v.string() }),
78
+ handler: async (ctx, args) => {
79
+ const orgId = requireOrgId(args.orgId);
80
+ return await enqueueMessage(ctx, { ...args, orgId });
81
+ },
82
+ });
83
+ export const claimNextForDelivery = mutation({
84
+ args: { orgId: v.string(), queueNamePrefix: v.string() },
85
+ returns: v.union(queueMessageResult, v.null()),
86
+ handler: async (ctx, args) => {
87
+ const orgId = requireOrgId(args.orgId);
88
+ const now = Date.now();
89
+ const candidates = await ctx.db
90
+ .query("worldQueueMessages")
91
+ .withIndex("by_org", (q) => q.eq("orgId", orgId))
92
+ .filter((q) => q.eq(q.field("status"), "pending"))
93
+ .collect();
94
+ const due = candidates
95
+ .filter((doc) => doc.queueName.startsWith(args.queueNamePrefix) && doc.deliverAt <= now)
96
+ .sort((a, b) => a.deliverAt - b.deliverAt || a.createdAt - b.createdAt);
97
+ const claim = due[0];
98
+ if (!claim) {
99
+ return null;
100
+ }
101
+ const attempt = claim.attempt + 1;
102
+ await ctx.db.patch(claim._id, { attempt, status: "claimed", updatedAt: now });
103
+ return {
104
+ messageId: claim.messageId,
105
+ queueName: claim.queueName,
106
+ payloadJson: claim.payloadJson,
107
+ idempotencyKey: claim.idempotencyKey,
108
+ attempt,
109
+ status: "claimed",
110
+ deliverAt: claim.deliverAt,
111
+ createdAt: claim.createdAt,
112
+ updatedAt: now,
113
+ };
114
+ },
115
+ });
116
+ export const markDelivered = mutation({
117
+ args: { orgId: v.string(), messageId: v.string() },
118
+ returns: v.null(),
119
+ handler: async (ctx, args) => {
120
+ const orgId = requireOrgId(args.orgId);
121
+ const doc = await ctx.db
122
+ .query("worldQueueMessages")
123
+ .withIndex("by_org_message", (q) => q.eq("orgId", orgId).eq("messageId", args.messageId))
124
+ .unique();
125
+ if (!doc || doc.orgId !== orgId) {
126
+ throw new Error("Queue message not found");
127
+ }
128
+ await ctx.db.patch(doc._id, { status: "delivered", updatedAt: Date.now() });
129
+ return null;
130
+ },
131
+ });
132
+ export const markFailedForRetry = mutation({
133
+ args: { orgId: v.string(), messageId: v.string(), delaySeconds: v.number() },
134
+ returns: v.null(),
135
+ handler: async (ctx, args) => {
136
+ const orgId = requireOrgId(args.orgId);
137
+ const doc = await ctx.db
138
+ .query("worldQueueMessages")
139
+ .withIndex("by_org_message", (q) => q.eq("orgId", orgId).eq("messageId", args.messageId))
140
+ .unique();
141
+ if (!doc || doc.orgId !== orgId) {
142
+ throw new Error("Queue message not found");
143
+ }
144
+ await ctx.db.patch(doc._id, {
145
+ status: "pending",
146
+ deliverAt: Date.now() + args.delaySeconds * 1000,
147
+ updatedAt: Date.now(),
148
+ });
149
+ return null;
150
+ },
151
+ });
152
+ export const getMessage = query({
153
+ args: { orgId: v.string(), messageId: v.string() },
154
+ returns: v.union(queueMessageResult, v.null()),
155
+ handler: async (ctx, args) => {
156
+ const orgId = requireOrgId(args.orgId);
157
+ const doc = await ctx.db
158
+ .query("worldQueueMessages")
159
+ .withIndex("by_org_message", (q) => q.eq("orgId", orgId).eq("messageId", args.messageId))
160
+ .unique();
161
+ if (!doc || doc.orgId !== orgId) {
162
+ return null;
163
+ }
164
+ return {
165
+ messageId: doc.messageId,
166
+ queueName: doc.queueName,
167
+ payloadJson: doc.payloadJson,
168
+ idempotencyKey: doc.idempotencyKey,
169
+ attempt: doc.attempt,
170
+ status: doc.status,
171
+ deliverAt: doc.deliverAt,
172
+ createdAt: doc.createdAt,
173
+ updatedAt: doc.updatedAt,
174
+ };
175
+ },
176
+ });
177
+ export const listPending = query({
178
+ args: { orgId: v.string(), queueNamePrefix: v.string() },
179
+ returns: v.array(queueMessageResult),
180
+ handler: async (ctx, args) => {
181
+ const orgId = requireOrgId(args.orgId);
182
+ const docs = await ctx.db
183
+ .query("worldQueueMessages")
184
+ .withIndex("by_org", (q) => q.eq("orgId", orgId))
185
+ .filter((q) => q.eq(q.field("status"), "pending"))
186
+ .collect();
187
+ return docs
188
+ .filter((doc) => doc.queueName.startsWith(args.queueNamePrefix))
189
+ .map((doc) => ({
190
+ messageId: doc.messageId,
191
+ queueName: doc.queueName,
192
+ payloadJson: doc.payloadJson,
193
+ idempotencyKey: doc.idempotencyKey,
194
+ attempt: doc.attempt,
195
+ status: doc.status,
196
+ deliverAt: doc.deliverAt,
197
+ createdAt: doc.createdAt,
198
+ updatedAt: doc.updatedAt,
199
+ }));
200
+ },
201
+ });
@@ -1,6 +1,6 @@
1
1
  export declare const resolveManifest: import("convex/server").RegisteredQuery<"public", {
2
2
  orgId: string;
3
- sessionId: import("convex/values").GenericId<"sessions">;
3
+ sessionId: string;
4
4
  }, Promise<{
5
5
  agentId: import("convex/values").GenericId<"agents">;
6
6
  name: string;
@@ -1,21 +1,9 @@
1
1
  import { v } from "convex/values";
2
2
  import { query } from "../_generated/server.js";
3
3
  import { assertSameOrg, requireOrgId } from "../lib/auth.js";
4
- // resolveManifest — the dynamic resolver (eve parity:
5
- // `dynamicInstructions`/`dynamicSkills`/`dynamicTools`,
6
- // packages/eve/src/compiler/manifest.ts:722-725). Produced FRESH from the
7
- // aggregate on every call, never compiled/cached — an agent edited between
8
- // sessions is visible on the very next `session.started`/`turn.started`
9
- // call with zero redeploy. `sessions.start` and `sessions.sendTurn` both
10
- // call this at their respective boundaries (ALLOWED_DYNAMIC_INSTRUCTION_
11
- // EVENTS, dynamic-tool-definition.ts:40-46).
12
- //
13
- // T2-QUINQUIES scope note: `toolApprovals` is included (in scope per
14
- // brief). `skills`/`connections` are NOT (out of scope for this pass — see
15
- // component/schema.ts SCOPE note); this resolver returns only what the
16
- // component boundary currently owns.
4
+ import { agentIdOfRun, loadRun } from "./world.js";
17
5
  export const resolveManifest = query({
18
- args: { orgId: v.string(), sessionId: v.id("sessions") },
6
+ args: { orgId: v.string(), sessionId: v.string() },
19
7
  returns: v.object({
20
8
  agentId: v.id("agents"),
21
9
  name: v.string(),
@@ -28,13 +16,13 @@ export const resolveManifest = query({
28
16
  }),
29
17
  handler: async (ctx, args) => {
30
18
  const orgId = requireOrgId(args.orgId);
31
- const session = await ctx.db.get(args.sessionId);
32
- assertSameOrg(orgId, session, "Session not found");
33
- const agent = await ctx.db.get(session.agentId);
19
+ const run = await loadRun(ctx, orgId, args.sessionId, "session", "Session not found");
20
+ const agentId = agentIdOfRun(ctx, run);
21
+ const agent = await ctx.db.get(agentId);
34
22
  assertSameOrg(orgId, agent, "Agent not found");
35
23
  const toolApprovals = await ctx.db
36
24
  .query("toolApprovals")
37
- .withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", session.agentId))
25
+ .withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", agentId))
38
26
  .collect();
39
27
  return {
40
28
  agentId: agent._id,
@@ -1,7 +1,7 @@
1
1
  export declare const startRun: import("convex/server").RegisteredMutation<"public", {
2
2
  orgId: string;
3
- workflowId: string;
4
3
  awaitedEventName: string;
4
+ workflowId: string;
5
5
  timeoutMs: number;
6
6
  }, Promise<import("convex/values").GenericId<"sandboxRuns">>>;
7
7
  export declare const attachSandboxId: import("convex/server").RegisteredMutation<"public", {
@@ -13,7 +13,7 @@ export declare const getStatus: import("convex/server").RegisteredQuery<"public"
13
13
  orgId: string;
14
14
  sandboxRunId: import("convex/values").GenericId<"sandboxRuns">;
15
15
  }, Promise<{
16
- status: "completed" | "pending" | "failed" | "timedOut";
16
+ status: "completed" | "failed" | "pending" | "timedOut";
17
17
  resultSummary: string | undefined;
18
18
  }>>;
19
19
  export declare const deadLetterMalformed: import("convex/server").RegisteredMutation<"public", {
@@ -3,47 +3,8 @@ import { v } from "convex/values";
3
3
  import { components, internal } from "../_generated/api.js";
4
4
  import { internalMutation, mutation, query } from "../_generated/server.js";
5
5
  import { assertSameOrg, requireOrgId } from "../lib/auth.js";
6
- // engine/sandboxCallback — THE SIGNED CALLBACK GATE'S DB-SIDE LOGIC.
7
- //
8
- // BOUNDARY (see the sandbox-bridge header in component/schema.ts for the
9
- // measured verdict behind each of these):
10
- // - The public HTTP route (`/sandbox/callback`) that verifies the
11
- // `SANDBOX_CALLBACK_SECRET` HMAC signature over the raw body CANNOT
12
- // live in this component (no official `@convex-dev/*` component ships
13
- // an `http.ts`, and none reads `process.env` directly). It stays in
14
- // the consumer app's `convex/http.ts`, which reads the secret, verifies
15
- // the signature, and ONLY THEN calls `processCallback` /
16
- // `deadLetterMalformed` below.
17
- // - The "use node" action that talks to `@vercel/sandbox`
18
- // (`Sandbox.create()` / `sandbox.runCommand()`) CANNOT live in this
19
- // component either (Convex's own bundler hard-crashes on any "use
20
- // node" file inside a child component). It stays app-level
21
- // and calls INTO `startRun` / `attachSandboxId` below (both PUBLIC
22
- // `mutation`s, not `internalMutation`s, precisely so a cross-component
23
- // caller can reach them — a component's `internal*` functions are
24
- // unreachable from outside the component; engine/mandates.ts's
25
- // `spendInternal` follows the same convention).
26
- //
27
- // Everything below assumes the signature already checked out (verified by
28
- // the caller, upstream of this module) — this module's job is dedup-by-
29
- // delivery-id + the actual wake, both inside ONE mutation transaction so a
30
- // replayed delivery can never cause a second `sendEvent`. `orgId` is an
31
- // explicit ARGUMENT on every public function (component boundaries do not
32
- // propagate `ctx.auth`, see component/lib/auth.ts).
33
- //
34
- // Same WorkflowManager component as engine/wakeup.ts / engine/workflows.ts.
35
6
  const workflow = new WorkflowManager(components.workflow);
36
7
  const wakeStatus = v.union(v.literal("completed"), v.literal("failed"));
37
- // startRun — the DB-side half of launching a sandbox run (called by the
38
- // app-level "use node" action, convex/engine/sandboxBridgeActions.ts,
39
- // AFTER `engine.mandates.spend` has funded it, same two-transaction
40
- // protocol as `engine.wakeup.enqueue`). Returns the row id so the caller
41
- // can inject it into the sandbox's own env (SANDBOX_RUN_ID) before
42
- // `Sandbox.create()`, and schedules the self-built awaitEvent timeout
43
- // (workflow#177 pattern, same discipline as engine/awaitEventTimeout.ts)
44
- // so a sandbox that never calls back — killed by the platform's own
45
- // ceiling, crashed, or just lost — still wakes the workflow, in FAILURE,
46
- // rather than leaving it asleep forever.
47
8
  export const startRun = mutation({
48
9
  args: {
49
10
  orgId: v.string(),
@@ -68,7 +29,7 @@ export const startRun = mutation({
68
29
  orgId,
69
30
  workflowId: args.workflowId,
70
31
  awaitedEventName: args.awaitedEventName,
71
- sandboxId: "", // patched by attachSandboxId once Sandbox.create() resolves
32
+ sandboxId: "",
72
33
  status: "pending",
73
34
  createdAt: now,
74
35
  updatedAt: now,
@@ -79,10 +40,6 @@ export const startRun = mutation({
79
40
  return runId;
80
41
  },
81
42
  });
82
- // attachSandboxId — patches the Vercel-assigned sandbox id onto the row
83
- // once `Sandbox.create()` returns it. Separate step because the row must
84
- // exist BEFORE the sandbox is created (its id is one of the sandbox's own
85
- // env vars, so the sandbox can name itself in its callback payload).
86
43
  export const attachSandboxId = mutation({
87
44
  args: { orgId: v.string(), sandboxRunId: v.id("sandboxRuns"), sandboxId: v.string() },
88
45
  returns: v.null(),
@@ -100,8 +57,6 @@ export const attachSandboxId = mutation({
100
57
  return null;
101
58
  },
102
59
  });
103
- // getStatus — client-facing read, same org-isolation discipline as every
104
- // other status query in this engine.
105
60
  export const getStatus = query({
106
61
  args: { orgId: v.string(), sandboxRunId: v.id("sandboxRuns") },
107
62
  returns: v.object({
@@ -115,14 +70,6 @@ export const getStatus = query({
115
70
  return { status: row.status, resultSummary: row.resultSummary };
116
71
  },
117
72
  });
118
- // deadLetterMalformed — reached from the consumer app's `convex/http.ts`
119
- // when a SIGNATURE-VALID request's body is not valid JSON or is missing
120
- // required fields. The signature already proved the caller holds the
121
- // shared secret, so this is not a security refusal (that already happened
122
- // upstream, in the app's own HTTP route, before this ever runs) — it is a
123
- // shape problem worth a human's attention, recorded rather than dropped.
124
- // `orgId` is optional: the payload may not have parsed far enough to know
125
- // which org this delivery even claims to belong to.
126
73
  export const deadLetterMalformed = mutation({
127
74
  args: {
128
75
  orgId: v.optional(v.string()),
@@ -142,15 +89,6 @@ export const deadLetterMalformed = mutation({
142
89
  return null;
143
90
  },
144
91
  });
145
- // processCallback — THE WAKE, reached ONLY from the consumer app's
146
- // `convex/http.ts` AFTER signature verification. `deliveryId` dedup is the
147
- // FIRST read in this transaction, and the "woke" delivery row is inserted
148
- // in the SAME transaction as the `sendEvent` call — Convex mutations are
149
- // single transactions, so there is no window in which two concurrent
150
- // replays of the same `deliveryId` could both observe "not yet delivered"
151
- // and both wake the workflow. A delivery whose signature verified but
152
- // whose `sandboxRunId` is unknown, cross-org, or already resolved is
153
- // DEAD-LETTERED rather than silently dropped or blindly retried.
154
92
  export const processCallback = mutation({
155
93
  args: {
156
94
  deliveryId: v.string(),
@@ -167,7 +105,6 @@ export const processCallback = mutation({
167
105
  if (args.deliveryId.length === 0) {
168
106
  throw new Error("Validation: deliveryId must not be empty");
169
107
  }
170
- // Dedup FIRST, inside this same transaction.
171
108
  const existingDelivery = await ctx.db
172
109
  .query("sandboxCallbackDeliveries")
173
110
  .withIndex("by_org_delivery", (q) => q.eq("orgId", orgId).eq("deliveryId", args.deliveryId))
@@ -188,10 +125,6 @@ export const processCallback = mutation({
188
125
  return { outcome: "dead-lettered" };
189
126
  }
190
127
  if (run.status !== "pending") {
191
- // Already resolved (completed/failed/timedOut) — a signature-valid
192
- // callback for a run that is no longer pending is not a legitimate
193
- // second wake; record it for human inspection instead of firing
194
- // sendEvent again or throwing an unreadable error.
195
128
  await ctx.db.insert("sandboxCallbackDeadLetters", {
196
129
  orgId,
197
130
  deliveryId: args.deliveryId,
@@ -222,23 +155,13 @@ export const processCallback = mutation({
222
155
  return { outcome: "woke" };
223
156
  },
224
157
  });
225
- // timeoutInternal — the self-built awaitEvent timeout (workflow#177
226
- // pattern, same discipline as engine/awaitEventTimeout.ts), scoped to a
227
- // sandbox run rather than a session. INTERNAL: reached only via
228
- // `ctx.scheduler.runAfter` from `startRun` above, inside this SAME
229
- // component — never a cross-component call, so `internalMutation` is
230
- // correct here (unlike `startRun`/`attachSandboxId`/`processCallback`/
231
- // `deadLetterMalformed`, which must be reachable from outside). Fires ONLY
232
- // if the run is still "pending" when the deadline arrives (idempotent
233
- // against a callback that lands in the same tick); wakes the workflow in a
234
- // CLEAN FAILURE, not a silent hang.
235
158
  export const timeoutInternal = internalMutation({
236
159
  args: { sandboxRunId: v.id("sandboxRuns") },
237
160
  returns: v.null(),
238
161
  handler: async (ctx, args) => {
239
162
  const run = await ctx.db.get(args.sandboxRunId);
240
163
  if (!run || run.status !== "pending") {
241
- return null; // already resolved by a real callback, or row gone
164
+ return null;
242
165
  }
243
166
  await ctx.db.patch(run._id, {
244
167
  status: "timedOut",
@@ -0,0 +1,9 @@
1
+ export declare const getBoxId: import("convex/server").RegisteredQuery<"public", {
2
+ orgId: string;
3
+ agentId: import("convex/values").GenericId<"agents">;
4
+ }, Promise<string | null>>;
5
+ export declare const setBoxId: import("convex/server").RegisteredMutation<"public", {
6
+ orgId: string;
7
+ agentId: import("convex/values").GenericId<"agents">;
8
+ boxId: string;
9
+ }, Promise<null>>;