@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,46 +2,7 @@ import { v } from "convex/values";
|
|
|
2
2
|
import { api, internal } from "../_generated/api.js";
|
|
3
3
|
import { action, internalMutation, internalQuery, mutation, query, } from "../_generated/server.js";
|
|
4
4
|
import { assertSameOrg, requireOrgId } from "../lib/auth.js";
|
|
5
|
-
// engine/delegation — an agent delegating to another agent uses THE SAME
|
|
6
|
-
// pause/wake system (awaitEvent/sendEvent) as the sandbox bridge: one
|
|
7
|
-
// system to maintain, not two. Every delegation path below (declared
|
|
8
|
-
// subagent AND the built-in `agent` clone tool) terminates in exactly ONE
|
|
9
|
-
// call to `engine.wakeup.enqueue` -- the same public action this component
|
|
10
|
-
// already ships, imported here and never reimplemented.
|
|
11
|
-
// grep proof: this file makes zero calls to `wakeupPool.*` / `workflow.*`
|
|
12
|
-
// directly; the only wake-capable call in this module is
|
|
13
|
-
// `api.engine.wakeup.enqueue` at the bottom of `delegateToSubagent` and
|
|
14
|
-
// `invokeAgentClone`.
|
|
15
|
-
//
|
|
16
|
-
// orgId discipline (component/lib/auth.ts): every PUBLIC function takes
|
|
17
|
-
// `orgId` as an explicit ARGUMENT (auth does not propagate across a
|
|
18
|
-
// component boundary), same as engine/mandates.ts / engine/wakeup.ts.
|
|
19
|
-
// `delegateToSubagent`/`invokeAgentClone` are `action`s because they each
|
|
20
|
-
// call `api.engine.wakeup.enqueue` (itself an action: the two-transaction
|
|
21
|
-
// funding protocol) -- `orgId` is threaded explicitly through
|
|
22
|
-
// every hop, never relying on any identity propagation across the boundary.
|
|
23
|
-
// Reserved tool name for the built-in, root-only, non-recursive clone tool
|
|
24
|
-
// (eve.dev/docs/subagents). A declared subagent may never claim this name --
|
|
25
|
-
// that would silently shadow the built-in, which eve explicitly refuses to
|
|
26
|
-
// do ("eve rejects the build rather than picking a winner").
|
|
27
5
|
export const BUILTIN_AGENT_TOOL_NAME = "agent";
|
|
28
|
-
// ---------------------------------------------------------------------
|
|
29
|
-
// (a.1) DECLARED SUBAGENTS -- strict isolation, build-time collision guard
|
|
30
|
-
// ---------------------------------------------------------------------
|
|
31
|
-
// declareSubagent — registers a declared subagent as a NAMED tool on the
|
|
32
|
-
// parent. Isolation is by construction: this mutation writes ONLY the
|
|
33
|
-
// parent->child link + toolName; it never copies a single field out of the
|
|
34
|
-
// child's own `skills`/`connections`/`toolApprovals` rows onto the parent,
|
|
35
|
-
// and never will -- a declared subagent's own aggregate is the ONLY place
|
|
36
|
-
// its slots ever live (eve.dev/docs/subagents, verbatim: "a declared
|
|
37
|
-
// subagent inherits nothing from the root's authored slots").
|
|
38
|
-
//
|
|
39
|
-
// BUILD-TIME COLLISION GUARD: `toolName` must be unique among (1) the
|
|
40
|
-
// reserved built-in `agent` tool name, (2) every OTHER declared subagent of
|
|
41
|
-
// this same parent, and (3) every `toolApprovals` row already registered
|
|
42
|
-
// for this parent (a toolApprovals row is how this engine represents "a
|
|
43
|
-
// named tool this agent already exposes"). Any collision REJECTS the
|
|
44
|
-
// mutation -- the row is never written, there is no silent "last one wins".
|
|
45
6
|
export const declareSubagent = mutation({
|
|
46
7
|
args: {
|
|
47
8
|
orgId: v.string(),
|
|
@@ -119,18 +80,6 @@ export const listSubagents = query({
|
|
|
119
80
|
.collect();
|
|
120
81
|
},
|
|
121
82
|
});
|
|
122
|
-
// ---------------------------------------------------------------------
|
|
123
|
-
// (a.2) DELEGATION -- reuses `engine.wakeup.enqueue` end to end
|
|
124
|
-
// ---------------------------------------------------------------------
|
|
125
|
-
// delegateToSubagent — A delegates to a declared subagent B by the SAME
|
|
126
|
-
// pause/wake mechanism the sandbox bridge uses: it enqueues the
|
|
127
|
-
// caller-supplied `workFunctionHandle` (an action that performs B's turn)
|
|
128
|
-
// through `api.engine.wakeup.enqueue`. That enqueue funds through
|
|
129
|
-
// `mandates.spendInternal` (the funding choke point, untouched), puts the work
|
|
130
|
-
// into `wakeupPool`, and when it resolves, `engine.wakeup.onWorkComplete`
|
|
131
|
-
// calls `workflow.sendEvent` (workflowId, awaitedEventName, ...) -- waking
|
|
132
|
-
// whatever workflow step A's own session parked on `step.awaitEvent`. There
|
|
133
|
-
// is no second onComplete/sendEvent path anywhere in this file.
|
|
134
83
|
export const delegateToSubagent = action({
|
|
135
84
|
args: {
|
|
136
85
|
orgId: v.string(),
|
|
@@ -164,10 +113,6 @@ export const delegateToSubagent = action({
|
|
|
164
113
|
workflowId: args.workflowId,
|
|
165
114
|
awaitedEventName: args.awaitedEventName,
|
|
166
115
|
});
|
|
167
|
-
// THE reuse point: no bespoke pause/wake code below this line -- this
|
|
168
|
-
// is a direct call into this component's own PUBLIC `engine.wakeup.enqueue`
|
|
169
|
-
// action. `orgId` is threaded explicitly (no identity propagation
|
|
170
|
-
// across the component boundary).
|
|
171
116
|
await ctx.runAction(api.engine.wakeup.enqueue, {
|
|
172
117
|
orgId,
|
|
173
118
|
agentId: subagent.childAgentId,
|
|
@@ -182,19 +127,6 @@ export const delegateToSubagent = action({
|
|
|
182
127
|
return delegationRunId;
|
|
183
128
|
},
|
|
184
129
|
});
|
|
185
|
-
// ---------------------------------------------------------------------
|
|
186
|
-
// (a.3) BUILT-IN `agent` TOOL -- root-only clone, NOT recursive
|
|
187
|
-
// ---------------------------------------------------------------------
|
|
188
|
-
// invokeAgentClone — the built-in `agent` tool (eve.dev/docs/subagents): a
|
|
189
|
-
// clone of the ROOT agent, invocable by the root itself, but the clone
|
|
190
|
-
// CANNOT call itself again. `viaAgentTool` is the recursion guard: the
|
|
191
|
-
// root's own first call always passes `viaAgentTool: false` (or omits it).
|
|
192
|
-
// Anything invoking this action must set `viaAgentTool: true` on any call
|
|
193
|
-
// made FROM INSIDE a clone's own work -- and that is exactly the value this
|
|
194
|
-
// handler refuses. There is no code path in this engine that can reach the
|
|
195
|
-
// pool a second time for the same clone chain: the guard runs BEFORE
|
|
196
|
-
// `wakeup.enqueue` is ever called, so a recursive attempt never enters the
|
|
197
|
-
// workpool at all.
|
|
198
130
|
export const invokeAgentClone = action({
|
|
199
131
|
args: {
|
|
200
132
|
orgId: v.string(),
|
|
@@ -229,8 +161,6 @@ export const invokeAgentClone = action({
|
|
|
229
161
|
workflowId: args.workflowId,
|
|
230
162
|
awaitedEventName: args.awaitedEventName,
|
|
231
163
|
});
|
|
232
|
-
// Same reuse point as `delegateToSubagent`: one call into the SAME
|
|
233
|
-
// public `engine.wakeup.enqueue`, nothing bespoke.
|
|
234
164
|
await ctx.runAction(api.engine.wakeup.enqueue, {
|
|
235
165
|
orgId,
|
|
236
166
|
agentId: args.agentId,
|
|
@@ -245,10 +175,6 @@ export const invokeAgentClone = action({
|
|
|
245
175
|
return cloneRunId;
|
|
246
176
|
},
|
|
247
177
|
});
|
|
248
|
-
// ---------------------------------------------------------------------
|
|
249
|
-
// INTERNAL helpers -- unreachable from any client, same discipline as
|
|
250
|
-
// `engine/wakeup.enqueueInternal`.
|
|
251
|
-
// ---------------------------------------------------------------------
|
|
252
178
|
export const getSubagentInternal = internalQuery({
|
|
253
179
|
args: { orgId: v.string(), subagentId: v.id("subagents") },
|
|
254
180
|
returns: v.union(v.object({
|
|
@@ -1,24 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { query } from "../_generated/server.js";
|
|
3
3
|
import { requireOrgId } from "../lib/auth.js";
|
|
4
|
-
// engine/directory — component-boundary port of the app-level
|
|
5
|
-
// `convex/directory.ts:listByOrg`.
|
|
6
|
-
//
|
|
7
|
-
// Trust-boundary divergence from the app-level prototype (see
|
|
8
|
-
// component/lib/auth.ts): auth does NOT propagate across a component
|
|
9
|
-
// boundary, so `orgId` is a caller-supplied ARGUMENT here (deny by default
|
|
10
|
-
// via `requireOrgId(args.orgId)`), never read from `ctx.auth`. The consumer
|
|
11
|
-
// app has already authenticated its user and resolved orgId on its own
|
|
12
|
-
// side before calling in.
|
|
13
|
-
//
|
|
14
|
-
// Shape divergence: the app-level prototype also reports
|
|
15
|
-
// `subagentCount`/`connectionCount` by joining the `subagents` and
|
|
16
|
-
// `connections` tables. Those tables are NOT (yet) re-hosted inside this
|
|
17
|
-
// component boundary (see component/schema.ts SCOPE note) -- porting a
|
|
18
|
-
// join against tables this component does not own would violate component
|
|
19
|
-
// isolation. `listByOrg` here therefore returns only what this component's
|
|
20
|
-
// own `agents` table can answer; counts can be added once `subagents`/
|
|
21
|
-
// `connections` gain their own component-level test harness.
|
|
22
4
|
export const listByOrg = query({
|
|
23
5
|
args: { orgId: v.string() },
|
|
24
6
|
returns: v.array(v.object({
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { WorkflowManager, type WorkflowId } from "@convex-dev/workflow";
|
|
2
|
+
type JobRunStatus = "running" | "completed" | "abandoned" | "cancelled";
|
|
2
3
|
export declare const workflow: WorkflowManager;
|
|
3
4
|
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
4
5
|
orgId: string;
|
|
6
|
+
kind: string;
|
|
5
7
|
stepFunctionHandle: string;
|
|
6
8
|
totalSteps: number;
|
|
7
9
|
maxAttemptsPerStep: number;
|
|
8
|
-
|
|
9
|
-
}, Promise<import("convex/values").GenericId<"durableJobs">>>;
|
|
10
|
+
}, Promise<string>>;
|
|
10
11
|
export declare const getStatus: import("convex/server").RegisteredQuery<"public", {
|
|
11
12
|
orgId: string;
|
|
12
|
-
jobId:
|
|
13
|
+
jobId: string;
|
|
13
14
|
}, Promise<{
|
|
14
|
-
status:
|
|
15
|
+
status: JobRunStatus;
|
|
15
16
|
cursor: number;
|
|
16
17
|
totalSteps: number;
|
|
17
18
|
lastError: string | undefined;
|
|
@@ -19,28 +20,34 @@ export declare const getStatus: import("convex/server").RegisteredQuery<"public"
|
|
|
19
20
|
}>>;
|
|
20
21
|
export declare const cancel: import("convex/server").RegisteredMutation<"public", {
|
|
21
22
|
orgId: string;
|
|
22
|
-
jobId:
|
|
23
|
+
jobId: string;
|
|
23
24
|
}, Promise<null>>;
|
|
24
|
-
export declare const
|
|
25
|
-
|
|
25
|
+
export declare const getJobInternalV2: import("convex/server").RegisteredQuery<"internal", {
|
|
26
|
+
orgId: string;
|
|
27
|
+
runId: string;
|
|
26
28
|
}, Promise<{
|
|
27
29
|
cursor: number;
|
|
28
30
|
totalSteps: number;
|
|
29
31
|
maxAttemptsPerStep: number;
|
|
30
|
-
status:
|
|
32
|
+
status: JobRunStatus;
|
|
31
33
|
} | null>>;
|
|
32
|
-
export declare const
|
|
33
|
-
|
|
34
|
+
export declare const runStepInternalV2: import("convex/server").RegisteredMutation<"internal", {
|
|
35
|
+
orgId: string;
|
|
36
|
+
runId: string;
|
|
34
37
|
stepIndex: number;
|
|
35
38
|
}, Promise<null>>;
|
|
36
|
-
export declare const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
export declare const markAbandonedInternalV2: import("convex/server").RegisteredMutation<"internal", {
|
|
40
|
+
orgId: string;
|
|
41
|
+
runId: string;
|
|
39
42
|
error: string;
|
|
43
|
+
stepIndex: number;
|
|
40
44
|
}, Promise<null>>;
|
|
41
|
-
export declare const
|
|
42
|
-
|
|
45
|
+
export declare const markCompletedInternalV2: import("convex/server").RegisteredMutation<"internal", {
|
|
46
|
+
orgId: string;
|
|
47
|
+
runId: string;
|
|
43
48
|
}, Promise<null>>;
|
|
44
|
-
export declare const
|
|
45
|
-
|
|
49
|
+
export declare const durableJobWorkflowV2: import("convex/server").RegisteredMutation<"internal", import("@convex-dev/workflow").WorkflowArgs<{
|
|
50
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
51
|
+
runId: import("convex/values").VString<string, "required">;
|
|
46
52
|
}>, WorkflowId>;
|
|
53
|
+
export {};
|
|
@@ -2,52 +2,38 @@ import { v } from "convex/values";
|
|
|
2
2
|
import { WorkflowManager } from "@convex-dev/workflow";
|
|
3
3
|
import { components, internal } from "../_generated/api.js";
|
|
4
4
|
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// - ABANDON AFTER N: `runStepInternal` retries the caller's step
|
|
37
|
-
// function up to `maxAttemptsPerStep` times INSIDE one workflow
|
|
38
|
-
// step (not via workflow-level retry, which does not apply to
|
|
39
|
-
// mutations); only once attempts are exhausted does the step throw,
|
|
40
|
-
// which the workflow handler catches and turns into
|
|
41
|
-
// `status: "abandoned"` with `abandonedAtStep` + `lastError`.
|
|
42
|
-
//
|
|
43
|
-
// Versioned workflow discipline (workflow#35) -- same as
|
|
44
|
-
// `engine/workflows.ts:sessionWorkflowV1`: never edit `durableJobWorkflowV1`
|
|
45
|
-
// in place once jobs reference it; ship `...V2` and drain v1 first.
|
|
46
|
-
const CURRENT_WORKFLOW_VERSION = "v1";
|
|
5
|
+
import { requireOrgId } from "../lib/auth.js";
|
|
6
|
+
import { appendEventCore, bindWorkflowToRun, loadRun, openRun, workflowOfRun, } from "./world.js";
|
|
7
|
+
const CURRENT_WORKFLOW_VERSION = "v2";
|
|
8
|
+
async function readJob(ctx, orgId, runId, notFoundMessage = "Job not found") {
|
|
9
|
+
const run = await loadRun(ctx, orgId, runId, "job", notFoundMessage);
|
|
10
|
+
const num = (field) => {
|
|
11
|
+
const value = run.created[field];
|
|
12
|
+
if (typeof value !== "number") {
|
|
13
|
+
throw new Error(`Job run_created payload is missing ${field}`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const stepHandle = run.created.stepFunctionHandle;
|
|
18
|
+
if (typeof stepHandle !== "string") {
|
|
19
|
+
throw new Error("Job run_created payload is missing stepFunctionHandle");
|
|
20
|
+
}
|
|
21
|
+
const steps = await ctx.db
|
|
22
|
+
.query("worldSteps")
|
|
23
|
+
.withIndex("by_org_run", (q) => q.eq("orgId", orgId).eq("runId", runId))
|
|
24
|
+
.collect();
|
|
25
|
+
return {
|
|
26
|
+
run,
|
|
27
|
+
totalSteps: num("totalSteps"),
|
|
28
|
+
maxAttemptsPerStep: num("maxAttemptsPerStep"),
|
|
29
|
+
stepFunctionHandle: stepHandle,
|
|
30
|
+
cursor: steps.filter((s) => s.status === "completed").length,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function publicStatus(status) {
|
|
34
|
+
return status === "failed" ? "abandoned" : status;
|
|
35
|
+
}
|
|
47
36
|
export const workflow = new WorkflowManager(components.workflow);
|
|
48
|
-
// ---------------------------------------------------------------------
|
|
49
|
-
// PUBLIC CONTRACT
|
|
50
|
-
// ---------------------------------------------------------------------
|
|
51
37
|
export const start = mutation({
|
|
52
38
|
args: {
|
|
53
39
|
orgId: v.string(),
|
|
@@ -56,7 +42,7 @@ export const start = mutation({
|
|
|
56
42
|
totalSteps: v.number(),
|
|
57
43
|
maxAttemptsPerStep: v.number(),
|
|
58
44
|
},
|
|
59
|
-
returns: v.
|
|
45
|
+
returns: v.string(),
|
|
60
46
|
handler: async (ctx, args) => {
|
|
61
47
|
const orgId = requireOrgId(args.orgId);
|
|
62
48
|
if (!Number.isInteger(args.totalSteps) || args.totalSteps <= 0) {
|
|
@@ -69,26 +55,29 @@ export const start = mutation({
|
|
|
69
55
|
if (args.stepFunctionHandle.length === 0) {
|
|
70
56
|
throw new Error("Validation: stepFunctionHandle must not be empty");
|
|
71
57
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
58
|
+
const runId = await openRun(ctx, {
|
|
59
|
+
orgId,
|
|
60
|
+
kind: "job",
|
|
61
|
+
data: {
|
|
62
|
+
kind: args.kind,
|
|
63
|
+
stepFunctionHandle: args.stepFunctionHandle,
|
|
64
|
+
totalSteps: args.totalSteps,
|
|
65
|
+
maxAttemptsPerStep: args.maxAttemptsPerStep,
|
|
66
|
+
workflowVersion: CURRENT_WORKFLOW_VERSION,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const workflowId = await workflow.start(ctx, internal.engine.durableJob.durableJobWorkflowV2, { orgId, runId });
|
|
70
|
+
await bindWorkflowToRun(ctx, {
|
|
74
71
|
orgId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
totalSteps: args.totalSteps,
|
|
78
|
-
maxAttemptsPerStep: args.maxAttemptsPerStep,
|
|
79
|
-
cursor: 0,
|
|
80
|
-
status: "running",
|
|
72
|
+
runId,
|
|
73
|
+
workflowId,
|
|
81
74
|
workflowVersion: CURRENT_WORKFLOW_VERSION,
|
|
82
|
-
createdAt: now,
|
|
83
|
-
updatedAt: now,
|
|
84
75
|
});
|
|
85
|
-
|
|
86
|
-
await ctx.db.patch(jobId, { workflowId });
|
|
87
|
-
return jobId;
|
|
76
|
+
return runId;
|
|
88
77
|
},
|
|
89
78
|
});
|
|
90
79
|
export const getStatus = query({
|
|
91
|
-
args: { orgId: v.string(), jobId: v.
|
|
80
|
+
args: { orgId: v.string(), jobId: v.string() },
|
|
92
81
|
returns: v.object({
|
|
93
82
|
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
94
83
|
cursor: v.number(),
|
|
@@ -98,44 +87,61 @@ export const getStatus = query({
|
|
|
98
87
|
}),
|
|
99
88
|
handler: async (ctx, args) => {
|
|
100
89
|
const orgId = requireOrgId(args.orgId);
|
|
101
|
-
const job = await ctx
|
|
102
|
-
|
|
90
|
+
const job = await readJob(ctx, orgId, args.jobId);
|
|
91
|
+
const status = publicStatus(job.run.status);
|
|
92
|
+
let lastError;
|
|
93
|
+
let abandonedAtStep;
|
|
94
|
+
if (job.run.status === "failed") {
|
|
95
|
+
const journal = await ctx.db
|
|
96
|
+
.query("worldEvents")
|
|
97
|
+
.withIndex("by_org_run_seq", (q) => q.eq("orgId", orgId).eq("runId", job.run.runId))
|
|
98
|
+
.collect();
|
|
99
|
+
const failed = [...journal].reverse().find((e) => e.eventType === "run_failed");
|
|
100
|
+
if (failed) {
|
|
101
|
+
try {
|
|
102
|
+
const data = JSON.parse(failed.dataJson);
|
|
103
|
+
if (typeof data.lastError === "string")
|
|
104
|
+
lastError = data.lastError;
|
|
105
|
+
if (typeof data.abandonedAtStep === "number") {
|
|
106
|
+
abandonedAtStep = data.abandonedAtStep;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
103
113
|
return {
|
|
104
|
-
status
|
|
114
|
+
status,
|
|
105
115
|
cursor: job.cursor,
|
|
106
116
|
totalSteps: job.totalSteps,
|
|
107
|
-
lastError
|
|
108
|
-
abandonedAtStep
|
|
117
|
+
lastError,
|
|
118
|
+
abandonedAtStep,
|
|
109
119
|
};
|
|
110
120
|
},
|
|
111
121
|
});
|
|
112
122
|
export const cancel = mutation({
|
|
113
|
-
args: { orgId: v.string(), jobId: v.
|
|
123
|
+
args: { orgId: v.string(), jobId: v.string() },
|
|
114
124
|
returns: v.null(),
|
|
115
125
|
handler: async (ctx, args) => {
|
|
116
126
|
const orgId = requireOrgId(args.orgId);
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
if (job.status !== "running")
|
|
127
|
+
const run = await loadRun(ctx, orgId, args.jobId, "job", "Job not found");
|
|
128
|
+
if (run.status !== "running")
|
|
120
129
|
return null;
|
|
121
|
-
await ctx
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
await appendEventCore(ctx, {
|
|
131
|
+
orgId,
|
|
132
|
+
runId: run.runId,
|
|
133
|
+
eventType: "run_cancelled",
|
|
134
|
+
dataJson: "{}",
|
|
124
135
|
});
|
|
125
|
-
|
|
126
|
-
|
|
136
|
+
const bound = await workflowOfRun(ctx, orgId, run.runId);
|
|
137
|
+
if (bound !== null) {
|
|
138
|
+
await workflow.cancel(ctx, bound.workflowId);
|
|
127
139
|
}
|
|
128
140
|
return null;
|
|
129
141
|
},
|
|
130
142
|
});
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// The workflow runs with no end-user identity: every handler below scopes
|
|
134
|
-
// strictly by the orgId already recorded on the `durableJobs` row, never a
|
|
135
|
-
// client-supplied one.
|
|
136
|
-
// ---------------------------------------------------------------------
|
|
137
|
-
export const getJobInternal = internalQuery({
|
|
138
|
-
args: { jobId: v.id("durableJobs") },
|
|
143
|
+
export const getJobInternalV2 = internalQuery({
|
|
144
|
+
args: { orgId: v.string(), runId: v.string() },
|
|
139
145
|
returns: v.union(v.object({
|
|
140
146
|
cursor: v.number(),
|
|
141
147
|
totalSteps: v.number(),
|
|
@@ -143,43 +149,59 @@ export const getJobInternal = internalQuery({
|
|
|
143
149
|
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
144
150
|
}), v.null()),
|
|
145
151
|
handler: async (ctx, args) => {
|
|
146
|
-
const
|
|
147
|
-
|
|
152
|
+
const run = await ctx.db
|
|
153
|
+
.query("worldRuns")
|
|
154
|
+
.withIndex("by_org_run", (q) => q.eq("orgId", args.orgId).eq("runId", args.runId))
|
|
155
|
+
.unique();
|
|
156
|
+
if (!run)
|
|
148
157
|
return null;
|
|
158
|
+
const job = await readJob(ctx, args.orgId, args.runId);
|
|
149
159
|
return {
|
|
150
160
|
cursor: job.cursor,
|
|
151
161
|
totalSteps: job.totalSteps,
|
|
152
162
|
maxAttemptsPerStep: job.maxAttemptsPerStep,
|
|
153
|
-
status: job.status,
|
|
163
|
+
status: publicStatus(job.run.status),
|
|
154
164
|
};
|
|
155
165
|
},
|
|
156
166
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// resume (journal replay after a kill mid-run) never re-executes the
|
|
160
|
-
// caller's step function for completed steps. ABANDON AFTER N: retries the
|
|
161
|
-
// caller's step function up to `maxAttemptsPerStep` times before throwing
|
|
162
|
-
// (throwing is what the workflow handler below turns into "abandoned").
|
|
163
|
-
export const runStepInternal = internalMutation({
|
|
164
|
-
args: { jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
167
|
+
export const runStepInternalV2 = internalMutation({
|
|
168
|
+
args: { orgId: v.string(), runId: v.string(), stepIndex: v.number() },
|
|
165
169
|
returns: v.null(),
|
|
166
170
|
handler: async (ctx, args) => {
|
|
167
|
-
const job = await ctx.
|
|
168
|
-
if (
|
|
169
|
-
throw new Error("Job not found");
|
|
170
|
-
if (job.status === "cancelled")
|
|
171
|
+
const job = await readJob(ctx, args.orgId, args.runId);
|
|
172
|
+
if (job.run.status === "cancelled")
|
|
171
173
|
return null;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
const stepId = String(args.stepIndex);
|
|
175
|
+
const existing = await ctx.db
|
|
176
|
+
.query("worldSteps")
|
|
177
|
+
.withIndex("by_org_run_step", (q) => q.eq("orgId", args.orgId).eq("runId", args.runId).eq("stepId", stepId))
|
|
178
|
+
.unique();
|
|
179
|
+
if (existing?.status === "completed")
|
|
174
180
|
return null;
|
|
175
|
-
|
|
181
|
+
await appendEventCore(ctx, {
|
|
182
|
+
orgId: args.orgId,
|
|
183
|
+
runId: args.runId,
|
|
184
|
+
eventType: "step_created",
|
|
185
|
+
stepId,
|
|
186
|
+
dataJson: "{}",
|
|
187
|
+
});
|
|
188
|
+
await appendEventCore(ctx, {
|
|
189
|
+
orgId: args.orgId,
|
|
190
|
+
runId: args.runId,
|
|
191
|
+
eventType: "step_started",
|
|
192
|
+
stepId,
|
|
193
|
+
dataJson: "{}",
|
|
194
|
+
});
|
|
176
195
|
let lastError;
|
|
177
196
|
for (let attempt = 1; attempt <= job.maxAttemptsPerStep; attempt++) {
|
|
178
197
|
try {
|
|
179
|
-
await ctx.runMutation(job.stepFunctionHandle, { orgId:
|
|
180
|
-
await ctx
|
|
181
|
-
|
|
182
|
-
|
|
198
|
+
await ctx.runMutation(job.stepFunctionHandle, { orgId: args.orgId, jobId: args.runId, stepIndex: args.stepIndex });
|
|
199
|
+
await appendEventCore(ctx, {
|
|
200
|
+
orgId: args.orgId,
|
|
201
|
+
runId: args.runId,
|
|
202
|
+
eventType: "step_completed",
|
|
203
|
+
stepId,
|
|
204
|
+
dataJson: JSON.stringify({ attempt }),
|
|
183
205
|
});
|
|
184
206
|
return null;
|
|
185
207
|
}
|
|
@@ -192,71 +214,93 @@ export const runStepInternal = internalMutation({
|
|
|
192
214
|
: new Error(String(lastError));
|
|
193
215
|
},
|
|
194
216
|
});
|
|
195
|
-
export const
|
|
196
|
-
args: {
|
|
217
|
+
export const markAbandonedInternalV2 = internalMutation({
|
|
218
|
+
args: {
|
|
219
|
+
orgId: v.string(),
|
|
220
|
+
runId: v.string(),
|
|
221
|
+
stepIndex: v.number(),
|
|
222
|
+
error: v.string(),
|
|
223
|
+
},
|
|
197
224
|
returns: v.null(),
|
|
198
225
|
handler: async (ctx, args) => {
|
|
199
|
-
const job = await ctx.
|
|
200
|
-
if (
|
|
226
|
+
const job = await readJob(ctx, args.orgId, args.runId);
|
|
227
|
+
if (job.run.status !== "running")
|
|
201
228
|
return null;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
229
|
+
const stepId = String(args.stepIndex);
|
|
230
|
+
await appendEventCore(ctx, {
|
|
231
|
+
orgId: args.orgId,
|
|
232
|
+
runId: args.runId,
|
|
233
|
+
eventType: "step_created",
|
|
234
|
+
stepId,
|
|
235
|
+
dataJson: "{}",
|
|
236
|
+
});
|
|
237
|
+
await appendEventCore(ctx, {
|
|
238
|
+
orgId: args.orgId,
|
|
239
|
+
runId: args.runId,
|
|
240
|
+
eventType: "step_failed",
|
|
241
|
+
stepId,
|
|
242
|
+
dataJson: JSON.stringify({ error: args.error }),
|
|
243
|
+
});
|
|
244
|
+
await appendEventCore(ctx, {
|
|
245
|
+
orgId: args.orgId,
|
|
246
|
+
runId: args.runId,
|
|
247
|
+
eventType: "run_failed",
|
|
248
|
+
dataJson: JSON.stringify({
|
|
249
|
+
reason: "abandoned",
|
|
250
|
+
abandonedAtStep: args.stepIndex,
|
|
251
|
+
lastError: args.error,
|
|
252
|
+
}),
|
|
207
253
|
});
|
|
208
254
|
return null;
|
|
209
255
|
},
|
|
210
256
|
});
|
|
211
|
-
export const
|
|
212
|
-
args: {
|
|
257
|
+
export const markCompletedInternalV2 = internalMutation({
|
|
258
|
+
args: { orgId: v.string(), runId: v.string() },
|
|
213
259
|
returns: v.null(),
|
|
214
260
|
handler: async (ctx, args) => {
|
|
215
|
-
const job = await ctx.
|
|
216
|
-
if (
|
|
261
|
+
const job = await readJob(ctx, args.orgId, args.runId);
|
|
262
|
+
if (job.run.status !== "running")
|
|
217
263
|
return null;
|
|
218
|
-
await ctx
|
|
219
|
-
|
|
220
|
-
|
|
264
|
+
await appendEventCore(ctx, {
|
|
265
|
+
orgId: args.orgId,
|
|
266
|
+
runId: args.runId,
|
|
267
|
+
eventType: "run_completed",
|
|
268
|
+
dataJson: "{}",
|
|
221
269
|
});
|
|
222
270
|
return null;
|
|
223
271
|
},
|
|
224
272
|
});
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// runMutation` returns only after `@convex-dev/workflow` has persisted the
|
|
228
|
-
// journal entry, so a process kill between any two iterations loses zero
|
|
229
|
-
// progress. On resume the workflow replays already-journaled steps from
|
|
230
|
-
// its own history (no caller-visible effect, since runStepInternal is
|
|
231
|
-
// idempotent below job.cursor) and continues the loop from where it left
|
|
232
|
-
// off.
|
|
233
|
-
export const durableJobWorkflowV1 = workflow.define({
|
|
234
|
-
args: { jobId: v.id("durableJobs") },
|
|
273
|
+
export const durableJobWorkflowV2 = workflow.define({
|
|
274
|
+
args: { orgId: v.string(), runId: v.string() },
|
|
235
275
|
returns: v.null(),
|
|
236
276
|
handler: async (step, args) => {
|
|
237
|
-
const job = await step.runQuery(internal.engine.durableJob.
|
|
238
|
-
|
|
277
|
+
const job = await step.runQuery(internal.engine.durableJob.getJobInternalV2, {
|
|
278
|
+
orgId: args.orgId,
|
|
279
|
+
runId: args.runId,
|
|
239
280
|
});
|
|
240
281
|
if (!job || job.status !== "running")
|
|
241
282
|
return null;
|
|
242
283
|
for (let i = job.cursor; i < job.totalSteps; i++) {
|
|
243
284
|
try {
|
|
244
|
-
await step.runMutation(internal.engine.durableJob.
|
|
245
|
-
|
|
285
|
+
await step.runMutation(internal.engine.durableJob.runStepInternalV2, {
|
|
286
|
+
orgId: args.orgId,
|
|
287
|
+
runId: args.runId,
|
|
246
288
|
stepIndex: i,
|
|
247
289
|
});
|
|
248
290
|
}
|
|
249
291
|
catch (err) {
|
|
250
|
-
await step.runMutation(internal.engine.durableJob.
|
|
251
|
-
|
|
292
|
+
await step.runMutation(internal.engine.durableJob.markAbandonedInternalV2, {
|
|
293
|
+
orgId: args.orgId,
|
|
294
|
+
runId: args.runId,
|
|
252
295
|
stepIndex: i,
|
|
253
296
|
error: err instanceof Error ? err.message : String(err),
|
|
254
297
|
});
|
|
255
298
|
return null;
|
|
256
299
|
}
|
|
257
300
|
}
|
|
258
|
-
await step.runMutation(internal.engine.durableJob.
|
|
259
|
-
|
|
301
|
+
await step.runMutation(internal.engine.durableJob.markCompletedInternalV2, {
|
|
302
|
+
orgId: args.orgId,
|
|
303
|
+
runId: args.runId,
|
|
260
304
|
});
|
|
261
305
|
return null;
|
|
262
306
|
},
|