@vantageos/agent-engine 0.1.0-alpha.1 → 0.1.0-alpha.3
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 +198 -17
- package/dist/component/_generated/api.d.ts +50 -0
- package/dist/component/_generated/api.js +30 -0
- package/dist/component/_generated/component.d.ts +124 -0
- package/dist/component/_generated/component.js +10 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +10 -0
- package/dist/component/_generated/server.d.ts +120 -0
- package/dist/component/_generated/server.js +77 -0
- package/dist/component/convex.config.js +19 -21
- package/dist/component/engine/agents.d.ts +19 -0
- package/dist/component/engine/agents.js +67 -0
- package/dist/component/engine/durableJob.d.ts +43 -0
- package/dist/component/engine/durableJob.js +262 -0
- package/dist/component/engine/resolvers.d.ts +12 -0
- package/dist/component/engine/resolvers.js +50 -0
- package/dist/component/engine/sessions.d.ts +36 -0
- package/dist/component/engine/sessions.js +233 -0
- package/dist/component/engine/toolApprovals.d.ts +5 -0
- package/dist/component/engine/toolApprovals.js +51 -0
- package/dist/component/lib/auth.d.ts +7 -0
- package/dist/component/lib/auth.js +24 -0
- package/dist/component/schema.d.ts +124 -0
- package/dist/component/schema.js +130 -0
- package/dist/component/testFixtures/callerSteps.d.ts +20 -0
- package/dist/component/testFixtures/callerSteps.js +90 -0
- package/package.json +8 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { actionGeneric, httpActionGeneric, queryGeneric, mutationGeneric, internalActionGeneric, internalMutationGeneric, internalQueryGeneric, } from "convex/server";
|
|
11
|
+
/**
|
|
12
|
+
* Define a query in this Convex app's public API.
|
|
13
|
+
*
|
|
14
|
+
* This function will be allowed to read your Convex database and will be accessible from the client.
|
|
15
|
+
*
|
|
16
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
17
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
18
|
+
*/
|
|
19
|
+
export const query = queryGeneric;
|
|
20
|
+
/**
|
|
21
|
+
* Define a query that is only accessible from other Convex functions (but not from the client).
|
|
22
|
+
*
|
|
23
|
+
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
|
24
|
+
*
|
|
25
|
+
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
|
26
|
+
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
|
27
|
+
*/
|
|
28
|
+
export const internalQuery = internalQueryGeneric;
|
|
29
|
+
/**
|
|
30
|
+
* Define a mutation in this Convex app's public API.
|
|
31
|
+
*
|
|
32
|
+
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
|
33
|
+
*
|
|
34
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
35
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
36
|
+
*/
|
|
37
|
+
export const mutation = mutationGeneric;
|
|
38
|
+
/**
|
|
39
|
+
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
|
40
|
+
*
|
|
41
|
+
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
|
42
|
+
*
|
|
43
|
+
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
|
44
|
+
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
|
45
|
+
*/
|
|
46
|
+
export const internalMutation = internalMutationGeneric;
|
|
47
|
+
/**
|
|
48
|
+
* Define an action in this Convex app's public API.
|
|
49
|
+
*
|
|
50
|
+
* An action is a function which can execute any JavaScript code, including non-deterministic
|
|
51
|
+
* code and code with side-effects, like calling third-party services.
|
|
52
|
+
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
|
53
|
+
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
|
54
|
+
*
|
|
55
|
+
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
|
56
|
+
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
|
57
|
+
*/
|
|
58
|
+
export const action = actionGeneric;
|
|
59
|
+
/**
|
|
60
|
+
* Define an action that is only accessible from other Convex functions (but not from the client).
|
|
61
|
+
*
|
|
62
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
|
63
|
+
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
|
64
|
+
*/
|
|
65
|
+
export const internalAction = internalActionGeneric;
|
|
66
|
+
/**
|
|
67
|
+
* Define an HTTP action.
|
|
68
|
+
*
|
|
69
|
+
* The wrapped function will be used to respond to HTTP requests received
|
|
70
|
+
* by a Convex deployment if the requests matches the path and method where
|
|
71
|
+
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
|
72
|
+
*
|
|
73
|
+
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
|
74
|
+
* and a Fetch API `Request` object as its second.
|
|
75
|
+
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
|
76
|
+
*/
|
|
77
|
+
export const httpAction = httpActionGeneric;
|
|
@@ -12,28 +12,26 @@ import agent from "@convex-dev/agent/convex.config";
|
|
|
12
12
|
// import agentEngine from "@vantageos/agent-engine/convex.config";
|
|
13
13
|
// app.use(agentEngine);
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// and what makes `app.use(agentEngine)` build without throwing.
|
|
15
|
+
// This file declares agent-engine's COMPONENT IDENTITY and its
|
|
16
|
+
// sub-component composition (workflow + 2 named workpools + agent), which
|
|
17
|
+
// is what makes the `@vantageos/agent-engine/convex.config` import
|
|
18
|
+
// RESOLVE for a consumer and what makes `app.use(agentEngine)` build
|
|
19
|
+
// without throwing.
|
|
21
20
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// convex-test harness behind it. It is the next, separately-scoped task.
|
|
21
|
+
// T2-QUATER (this pass): `convex/component/engine/durableJob.ts` +
|
|
22
|
+
// `convex/component/schema.ts` (its `durableJobs` table) +
|
|
23
|
+
// `convex/component/lib/auth.ts` (orgId isolation) are now CO-LOCATED in
|
|
24
|
+
// this same directory tree, alongside this file — this is what a
|
|
25
|
+
// consumer's `npx convex dev`/`deploy` actually bundles and deploys under
|
|
26
|
+
// the component boundary, and what makes
|
|
27
|
+
// `components.agentEngine.engine.durableJob.start(...)` resolve to a real
|
|
28
|
+
// function instead of `undefined`. See README.md "What's callable" for
|
|
29
|
+
// the exact, current boundary: durableJob is IN; the rest of the agent
|
|
30
|
+
// aggregate (agents, skills, subagents, connections, habits,
|
|
31
|
+
// sandboxConfig, toolApprovals, modelKeys, sessions, sessionEvents,
|
|
32
|
+
// awaitEventTimeouts, wakeableWork) is deliberately NOT yet re-hosted
|
|
33
|
+
// here — porting those without a working component-level test harness
|
|
34
|
+
// behind each of them would violate Day 128 (never ship unverified).
|
|
37
35
|
const component = defineComponent("agentEngine");
|
|
38
36
|
component.use(workflow);
|
|
39
37
|
component.use(workpool, { name: "agentTurnPool" });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const create: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
name: string;
|
|
3
|
+
instructions: string;
|
|
4
|
+
model: string;
|
|
5
|
+
}, Promise<import("convex/values").GenericId<"agents">>>;
|
|
6
|
+
export declare const get: import("convex/server").RegisteredQuery<"public", {
|
|
7
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
8
|
+
}, Promise<{
|
|
9
|
+
_id: import("convex/values").GenericId<"agents">;
|
|
10
|
+
_creationTime: number;
|
|
11
|
+
name: string;
|
|
12
|
+
orgId: string;
|
|
13
|
+
status: "draft" | "live" | "archived";
|
|
14
|
+
createdAt: number;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
instructions: string;
|
|
17
|
+
model: string;
|
|
18
|
+
createdBy: string;
|
|
19
|
+
} | null>>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "../_generated/server";
|
|
3
|
+
import { requireOrgId } from "../lib/auth";
|
|
4
|
+
// engine/agents — MINIMAL root-sheet bootstrap for the sessions surface
|
|
5
|
+
// (T2-QUINQUIES). `sessions.start` needs an `agentId` it can read INSIDE
|
|
6
|
+
// the component boundary (component isolation: it cannot reach into a
|
|
7
|
+
// consumer app's own `agents` table). This is deliberately narrow: only
|
|
8
|
+
// `create`/`get`, no `update`/`list`/skills/connections — the full agent
|
|
9
|
+
// aggregate CRUD stays out of this component boundary for this pass (see
|
|
10
|
+
// component/schema.ts SCOPE note).
|
|
11
|
+
const agentDoc = v.object({
|
|
12
|
+
_id: v.id("agents"),
|
|
13
|
+
_creationTime: v.number(),
|
|
14
|
+
orgId: v.string(),
|
|
15
|
+
name: v.string(),
|
|
16
|
+
instructions: v.string(),
|
|
17
|
+
model: v.string(),
|
|
18
|
+
status: v.union(v.literal("draft"), v.literal("live"), v.literal("archived")),
|
|
19
|
+
createdBy: v.string(),
|
|
20
|
+
createdAt: v.number(),
|
|
21
|
+
updatedAt: v.number(),
|
|
22
|
+
});
|
|
23
|
+
export const create = mutation({
|
|
24
|
+
args: {
|
|
25
|
+
name: v.string(),
|
|
26
|
+
instructions: v.string(),
|
|
27
|
+
model: v.string(),
|
|
28
|
+
},
|
|
29
|
+
returns: v.id("agents"),
|
|
30
|
+
handler: async (ctx, args) => {
|
|
31
|
+
const orgId = await requireOrgId(ctx);
|
|
32
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
33
|
+
if (!identity)
|
|
34
|
+
throw new Error("Unauthenticated");
|
|
35
|
+
if (args.name.trim().length === 0) {
|
|
36
|
+
throw new Error("Validation: name must not be empty");
|
|
37
|
+
}
|
|
38
|
+
if (args.instructions.trim().length === 0) {
|
|
39
|
+
throw new Error("Validation: instructions must not be empty");
|
|
40
|
+
}
|
|
41
|
+
if (args.model.trim().length === 0) {
|
|
42
|
+
throw new Error("Validation: model must not be empty");
|
|
43
|
+
}
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
return await ctx.db.insert("agents", {
|
|
46
|
+
orgId,
|
|
47
|
+
name: args.name,
|
|
48
|
+
instructions: args.instructions,
|
|
49
|
+
model: args.model,
|
|
50
|
+
status: "live",
|
|
51
|
+
createdBy: identity.subject,
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
export const get = query({
|
|
58
|
+
args: { agentId: v.id("agents") },
|
|
59
|
+
returns: v.union(agentDoc, v.null()),
|
|
60
|
+
handler: async (ctx, args) => {
|
|
61
|
+
const orgId = await requireOrgId(ctx);
|
|
62
|
+
const doc = await ctx.db.get(args.agentId);
|
|
63
|
+
if (!doc || doc.orgId !== orgId)
|
|
64
|
+
return null;
|
|
65
|
+
return doc;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { WorkflowManager, type WorkflowId } from "@convex-dev/workflow";
|
|
2
|
+
export declare const workflow: WorkflowManager;
|
|
3
|
+
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
4
|
+
stepFunctionHandle: string;
|
|
5
|
+
totalSteps: number;
|
|
6
|
+
maxAttemptsPerStep: number;
|
|
7
|
+
kind: string;
|
|
8
|
+
}, Promise<import("convex/values").GenericId<"durableJobs">>>;
|
|
9
|
+
export declare const getStatus: import("convex/server").RegisteredQuery<"public", {
|
|
10
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
11
|
+
}, Promise<{
|
|
12
|
+
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
13
|
+
cursor: number;
|
|
14
|
+
totalSteps: number;
|
|
15
|
+
lastError: string | undefined;
|
|
16
|
+
abandonedAtStep: number | undefined;
|
|
17
|
+
}>>;
|
|
18
|
+
export declare const cancel: import("convex/server").RegisteredMutation<"public", {
|
|
19
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
20
|
+
}, Promise<null>>;
|
|
21
|
+
export declare const getJobInternal: import("convex/server").RegisteredQuery<"internal", {
|
|
22
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
23
|
+
}, Promise<{
|
|
24
|
+
cursor: number;
|
|
25
|
+
totalSteps: number;
|
|
26
|
+
maxAttemptsPerStep: number;
|
|
27
|
+
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
28
|
+
} | null>>;
|
|
29
|
+
export declare const runStepInternal: import("convex/server").RegisteredMutation<"internal", {
|
|
30
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
31
|
+
stepIndex: number;
|
|
32
|
+
}, Promise<null>>;
|
|
33
|
+
export declare const markAbandonedInternal: import("convex/server").RegisteredMutation<"internal", {
|
|
34
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
35
|
+
stepIndex: number;
|
|
36
|
+
error: string;
|
|
37
|
+
}, Promise<null>>;
|
|
38
|
+
export declare const markCompletedInternal: import("convex/server").RegisteredMutation<"internal", {
|
|
39
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
40
|
+
}, Promise<null>>;
|
|
41
|
+
export declare const durableJobWorkflowV1: import("convex/server").RegisteredMutation<"internal", import("@convex-dev/workflow").WorkflowArgs<{
|
|
42
|
+
jobId: import("convex/values").VId<import("convex/values").GenericId<"durableJobs">, "required">;
|
|
43
|
+
}>, WorkflowId>;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { WorkflowManager } from "@convex-dev/workflow";
|
|
3
|
+
import { components, internal } from "../_generated/api";
|
|
4
|
+
import { internalMutation, internalQuery, mutation, query, } from "../_generated/server";
|
|
5
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
6
|
+
// durableJobs — THE SURVIVAL CONTRACT (I1, SYNTHESE.md §5/§7): "mise en
|
|
7
|
+
// pause à la frontière d'étape, rejeu déterministe, reprise, abandon après
|
|
8
|
+
// N échecs." This ENVELOPS `@convex-dev/workflow` -- it does not reimplement
|
|
9
|
+
// durable execution. Every real unit of work (the caller-supplied
|
|
10
|
+
// `stepFunctionHandle`) runs inside its own bounded mutation call; the
|
|
11
|
+
// workflow persists `cursor` (steps [0, cursor) done) between calls, so a
|
|
12
|
+
// job whose TOTAL wall-clock time exceeds a single Convex action's ceiling
|
|
13
|
+
// still completes -- no single call has to survive past the ceiling, only
|
|
14
|
+
// the aggregate progress does.
|
|
15
|
+
//
|
|
16
|
+
// Contract surface (this is what a consumer -- VantagePeers/Sigma first --
|
|
17
|
+
// calls; see examples/vantage-peers-long-task for a worked integration):
|
|
18
|
+
// 1. `start` -- enqueue a job of N steps, driven by a step function
|
|
19
|
+
// the CALLER owns (created via `createFunctionHandle`
|
|
20
|
+
// in the caller's own app). We never read the
|
|
21
|
+
// caller's tables directly -- component isolation.
|
|
22
|
+
// 2. `getStatus` -- poll status/cursor/lastError/abandonedAtStep.
|
|
23
|
+
// 3. `cancel` -- best-effort stop (checked at the next step boundary
|
|
24
|
+
// and by the workflow's own cancel()).
|
|
25
|
+
// 4. (internal) the workflow itself is the pause/resume/replay engine:
|
|
26
|
+
// - PAUSE AT STEP BOUNDARY: each loop iteration is one workflow step;
|
|
27
|
+
// `@convex-dev/workflow` durably persists after each step returns,
|
|
28
|
+
// so the process can die between any two steps with zero data loss.
|
|
29
|
+
// - DETERMINISTIC REPLAY: `runStepInternal` is idempotent -- it
|
|
30
|
+
// no-ops for any stepIndex < job.cursor, so replaying the workflow
|
|
31
|
+
// handler after a resume never re-runs completed work.
|
|
32
|
+
// - RESUME: on restart, the workflow replays its journal up to the
|
|
33
|
+
// last completed step then continues the same in-memory loop; our
|
|
34
|
+
// `cursor` on the `durableJobs` row is the second, redundant source
|
|
35
|
+
// of truth a caller can poll without touching workflow internals.
|
|
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, I0 §7.1) -- 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";
|
|
47
|
+
export const workflow = new WorkflowManager(components.workflow);
|
|
48
|
+
// ---------------------------------------------------------------------
|
|
49
|
+
// PUBLIC CONTRACT
|
|
50
|
+
// ---------------------------------------------------------------------
|
|
51
|
+
export const start = mutation({
|
|
52
|
+
args: {
|
|
53
|
+
kind: v.string(),
|
|
54
|
+
stepFunctionHandle: v.string(),
|
|
55
|
+
totalSteps: v.number(),
|
|
56
|
+
maxAttemptsPerStep: v.number(),
|
|
57
|
+
},
|
|
58
|
+
returns: v.id("durableJobs"),
|
|
59
|
+
handler: async (ctx, args) => {
|
|
60
|
+
const orgId = await requireOrgId(ctx);
|
|
61
|
+
if (!Number.isInteger(args.totalSteps) || args.totalSteps <= 0) {
|
|
62
|
+
throw new Error("Validation: totalSteps must be a positive integer");
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isInteger(args.maxAttemptsPerStep) ||
|
|
65
|
+
args.maxAttemptsPerStep <= 0) {
|
|
66
|
+
throw new Error("Validation: maxAttemptsPerStep must be a positive integer");
|
|
67
|
+
}
|
|
68
|
+
if (args.stepFunctionHandle.length === 0) {
|
|
69
|
+
throw new Error("Validation: stepFunctionHandle must not be empty");
|
|
70
|
+
}
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const jobId = await ctx.db.insert("durableJobs", {
|
|
73
|
+
orgId,
|
|
74
|
+
kind: args.kind,
|
|
75
|
+
stepFunctionHandle: args.stepFunctionHandle,
|
|
76
|
+
totalSteps: args.totalSteps,
|
|
77
|
+
maxAttemptsPerStep: args.maxAttemptsPerStep,
|
|
78
|
+
cursor: 0,
|
|
79
|
+
status: "running",
|
|
80
|
+
workflowVersion: CURRENT_WORKFLOW_VERSION,
|
|
81
|
+
createdAt: now,
|
|
82
|
+
updatedAt: now,
|
|
83
|
+
});
|
|
84
|
+
const workflowId = await workflow.start(ctx, internal.engine.durableJob.durableJobWorkflowV1, { jobId });
|
|
85
|
+
await ctx.db.patch(jobId, { workflowId });
|
|
86
|
+
return jobId;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
export const getStatus = query({
|
|
90
|
+
args: { jobId: v.id("durableJobs") },
|
|
91
|
+
returns: v.object({
|
|
92
|
+
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
93
|
+
cursor: v.number(),
|
|
94
|
+
totalSteps: v.number(),
|
|
95
|
+
lastError: v.optional(v.string()),
|
|
96
|
+
abandonedAtStep: v.optional(v.number()),
|
|
97
|
+
}),
|
|
98
|
+
handler: async (ctx, args) => {
|
|
99
|
+
const orgId = await requireOrgId(ctx);
|
|
100
|
+
const job = await ctx.db.get(args.jobId);
|
|
101
|
+
assertSameOrg(orgId, job, "Job not found");
|
|
102
|
+
return {
|
|
103
|
+
status: job.status,
|
|
104
|
+
cursor: job.cursor,
|
|
105
|
+
totalSteps: job.totalSteps,
|
|
106
|
+
lastError: job.lastError,
|
|
107
|
+
abandonedAtStep: job.abandonedAtStep,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
export const cancel = mutation({
|
|
112
|
+
args: { jobId: v.id("durableJobs") },
|
|
113
|
+
returns: v.null(),
|
|
114
|
+
handler: async (ctx, args) => {
|
|
115
|
+
const orgId = await requireOrgId(ctx);
|
|
116
|
+
const job = await ctx.db.get(args.jobId);
|
|
117
|
+
assertSameOrg(orgId, job, "Job not found");
|
|
118
|
+
if (job.status !== "running")
|
|
119
|
+
return null;
|
|
120
|
+
await ctx.db.patch(args.jobId, {
|
|
121
|
+
status: "cancelled",
|
|
122
|
+
updatedAt: Date.now(),
|
|
123
|
+
});
|
|
124
|
+
if (job.workflowId) {
|
|
125
|
+
await workflow.cancel(ctx, job.workflowId);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
// ---------------------------------------------------------------------
|
|
131
|
+
// INTERNAL — server-to-server only, invoked by the workflow's own steps.
|
|
132
|
+
// The workflow runs with no end-user identity: every handler below scopes
|
|
133
|
+
// strictly by the orgId already recorded on the `durableJobs` row, never a
|
|
134
|
+
// client-supplied one.
|
|
135
|
+
// ---------------------------------------------------------------------
|
|
136
|
+
export const getJobInternal = internalQuery({
|
|
137
|
+
args: { jobId: v.id("durableJobs") },
|
|
138
|
+
returns: v.union(v.object({
|
|
139
|
+
cursor: v.number(),
|
|
140
|
+
totalSteps: v.number(),
|
|
141
|
+
maxAttemptsPerStep: v.number(),
|
|
142
|
+
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
143
|
+
}), v.null()),
|
|
144
|
+
handler: async (ctx, args) => {
|
|
145
|
+
const job = await ctx.db.get(args.jobId);
|
|
146
|
+
if (!job)
|
|
147
|
+
return null;
|
|
148
|
+
return {
|
|
149
|
+
cursor: job.cursor,
|
|
150
|
+
totalSteps: job.totalSteps,
|
|
151
|
+
maxAttemptsPerStep: job.maxAttemptsPerStep,
|
|
152
|
+
status: job.status,
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// The one real unit of work per step-boundary. DETERMINISTIC REPLAY GUARD:
|
|
157
|
+
// no-ops for any stepIndex already covered by job.cursor, so a workflow
|
|
158
|
+
// resume (journal replay after a kill mid-run) never re-executes the
|
|
159
|
+
// caller's step function for completed steps. ABANDON AFTER N: retries the
|
|
160
|
+
// caller's step function up to `maxAttemptsPerStep` times before throwing
|
|
161
|
+
// (throwing is what the workflow handler below turns into "abandoned").
|
|
162
|
+
export const runStepInternal = internalMutation({
|
|
163
|
+
args: { jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
164
|
+
returns: v.null(),
|
|
165
|
+
handler: async (ctx, args) => {
|
|
166
|
+
const job = await ctx.db.get(args.jobId);
|
|
167
|
+
if (!job)
|
|
168
|
+
throw new Error("Job not found");
|
|
169
|
+
if (job.status === "cancelled")
|
|
170
|
+
return null;
|
|
171
|
+
if (args.stepIndex < job.cursor) {
|
|
172
|
+
// Already-completed step replayed by a resumed workflow -- no-op.
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
let lastError;
|
|
176
|
+
for (let attempt = 1; attempt <= job.maxAttemptsPerStep; attempt++) {
|
|
177
|
+
try {
|
|
178
|
+
await ctx.runMutation(job.stepFunctionHandle, { orgId: job.orgId, jobId: args.jobId, stepIndex: args.stepIndex });
|
|
179
|
+
await ctx.db.patch(args.jobId, {
|
|
180
|
+
cursor: args.stepIndex + 1,
|
|
181
|
+
updatedAt: Date.now(),
|
|
182
|
+
});
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
lastError = err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw lastError instanceof Error
|
|
190
|
+
? lastError
|
|
191
|
+
: new Error(String(lastError));
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
export const markAbandonedInternal = internalMutation({
|
|
195
|
+
args: { jobId: v.id("durableJobs"), stepIndex: v.number(), error: v.string() },
|
|
196
|
+
returns: v.null(),
|
|
197
|
+
handler: async (ctx, args) => {
|
|
198
|
+
const job = await ctx.db.get(args.jobId);
|
|
199
|
+
if (!job || job.status !== "running")
|
|
200
|
+
return null;
|
|
201
|
+
await ctx.db.patch(args.jobId, {
|
|
202
|
+
status: "abandoned",
|
|
203
|
+
abandonedAtStep: args.stepIndex,
|
|
204
|
+
lastError: args.error,
|
|
205
|
+
updatedAt: Date.now(),
|
|
206
|
+
});
|
|
207
|
+
return null;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
export const markCompletedInternal = internalMutation({
|
|
211
|
+
args: { jobId: v.id("durableJobs") },
|
|
212
|
+
returns: v.null(),
|
|
213
|
+
handler: async (ctx, args) => {
|
|
214
|
+
const job = await ctx.db.get(args.jobId);
|
|
215
|
+
if (!job || job.status !== "running")
|
|
216
|
+
return null;
|
|
217
|
+
await ctx.db.patch(args.jobId, {
|
|
218
|
+
status: "completed",
|
|
219
|
+
updatedAt: Date.now(),
|
|
220
|
+
});
|
|
221
|
+
return null;
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
// durableJobWorkflowV1 -- the pause/resume/replay engine. One loop
|
|
225
|
+
// iteration == one workflow step == one durability boundary: `step.
|
|
226
|
+
// runMutation` returns only after `@convex-dev/workflow` has persisted the
|
|
227
|
+
// journal entry, so a process kill between any two iterations loses zero
|
|
228
|
+
// progress. On resume the workflow replays already-journaled steps from
|
|
229
|
+
// its own history (no caller-visible effect, since runStepInternal is
|
|
230
|
+
// idempotent below job.cursor) and continues the loop from where it left
|
|
231
|
+
// off.
|
|
232
|
+
export const durableJobWorkflowV1 = workflow.define({
|
|
233
|
+
args: { jobId: v.id("durableJobs") },
|
|
234
|
+
returns: v.null(),
|
|
235
|
+
handler: async (step, args) => {
|
|
236
|
+
const job = await step.runQuery(internal.engine.durableJob.getJobInternal, {
|
|
237
|
+
jobId: args.jobId,
|
|
238
|
+
});
|
|
239
|
+
if (!job || job.status !== "running")
|
|
240
|
+
return null;
|
|
241
|
+
for (let i = job.cursor; i < job.totalSteps; i++) {
|
|
242
|
+
try {
|
|
243
|
+
await step.runMutation(internal.engine.durableJob.runStepInternal, {
|
|
244
|
+
jobId: args.jobId,
|
|
245
|
+
stepIndex: i,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
await step.runMutation(internal.engine.durableJob.markAbandonedInternal, {
|
|
250
|
+
jobId: args.jobId,
|
|
251
|
+
stepIndex: i,
|
|
252
|
+
error: err instanceof Error ? err.message : String(err),
|
|
253
|
+
});
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
await step.runMutation(internal.engine.durableJob.markCompletedInternal, {
|
|
258
|
+
jobId: args.jobId,
|
|
259
|
+
});
|
|
260
|
+
return null;
|
|
261
|
+
},
|
|
262
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const resolveManifest: import("convex/server").RegisteredQuery<"public", {
|
|
2
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
3
|
+
}, Promise<{
|
|
4
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
5
|
+
name: string;
|
|
6
|
+
instructions: string;
|
|
7
|
+
model: string;
|
|
8
|
+
toolApprovals: {
|
|
9
|
+
toolName: string;
|
|
10
|
+
approval: "always" | "once" | "never";
|
|
11
|
+
}[];
|
|
12
|
+
}>>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { query } from "../_generated/server";
|
|
3
|
+
import { assertSameOrg, requireOrgId } from "../lib/auth";
|
|
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.
|
|
17
|
+
export const resolveManifest = query({
|
|
18
|
+
args: { sessionId: v.id("sessions") },
|
|
19
|
+
returns: v.object({
|
|
20
|
+
agentId: v.id("agents"),
|
|
21
|
+
name: v.string(),
|
|
22
|
+
instructions: v.string(),
|
|
23
|
+
model: v.string(),
|
|
24
|
+
toolApprovals: v.array(v.object({
|
|
25
|
+
toolName: v.string(),
|
|
26
|
+
approval: v.union(v.literal("always"), v.literal("once"), v.literal("never")),
|
|
27
|
+
})),
|
|
28
|
+
}),
|
|
29
|
+
handler: async (ctx, args) => {
|
|
30
|
+
const orgId = await requireOrgId(ctx);
|
|
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);
|
|
34
|
+
assertSameOrg(orgId, agent, "Agent not found");
|
|
35
|
+
const toolApprovals = await ctx.db
|
|
36
|
+
.query("toolApprovals")
|
|
37
|
+
.withIndex("by_org_agent", (q) => q.eq("orgId", orgId).eq("agentId", session.agentId))
|
|
38
|
+
.collect();
|
|
39
|
+
return {
|
|
40
|
+
agentId: agent._id,
|
|
41
|
+
name: agent.name,
|
|
42
|
+
instructions: agent.instructions,
|
|
43
|
+
model: agent.model,
|
|
44
|
+
toolApprovals: toolApprovals.map((t) => ({
|
|
45
|
+
toolName: t.toolName,
|
|
46
|
+
approval: t.approval,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export declare const start: import("convex/server").RegisteredMutation<"public", {
|
|
2
|
+
agentId: import("convex/values").GenericId<"agents">;
|
|
3
|
+
}, Promise<import("convex/values").GenericId<"sessions">>>;
|
|
4
|
+
export declare const sendTurn: import("convex/server").RegisteredMutation<"public", {
|
|
5
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
6
|
+
input: string;
|
|
7
|
+
}, Promise<null>>;
|
|
8
|
+
export declare const appendReasoning: import("convex/server").RegisteredMutation<"public", {
|
|
9
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
10
|
+
chunk: string;
|
|
11
|
+
}, Promise<null>>;
|
|
12
|
+
export declare const requestToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
13
|
+
toolName: string;
|
|
14
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
15
|
+
argsJson: string;
|
|
16
|
+
}, Promise<{
|
|
17
|
+
authorized: boolean;
|
|
18
|
+
approvalPending: boolean;
|
|
19
|
+
}>>;
|
|
20
|
+
export declare const completeToolCall: import("convex/server").RegisteredMutation<"public", {
|
|
21
|
+
toolName: string;
|
|
22
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
23
|
+
resultJson: string;
|
|
24
|
+
}, Promise<null>>;
|
|
25
|
+
export declare const completeTurn: import("convex/server").RegisteredMutation<"public", {
|
|
26
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
27
|
+
}, Promise<null>>;
|
|
28
|
+
export declare const listEvents: import("convex/server").RegisteredQuery<"public", {
|
|
29
|
+
sessionId: import("convex/values").GenericId<"sessions">;
|
|
30
|
+
}, Promise<{
|
|
31
|
+
_id: import("convex/values").GenericId<"sessionEvents">;
|
|
32
|
+
_creationTime: number;
|
|
33
|
+
name: "session.started" | "turn.started" | "turn.reasoning" | "turn.toolCall.requested" | "turn.toolCall.completed" | "turn.completed" | "tool.approval.requested" | "tool.approval.resolved";
|
|
34
|
+
payload: string | undefined;
|
|
35
|
+
createdAt: number;
|
|
36
|
+
}[]>>;
|