@vantageos/agent-engine 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +69 -14
- package/dist/component/_generated/api.d.ts +42 -0
- package/dist/component/_generated/api.js +30 -0
- package/dist/component/_generated/component.d.ts +44 -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/durableJob.d.ts +43 -0
- package/dist/component/engine/durableJob.js +262 -0
- package/dist/component/lib/auth.d.ts +7 -0
- package/dist/component/lib/auth.js +24 -0
- package/dist/component/schema.d.ts +49 -0
- package/dist/component/schema.js +56 -0
- package/dist/component/testFixtures/callerSteps.d.ts +20 -0
- package/dist/component/testFixtures/callerSteps.js +90 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -215,16 +215,21 @@ that all contributors run) is Pi's to make, not this PR's.
|
|
|
215
215
|
interruption); a step that always fails is retried and the job is
|
|
216
216
|
`abandoned` (never touching the next step); a step that fails once then
|
|
217
217
|
succeeds still reaches `completed`; cross-org `getStatus` is denied.
|
|
218
|
+
- `component/durableJob.componentPackaging.test.ts` — the SAME contract
|
|
219
|
+
(long job survives, abandon-after-N, org isolation, cancel), but run
|
|
220
|
+
against `component/_generated/api` — the exact source tree compiled to
|
|
221
|
+
`dist/component/` and published — proving the migrated, packaged code
|
|
222
|
+
itself, not only the repo-root dev app.
|
|
218
223
|
|
|
219
224
|
Run: `npx vitest run` (or `npm test`).
|
|
220
225
|
|
|
221
|
-
## Packaging & publication (T2-TER)
|
|
226
|
+
## Packaging & publication (T2-TER, functions since T2-QUATER)
|
|
222
227
|
|
|
223
228
|
`@vantageos/agent-engine` is published to npm under the `alpha` dist-tag
|
|
224
229
|
(`npm view @vantageos/agent-engine version` / `dist-tags`, read from the
|
|
225
230
|
registry after publish, never from `npm publish`'s own exit status). The
|
|
226
231
|
package exposes a resolvable `"./convex.config"` subpath (types + JS,
|
|
227
|
-
built by `npm run build` from `
|
|
232
|
+
built by `npm run build` from `component/convex.config.ts` via
|
|
228
233
|
`tsc -p tsconfig.build.json` into `dist/component/`), so a consumer's
|
|
229
234
|
`convex/convex.config.ts` can do:
|
|
230
235
|
|
|
@@ -237,21 +242,71 @@ app.use(agentEngine);
|
|
|
237
242
|
export default app;
|
|
238
243
|
```
|
|
239
244
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
### What's callable, and what isn't (read this before wiring a consumer)
|
|
246
|
+
|
|
247
|
+
Since T2-QUATER, `component/` (sibling of `convex/`, NOT nested under it —
|
|
248
|
+
see "Why `component/` and not `convex/component/`" below) hosts the
|
|
249
|
+
**actual, compiled, published functions** for a real slice of the engine,
|
|
250
|
+
not just the component's identity:
|
|
251
|
+
|
|
252
|
+
- `component/schema.ts` — the `durableJobs` table (+ a test-only
|
|
253
|
+
`callerStepInvocations` fixture table).
|
|
254
|
+
- `component/engine/durableJob.ts` — `start` / `getStatus` / `cancel`
|
|
255
|
+
(public contract) + `durableJobWorkflowV1` (the versioned pause/resume/
|
|
256
|
+
replay workflow) + its internal step-runner mutations.
|
|
257
|
+
- `component/lib/auth.ts` — `requireOrgId` / `assertSameOrg`: orgId is
|
|
258
|
+
derived server-side and is the first field of every index; deny by
|
|
259
|
+
default travels WITH the component, not just with this repo's own app.
|
|
260
|
+
|
|
261
|
+
**Callable through the published component today:**
|
|
262
|
+
`components.agentEngine.engine.durableJob.start(...)`,
|
|
263
|
+
`.getStatus(...)`, `.cancel(...)` — verified two ways: (1)
|
|
264
|
+
`component/durableJob.componentPackaging.test.ts` runs `convex-test`
|
|
265
|
+
directly against `component/_generated/api` (the exact source tree
|
|
266
|
+
`tsc -p tsconfig.build.json` compiles into `dist/component/`, which is
|
|
267
|
+
what `npm pack` ships), so the compiled artifact has a real green test
|
|
268
|
+
suite behind it, not just the repo-root dev app's; (2) a from-scratch
|
|
269
|
+
consumer probe (install the published tarball into a bare `npx convex
|
|
270
|
+
dev` project, register the component, call `durableJob.start` +
|
|
271
|
+
`getStatus` from the consumer's own function) — see the T2-QUATER
|
|
272
|
+
completion report for the verbatim transcript.
|
|
273
|
+
|
|
274
|
+
**NOT yet callable through the published component** (still only exists
|
|
275
|
+
in this repo's OWN dev/test deployment under `convex/schema.ts` +
|
|
276
|
+
`convex/agents.ts` + `convex/skills.ts`, used by `npx convex dev` and the
|
|
277
|
+
repo-root `convex-test` suite): the agent aggregate itself — `agents`,
|
|
278
|
+
`skills`, `subagents`, `connections`, `habits`, `sandboxConfig`,
|
|
279
|
+
`toolApprovals`, `modelKeys` — and the rest of the durable-session engine
|
|
280
|
+
— `sessions`, `sessionEvents`, `awaitEventTimeouts`, `wakeableWork`,
|
|
281
|
+
`engine/wakeup.ts`, `engine/resolvers.ts`, `engine/workflows.ts`
|
|
282
|
+
(`sessionWorkflowV1`). Porting each of those under `component/` without a
|
|
283
|
+
working component-level `convex-test` harness behind it would ship
|
|
284
|
+
unverified code — Day 128 forbids that; it is deliberately left for the
|
|
285
|
+
next, separately-scoped migration.
|
|
286
|
+
|
|
287
|
+
### Why `component/` and not `convex/component/`
|
|
288
|
+
|
|
289
|
+
`npx convex codegen --component-dir <path>` builds a synthetic root app
|
|
290
|
+
that does `import component from "<relative-import-to-path>"`. When
|
|
291
|
+
`<path>` is nested INSIDE the functions directory it resolves from
|
|
292
|
+
(`convex/`), that relative import comes out as a bare specifier with no
|
|
293
|
+
leading `./` or `../` (e.g. `"component/convex.config.js"`), which esbuild
|
|
294
|
+
correctly refuses to resolve as a relative path — not a bug in our code,
|
|
295
|
+
a corner case of running `--component-dir` against a directory nested
|
|
296
|
+
under the default functions directory. Moving the component's source tree
|
|
297
|
+
one level up, to `component/` (a sibling of `convex/`, mirroring how
|
|
298
|
+
`@convex-dev/workflow` itself keeps its component source under `src/component`,
|
|
299
|
+
outside its `example/convex` app directory) makes the generated relative
|
|
300
|
+
import start with `../`, which resolves — measured empirically against
|
|
301
|
+
this exact repo layout, not guessed.
|
|
250
302
|
|
|
251
303
|
## Status
|
|
252
304
|
|
|
253
305
|
T1 (schema + durable session engine, PR #1) merged @
|
|
254
306
|
`9f3401ba63e6e663af95e9e4f5da42bc30b3550b`. I1 (long-task survival
|
|
255
307
|
contract, `engine/durableJob.ts`) — not merged, Eta reviews, merge on Pi
|
|
256
|
-
token. T2-TER (
|
|
257
|
-
|
|
308
|
+
token. T2-TER (npm packaging + component identity + publication) and
|
|
309
|
+
T2-QUATER (this pass: migrated `durableJob` + its schema + `lib/auth.ts`
|
|
310
|
+
under the actual component boundary, `component/`, with its own
|
|
311
|
+
`_generated/` and its own green `convex-test` suite) — see "Packaging &
|
|
312
|
+
publication" above; **not merged**.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated `api` utility.
|
|
3
|
+
*
|
|
4
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
5
|
+
*
|
|
6
|
+
* To regenerate, run `npx convex dev`.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type * as engine_durableJob from "../engine/durableJob.js";
|
|
10
|
+
import type * as lib_auth from "../lib/auth.js";
|
|
11
|
+
import type * as testFixtures_callerSteps from "../testFixtures/callerSteps.js";
|
|
12
|
+
import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
|
|
13
|
+
declare const fullApi: ApiFromModules<{
|
|
14
|
+
"engine/durableJob": typeof engine_durableJob;
|
|
15
|
+
"lib/auth": typeof lib_auth;
|
|
16
|
+
"testFixtures/callerSteps": typeof testFixtures_callerSteps;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* A utility for referencing Convex functions in your app's public API.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```js
|
|
23
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare const api: FilterApi<typeof fullApi, FunctionReference<any, "public">>;
|
|
27
|
+
/**
|
|
28
|
+
* A utility for referencing Convex functions in your app's internal API.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ```js
|
|
32
|
+
* const myFunctionReference = internal.myModule.myFunction;
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;
|
|
36
|
+
export declare const components: {
|
|
37
|
+
workflow: import("@convex-dev/workflow/_generated/component.js").ComponentApi<"workflow">;
|
|
38
|
+
agentTurnPool: import("@convex-dev/workpool/_generated/component.js").ComponentApi<"agentTurnPool">;
|
|
39
|
+
wakeupPool: import("@convex-dev/workpool/_generated/component.js").ComponentApi<"wakeupPool">;
|
|
40
|
+
agent: import("@convex-dev/agent/_generated/component.js").ComponentApi<"agent">;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Generated `api` utility.
|
|
4
|
+
*
|
|
5
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
6
|
+
*
|
|
7
|
+
* To regenerate, run `npx convex dev`.
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
import { anyApi, componentsGeneric } from "convex/server";
|
|
11
|
+
const fullApi = anyApi;
|
|
12
|
+
/**
|
|
13
|
+
* A utility for referencing Convex functions in your app's public API.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```js
|
|
17
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export const api = anyApi;
|
|
21
|
+
/**
|
|
22
|
+
* A utility for referencing Convex functions in your app's internal API.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* ```js
|
|
26
|
+
* const myFunctionReference = internal.myModule.myFunction;
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export const internal = anyApi;
|
|
30
|
+
export const components = componentsGeneric();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated `ComponentApi` utility.
|
|
3
|
+
*
|
|
4
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
5
|
+
*
|
|
6
|
+
* To regenerate, run `npx convex dev`.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { FunctionReference } from "convex/server";
|
|
10
|
+
/**
|
|
11
|
+
* A utility for referencing a Convex component's exposed API.
|
|
12
|
+
*
|
|
13
|
+
* Useful when expecting a parameter like `components.myComponent`.
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```ts
|
|
16
|
+
* async function myFunction(ctx: QueryCtx, component: ComponentApi) {
|
|
17
|
+
* return ctx.runQuery(component.someFile.someQuery, { ...args });
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export type ComponentApi<Name extends string | undefined = string | undefined> = {
|
|
22
|
+
engine: {
|
|
23
|
+
durableJob: {
|
|
24
|
+
cancel: FunctionReference<"mutation", "internal", {
|
|
25
|
+
jobId: string;
|
|
26
|
+
}, null, Name>;
|
|
27
|
+
getStatus: FunctionReference<"query", "internal", {
|
|
28
|
+
jobId: string;
|
|
29
|
+
}, {
|
|
30
|
+
abandonedAtStep?: number;
|
|
31
|
+
cursor: number;
|
|
32
|
+
lastError?: string;
|
|
33
|
+
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
34
|
+
totalSteps: number;
|
|
35
|
+
}, Name>;
|
|
36
|
+
start: FunctionReference<"mutation", "internal", {
|
|
37
|
+
kind: string;
|
|
38
|
+
maxAttemptsPerStep: number;
|
|
39
|
+
stepFunctionHandle: string;
|
|
40
|
+
totalSteps: number;
|
|
41
|
+
}, string, Name>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated data model types.
|
|
3
|
+
*
|
|
4
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
5
|
+
*
|
|
6
|
+
* To regenerate, run `npx convex dev`.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { DataModelFromSchemaDefinition, DocumentByName, TableNamesInDataModel, SystemTableNames } from "convex/server";
|
|
10
|
+
import type { GenericId } from "convex/values";
|
|
11
|
+
import schema from "../schema.js";
|
|
12
|
+
/**
|
|
13
|
+
* The names of all of your Convex tables.
|
|
14
|
+
*/
|
|
15
|
+
export type TableNames = TableNamesInDataModel<DataModel>;
|
|
16
|
+
/**
|
|
17
|
+
* The type of a document stored in Convex.
|
|
18
|
+
*
|
|
19
|
+
* @typeParam TableName - A string literal type of the table name (like "users").
|
|
20
|
+
*/
|
|
21
|
+
export type Doc<TableName extends TableNames> = DocumentByName<DataModel, TableName>;
|
|
22
|
+
/**
|
|
23
|
+
* An identifier for a document in Convex.
|
|
24
|
+
*
|
|
25
|
+
* Convex documents are uniquely identified by their `Id`, which is accessible
|
|
26
|
+
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
|
27
|
+
*
|
|
28
|
+
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
|
29
|
+
*
|
|
30
|
+
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
|
31
|
+
* strings when type checking.
|
|
32
|
+
*
|
|
33
|
+
* @typeParam TableName - A string literal type of the table name (like "users").
|
|
34
|
+
*/
|
|
35
|
+
export type Id<TableName extends TableNames | SystemTableNames> = GenericId<TableName>;
|
|
36
|
+
/**
|
|
37
|
+
* A type describing your Convex data model.
|
|
38
|
+
*
|
|
39
|
+
* This type includes information about what tables you have, the type of
|
|
40
|
+
* documents stored in those tables, and the indexes defined on them.
|
|
41
|
+
*
|
|
42
|
+
* This type is used to parameterize methods like `queryGeneric` and
|
|
43
|
+
* `mutationGeneric` to make them type-safe.
|
|
44
|
+
*/
|
|
45
|
+
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated utilities for implementing server-side Convex query and mutation functions.
|
|
3
|
+
*
|
|
4
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
5
|
+
*
|
|
6
|
+
* To regenerate, run `npx convex dev`.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { ActionBuilder, HttpActionBuilder, MutationBuilder, QueryBuilder, GenericActionCtx, GenericMutationCtx, GenericQueryCtx, GenericDatabaseReader, GenericDatabaseWriter } from "convex/server";
|
|
10
|
+
import type { DataModel } from "./dataModel.js";
|
|
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 declare const query: QueryBuilder<DataModel, "public">;
|
|
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 declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
|
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 declare const mutation: MutationBuilder<DataModel, "public">;
|
|
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 declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
|
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 declare const action: ActionBuilder<DataModel, "public">;
|
|
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 declare const internalAction: ActionBuilder<DataModel, "internal">;
|
|
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 declare const httpAction: HttpActionBuilder;
|
|
78
|
+
/**
|
|
79
|
+
* A set of services for use within Convex query functions.
|
|
80
|
+
*
|
|
81
|
+
* The query context is passed as the first argument to any Convex query
|
|
82
|
+
* function run on the server.
|
|
83
|
+
*
|
|
84
|
+
* If you're using code generation, use the `QueryCtx` type in `convex/_generated/server.d.ts` instead.
|
|
85
|
+
*/
|
|
86
|
+
export type QueryCtx = GenericQueryCtx<DataModel>;
|
|
87
|
+
/**
|
|
88
|
+
* A set of services for use within Convex mutation functions.
|
|
89
|
+
*
|
|
90
|
+
* The mutation context is passed as the first argument to any Convex mutation
|
|
91
|
+
* function run on the server.
|
|
92
|
+
*
|
|
93
|
+
* If you're using code generation, use the `MutationCtx` type in `convex/_generated/server.d.ts` instead.
|
|
94
|
+
*/
|
|
95
|
+
export type MutationCtx = GenericMutationCtx<DataModel>;
|
|
96
|
+
/**
|
|
97
|
+
* A set of services for use within Convex action functions.
|
|
98
|
+
*
|
|
99
|
+
* The action context is passed as the first argument to any Convex action
|
|
100
|
+
* function run on the server.
|
|
101
|
+
*/
|
|
102
|
+
export type ActionCtx = GenericActionCtx<DataModel>;
|
|
103
|
+
/**
|
|
104
|
+
* An interface to read from the database within Convex query functions.
|
|
105
|
+
*
|
|
106
|
+
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
|
107
|
+
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
|
108
|
+
* building a query.
|
|
109
|
+
*/
|
|
110
|
+
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
|
111
|
+
/**
|
|
112
|
+
* An interface to read from and write to the database within Convex mutation
|
|
113
|
+
* functions.
|
|
114
|
+
*
|
|
115
|
+
* Convex guarantees that all writes within a single mutation are
|
|
116
|
+
* executed atomically, so you never have to worry about partial writes leaving
|
|
117
|
+
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
|
118
|
+
* for the guarantees Convex provides your functions.
|
|
119
|
+
*/
|
|
120
|
+
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
|
@@ -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,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,7 @@
|
|
|
1
|
+
import type { QueryCtx, MutationCtx } from "../_generated/server";
|
|
2
|
+
export declare function requireOrgId(ctx: QueryCtx | MutationCtx): Promise<string>;
|
|
3
|
+
export declare function assertSameOrg(callerOrgId: string, doc: {
|
|
4
|
+
orgId: string;
|
|
5
|
+
} | null, notFoundMessage: string): asserts doc is {
|
|
6
|
+
orgId: string;
|
|
7
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Deny by default. orgId is NEVER read from client args — it is derived
|
|
2
|
+
// server-side from the authenticated identity (Clerk org claim). Every
|
|
3
|
+
// mutation/query in this component must call this before touching data.
|
|
4
|
+
export async function requireOrgId(ctx) {
|
|
5
|
+
const identity = await ctx.auth.getUserIdentity();
|
|
6
|
+
if (!identity) {
|
|
7
|
+
throw new Error("Unauthenticated: no identity on ctx.auth");
|
|
8
|
+
}
|
|
9
|
+
// Clerk org-scoped JWT template exposes `org_id` as a top-level claim.
|
|
10
|
+
const orgId = identity.org_id;
|
|
11
|
+
if (!orgId || typeof orgId !== "string" || orgId.length === 0) {
|
|
12
|
+
throw new Error("Unauthenticated: identity carries no org_id claim");
|
|
13
|
+
}
|
|
14
|
+
return orgId;
|
|
15
|
+
}
|
|
16
|
+
// Throws unless the document's orgId matches the caller's server-derived
|
|
17
|
+
// orgId. Use on every read/write of a document fetched by _id, since
|
|
18
|
+
// db.get() does not filter by index.
|
|
19
|
+
export function assertSameOrg(callerOrgId, doc, notFoundMessage) {
|
|
20
|
+
if (!doc || doc.orgId !== callerOrgId) {
|
|
21
|
+
// Same error for "not found" and "cross-org" -- do not leak existence.
|
|
22
|
+
throw new Error(notFoundMessage);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
declare const _default: import("convex/server").SchemaDefinition<{
|
|
2
|
+
durableJobs: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
3
|
+
workflowId?: string | undefined;
|
|
4
|
+
lastError?: string | undefined;
|
|
5
|
+
abandonedAtStep?: number | undefined;
|
|
6
|
+
orgId: string;
|
|
7
|
+
stepFunctionHandle: string;
|
|
8
|
+
totalSteps: number;
|
|
9
|
+
maxAttemptsPerStep: number;
|
|
10
|
+
cursor: number;
|
|
11
|
+
status: "running" | "completed" | "abandoned" | "cancelled";
|
|
12
|
+
workflowVersion: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
kind: string;
|
|
16
|
+
}, {
|
|
17
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
18
|
+
kind: import("convex/values").VString<string, "required">;
|
|
19
|
+
stepFunctionHandle: import("convex/values").VString<string, "required">;
|
|
20
|
+
totalSteps: import("convex/values").VFloat64<number, "required">;
|
|
21
|
+
maxAttemptsPerStep: import("convex/values").VFloat64<number, "required">;
|
|
22
|
+
cursor: import("convex/values").VFloat64<number, "required">;
|
|
23
|
+
status: import("convex/values").VUnion<"running" | "completed" | "abandoned" | "cancelled", [import("convex/values").VLiteral<"running", "required">, import("convex/values").VLiteral<"completed", "required">, import("convex/values").VLiteral<"abandoned", "required">, import("convex/values").VLiteral<"cancelled", "required">], "required", never>;
|
|
24
|
+
workflowId: import("convex/values").VString<string | undefined, "optional">;
|
|
25
|
+
workflowVersion: import("convex/values").VString<string, "required">;
|
|
26
|
+
lastError: import("convex/values").VString<string | undefined, "optional">;
|
|
27
|
+
abandonedAtStep: import("convex/values").VFloat64<number | undefined, "optional">;
|
|
28
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
29
|
+
updatedAt: import("convex/values").VFloat64<number, "required">;
|
|
30
|
+
}, "required", "orgId" | "stepFunctionHandle" | "totalSteps" | "maxAttemptsPerStep" | "cursor" | "status" | "workflowId" | "workflowVersion" | "lastError" | "abandonedAtStep" | "createdAt" | "updatedAt" | "kind">, {
|
|
31
|
+
by_org: ["orgId", "_creationTime"];
|
|
32
|
+
by_org_status: ["orgId", "status", "_creationTime"];
|
|
33
|
+
}, {}, {}>;
|
|
34
|
+
callerStepInvocations: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
35
|
+
orgId: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
38
|
+
stepIndex: number;
|
|
39
|
+
}, {
|
|
40
|
+
orgId: import("convex/values").VString<string, "required">;
|
|
41
|
+
jobId: import("convex/values").VId<import("convex/values").GenericId<"durableJobs">, "required">;
|
|
42
|
+
stepIndex: import("convex/values").VFloat64<number, "required">;
|
|
43
|
+
createdAt: import("convex/values").VFloat64<number, "required">;
|
|
44
|
+
}, "required", "orgId" | "createdAt" | "jobId" | "stepIndex">, {
|
|
45
|
+
by_org: ["orgId", "_creationTime"];
|
|
46
|
+
by_org_job: ["orgId", "jobId", "_creationTime"];
|
|
47
|
+
}, {}, {}>;
|
|
48
|
+
}, true>;
|
|
49
|
+
export default _default;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
// @vantageos/agent-engine — COMPONENT schema (T2-QUATER).
|
|
4
|
+
//
|
|
5
|
+
// This is the schema that actually ships INSIDE the published package and
|
|
6
|
+
// is deployed under the component boundary when a consumer registers
|
|
7
|
+
// `app.use(agentEngine)` — as opposed to `convex/schema.ts` at the repo
|
|
8
|
+
// root, which only backs THIS repo's own dev/test deployment.
|
|
9
|
+
//
|
|
10
|
+
// SCOPE (T2-QUATER, deliberately narrow — see README "What's callable"):
|
|
11
|
+
// only the tables `engine/durableJob.ts` requires. The full agent
|
|
12
|
+
// aggregate (agents, skills, subagents, connections, habits,
|
|
13
|
+
// sandboxConfig, toolApprovals, modelKeys, sessions, sessionEvents,
|
|
14
|
+
// awaitEventTimeouts, wakeableWork, wakeupDemoResults) is NOT yet
|
|
15
|
+
// re-hosted under this component boundary — that is the next, separately
|
|
16
|
+
// scoped migration. Porting the durableJob engine first unblocks
|
|
17
|
+
// VantagePeers/Sigma's I1 need (durable long-task survival) without
|
|
18
|
+
// widening the surface being shipped-and-verified beyond what has an
|
|
19
|
+
// actual working component-level test harness behind it (Day 128: never
|
|
20
|
+
// ship unverified).
|
|
21
|
+
export default defineSchema({
|
|
22
|
+
// durableJobs — GENERIC durable step-batch primitive (I1, SYNTHESE.md
|
|
23
|
+
// §5/§7). See convex/engine/durableJob.ts (repo root) for the full
|
|
24
|
+
// contract description; this table definition is byte-identical to the
|
|
25
|
+
// one there.
|
|
26
|
+
durableJobs: defineTable({
|
|
27
|
+
orgId: v.string(),
|
|
28
|
+
kind: v.string(),
|
|
29
|
+
stepFunctionHandle: v.string(),
|
|
30
|
+
totalSteps: v.number(),
|
|
31
|
+
maxAttemptsPerStep: v.number(),
|
|
32
|
+
cursor: v.number(),
|
|
33
|
+
status: v.union(v.literal("running"), v.literal("completed"), v.literal("abandoned"), v.literal("cancelled")),
|
|
34
|
+
workflowId: v.optional(v.string()),
|
|
35
|
+
workflowVersion: v.string(),
|
|
36
|
+
lastError: v.optional(v.string()),
|
|
37
|
+
abandonedAtStep: v.optional(v.number()),
|
|
38
|
+
createdAt: v.number(),
|
|
39
|
+
updatedAt: v.number(),
|
|
40
|
+
})
|
|
41
|
+
.index("by_org", ["orgId"])
|
|
42
|
+
.index("by_org_status", ["orgId", "status"]),
|
|
43
|
+
// callerStepInvocations — TEST-ONLY fixture table, see
|
|
44
|
+
// convex/component/testFixtures/callerSteps.ts. Not part of the public
|
|
45
|
+
// contract; kept so the component-level test harness can prove the
|
|
46
|
+
// deterministic-replay / abandon-after-N properties the same way the
|
|
47
|
+
// repo-root test suite does.
|
|
48
|
+
callerStepInvocations: defineTable({
|
|
49
|
+
orgId: v.string(),
|
|
50
|
+
jobId: v.id("durableJobs"),
|
|
51
|
+
stepIndex: v.number(),
|
|
52
|
+
createdAt: v.number(),
|
|
53
|
+
})
|
|
54
|
+
.index("by_org", ["orgId"])
|
|
55
|
+
.index("by_org_job", ["orgId", "jobId"]),
|
|
56
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const getAttemptCount: import("convex/server").RegisteredQuery<"internal", {
|
|
2
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
3
|
+
stepIndex: number;
|
|
4
|
+
}, Promise<number>>;
|
|
5
|
+
export declare const resetAttemptCounts: import("convex/server").RegisteredMutation<"internal", {}, Promise<null>>;
|
|
6
|
+
export declare const alwaysSucceeds: import("convex/server").RegisteredMutation<"internal", {
|
|
7
|
+
orgId: string;
|
|
8
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
9
|
+
stepIndex: number;
|
|
10
|
+
}, Promise<null>>;
|
|
11
|
+
export declare const alwaysFails: import("convex/server").RegisteredMutation<"internal", {
|
|
12
|
+
orgId: string;
|
|
13
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
14
|
+
stepIndex: number;
|
|
15
|
+
}, Promise<never>>;
|
|
16
|
+
export declare const failsFirstAttemptThenSucceeds: import("convex/server").RegisteredMutation<"internal", {
|
|
17
|
+
orgId: string;
|
|
18
|
+
jobId: import("convex/values").GenericId<"durableJobs">;
|
|
19
|
+
stepIndex: number;
|
|
20
|
+
}, Promise<null>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internalMutation, internalQuery } from "../_generated/server";
|
|
3
|
+
// testFixtures/callerSteps — stand-ins for a CALLER'S OWN step function
|
|
4
|
+
// (the thing a real consumer, e.g. VantagePeers, would define in ITS OWN
|
|
5
|
+
// app and pass to `engine/durableJob.start` as `stepFunctionHandle` via
|
|
6
|
+
// `createFunctionHandle`). These exist ONLY to drive the contract tests in
|
|
7
|
+
// convex/engineDurableJob.test.ts -- they are not part of the public
|
|
8
|
+
// surface (see examples/vantage-peers-long-task for the real, documented
|
|
9
|
+
// integration a consumer would write).
|
|
10
|
+
//
|
|
11
|
+
// Attempt counts are tracked IN-MEMORY (module-level Map), not via a DB
|
|
12
|
+
// write, because a mutation that throws rolls back every write made in its
|
|
13
|
+
// OWN transaction -- including a "record that I was called" insert made
|
|
14
|
+
// just before the throw. In-memory state survives the rollback since it is
|
|
15
|
+
// plain JS state, not part of the Convex transaction.
|
|
16
|
+
const attemptCounts = new Map();
|
|
17
|
+
function bump(jobId, stepIndex) {
|
|
18
|
+
const key = `${jobId}:${stepIndex}`;
|
|
19
|
+
const next = (attemptCounts.get(key) ?? 0) + 1;
|
|
20
|
+
attemptCounts.set(key, next);
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
export const getAttemptCount = internalQuery({
|
|
24
|
+
args: { jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
25
|
+
returns: v.number(),
|
|
26
|
+
handler: async (_ctx, args) => {
|
|
27
|
+
return attemptCounts.get(`${args.jobId}:${args.stepIndex}`) ?? 0;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
// TEST-ONLY: `convexTest()` reuses deterministic, low-cardinality document
|
|
31
|
+
// IDs across SEPARATE test instances within the same process, so this
|
|
32
|
+
// module-level Map must be cleared between tests to avoid one test's
|
|
33
|
+
// counters leaking into the next via a colliding `${jobId}:${stepIndex}`
|
|
34
|
+
// key. Called from `beforeEach` in engineDurableJob.test.ts.
|
|
35
|
+
export const resetAttemptCounts = internalMutation({
|
|
36
|
+
args: {},
|
|
37
|
+
returns: v.null(),
|
|
38
|
+
handler: async () => {
|
|
39
|
+
attemptCounts.clear();
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// Records one row per SUCCESSFUL invocation so tests can assert exactly
|
|
44
|
+
// which step indices actually completed (idempotent-replay assertions).
|
|
45
|
+
export const alwaysSucceeds = internalMutation({
|
|
46
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
47
|
+
returns: v.null(),
|
|
48
|
+
handler: async (ctx, args) => {
|
|
49
|
+
bump(args.jobId, args.stepIndex);
|
|
50
|
+
await ctx.db.insert("callerStepInvocations", {
|
|
51
|
+
orgId: args.orgId,
|
|
52
|
+
jobId: args.jobId,
|
|
53
|
+
stepIndex: args.stepIndex,
|
|
54
|
+
createdAt: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
// Fails on every attempt -- used to prove "abandon after N échecs". Attempt
|
|
60
|
+
// count is observable via `getAttemptCount` even though every DB write this
|
|
61
|
+
// function might otherwise make would be rolled back by the throw.
|
|
62
|
+
export const alwaysFails = internalMutation({
|
|
63
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
64
|
+
returns: v.null(),
|
|
65
|
+
handler: async (_ctx, args) => {
|
|
66
|
+
bump(args.jobId, args.stepIndex);
|
|
67
|
+
throw new Error(`simulated failure at step ${args.stepIndex}`);
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
// Fails on the FIRST attempt of ANY step, then succeeds on every
|
|
71
|
+
// subsequent attempt of that same step -- used to prove retry-then-succeed
|
|
72
|
+
// still advances the cursor (not just outright failure), exercising the
|
|
73
|
+
// `maxAttemptsPerStep > 1` path distinctly from `alwaysFails`.
|
|
74
|
+
export const failsFirstAttemptThenSucceeds = internalMutation({
|
|
75
|
+
args: { orgId: v.string(), jobId: v.id("durableJobs"), stepIndex: v.number() },
|
|
76
|
+
returns: v.null(),
|
|
77
|
+
handler: async (ctx, args) => {
|
|
78
|
+
const attemptNumber = bump(args.jobId, args.stepIndex);
|
|
79
|
+
if (attemptNumber === 1) {
|
|
80
|
+
throw new Error(`simulated single failure at step ${args.stepIndex}`);
|
|
81
|
+
}
|
|
82
|
+
await ctx.db.insert("callerStepInvocations", {
|
|
83
|
+
orgId: args.orgId,
|
|
84
|
+
jobId: args.jobId,
|
|
85
|
+
stepIndex: args.stepIndex,
|
|
86
|
+
createdAt: Date.now(),
|
|
87
|
+
});
|
|
88
|
+
return null;
|
|
89
|
+
},
|
|
90
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vantageos/agent-engine",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "Reusable Convex component: durable agent engine. Agent = data aggregate (root sheet + child tables), never compiled code. Built on @convex-dev/workflow + @convex-dev/workpool (+ @convex-dev/agent). Eve parity by design.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
"./convex.config.js": {
|
|
22
22
|
"types": "./dist/component/convex.config.d.ts",
|
|
23
23
|
"default": "./dist/component/convex.config.js"
|
|
24
|
+
},
|
|
25
|
+
"./_generated/component.js": {
|
|
26
|
+
"types": "./dist/component/_generated/component.d.ts"
|
|
24
27
|
}
|
|
25
28
|
},
|
|
26
29
|
"scripts": {
|