@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.
- package/README.md +33 -8
- package/dist/component/_generated/api.d.ts +14 -3
- package/dist/component/_generated/api.js +0 -25
- package/dist/component/_generated/component.d.ts +369 -73
- package/dist/component/_generated/component.js +0 -9
- package/dist/component/_generated/dataModel.js +0 -9
- package/dist/component/_generated/server.js +0 -69
- package/dist/component/convex.config.js +0 -44
- package/dist/component/engine/agents.d.ts +3 -3
- package/dist/component/engine/agents.js +0 -7
- package/dist/component/engine/awaitEventTimeout.d.ts +1 -1
- package/dist/component/engine/awaitEventTimeout.js +16 -26
- package/dist/component/engine/connections.d.ts +74 -0
- package/dist/component/engine/connections.js +254 -0
- package/dist/component/engine/delegation.d.ts +5 -5
- package/dist/component/engine/delegation.js +0 -74
- package/dist/component/engine/directory.js +0 -18
- package/dist/component/engine/durableJob.d.ts +24 -17
- package/dist/component/engine/durableJob.js +182 -138
- package/dist/component/engine/mandates.d.ts +2 -2
- package/dist/component/engine/mandates.js +0 -59
- package/dist/component/engine/queue.d.ts +97 -0
- package/dist/component/engine/queue.js +201 -0
- package/dist/component/engine/resolvers.d.ts +1 -1
- package/dist/component/engine/resolvers.js +6 -18
- package/dist/component/engine/sandboxCallback.d.ts +2 -2
- package/dist/component/engine/sandboxCallback.js +2 -79
- package/dist/component/engine/sandboxConfigProvider.d.ts +9 -0
- package/dist/component/engine/sandboxConfigProvider.js +32 -0
- package/dist/component/engine/sessions.d.ts +28 -10
- package/dist/component/engine/sessions.js +130 -170
- package/dist/component/engine/streams.d.ts +47 -0
- package/dist/component/engine/streams.js +162 -0
- package/dist/component/engine/toolApprovals.d.ts +1 -1
- package/dist/component/engine/toolApprovals.js +12 -37
- package/dist/component/engine/vpConnection.js +0 -40
- package/dist/component/engine/wakeup.d.ts +7 -9
- package/dist/component/engine/wakeup.js +54 -127
- package/dist/component/engine/workflows.d.ts +26 -8
- package/dist/component/engine/workflows.js +59 -64
- package/dist/component/engine/world.d.ts +250 -0
- package/dist/component/engine/world.js +689 -0
- package/dist/component/lib/auth.js +0 -26
- package/dist/component/lib/connectionSecret.d.ts +5 -0
- package/dist/component/lib/connectionSecret.js +32 -0
- package/dist/component/schema.d.ts +240 -175
- package/dist/component/schema.js +134 -294
- package/package.json +21 -6
- package/dist/component/engine/habits.d.ts +0 -70
- package/dist/component/engine/habits.js +0 -218
- package/dist/component/testFixtures/callerSteps.d.ts +0 -20
- package/dist/component/testFixtures/callerSteps.js +0 -90
- package/dist/component/testFixtures/delegationDemoWorkflow.d.ts +0 -18
- package/dist/component/testFixtures/delegationDemoWorkflow.js +0 -75
- package/dist/component/testFixtures/sandboxDemoWorkflow.d.ts +0 -19
- package/dist/component/testFixtures/sandboxDemoWorkflow.js +0 -69
- package/dist/component/testFixtures/vpTool.d.ts +0 -4
- package/dist/component/testFixtures/vpTool.js +0 -25
- package/dist/component/testFixtures/wakeupDemoWorkflow.d.ts +0 -19
- package/dist/component/testFixtures/wakeupDemoWorkflow.js +0 -77
- package/dist/component/testFixtures/wakeupWork.d.ts +0 -8
- 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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
32
|
-
|
|
33
|
-
const agent = await ctx.db.get(
|
|
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",
|
|
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" | "
|
|
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: "",
|
|
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;
|
|
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>>;
|