@vercel/factory 0.0.15 → 0.0.16
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/CHANGELOG.md +356 -0
- package/README.md +49 -261
- package/dist/agent-routes.d.mts +47 -3
- package/dist/agent-routes.mjs +28 -1
- package/dist/agent-routes.mjs.map +1 -1
- package/dist/api-contracts.d.mts +20 -1
- package/dist/api-contracts.mjs +2 -1
- package/dist/api-contracts.mjs.map +1 -1
- package/dist/api.d.mts +45 -2
- package/dist/api.mjs +199 -10
- package/dist/api.mjs.map +1 -1
- package/dist/approval-contracts.d.mts +6 -0
- package/dist/blob/index.d.mts +51 -14
- package/dist/blob/index.mjs +26 -10
- package/dist/blob/index.mjs.map +1 -1
- package/dist/budget.d.mts +7 -0
- package/dist/budget.mjs +6 -0
- package/dist/budget.mjs.map +1 -1
- package/dist/build-factory.d.mts +1 -0
- package/dist/change-verification/dispatch.d.mts +16 -3
- package/dist/change-verification/dispatch.mjs +45 -7
- package/dist/change-verification/dispatch.mjs.map +1 -1
- package/dist/change-verification/eve-tool.d.mts +2 -1
- package/dist/change-verification/eve-tool.mjs +35 -47
- package/dist/change-verification/eve-tool.mjs.map +1 -1
- package/dist/change-verification/result.mjs +130 -0
- package/dist/change-verification/result.mjs.map +1 -0
- package/dist/changes/eve-record-change.d.mts +2 -2
- package/dist/changes/eve-record-change.mjs +43 -9
- package/dist/changes/eve-record-change.mjs.map +1 -1
- package/dist/changes.d.mts +4 -3
- package/dist/changes.mjs +2 -2
- package/dist/changes.mjs.map +1 -1
- package/dist/client-events.d.mts +10 -3
- package/dist/client-events.mjs +6 -2
- package/dist/client-events.mjs.map +1 -1
- package/dist/client-stream.mjs +8 -2
- package/dist/client-stream.mjs.map +1 -1
- package/dist/client-transcript.mjs +5 -1
- package/dist/client-transcript.mjs.map +1 -1
- package/dist/client.d.mts +121 -12
- package/dist/client.mjs +117 -9
- package/dist/client.mjs.map +1 -1
- package/dist/code-review/contracts.d.mts +1 -0
- package/dist/code-review/eve-post-review.d.mts +4 -4
- package/dist/code-review/eve-post-review.mjs +29 -17
- package/dist/code-review/eve-post-review.mjs.map +1 -1
- package/dist/code-review/eve-review-comments.d.mts +13 -2
- package/dist/code-review/eve-review-comments.mjs +45 -11
- package/dist/code-review/eve-review-comments.mjs.map +1 -1
- package/dist/code-review/github-reporter.d.mts +2 -0
- package/dist/code-review/github-reporter.mjs +7 -5
- package/dist/code-review/github-reporter.mjs.map +1 -1
- package/dist/code-review.d.mts +3 -2
- package/dist/deepsec/eve-tool.mjs +3 -1
- package/dist/deepsec/eve-tool.mjs.map +1 -1
- package/dist/dispatch.d.mts +79 -8
- package/dist/dispatch.mjs +68 -9
- package/dist/dispatch.mjs.map +1 -1
- package/dist/eve/index.d.mts +70 -10
- package/dist/eve/index.mjs +93 -22
- package/dist/eve/index.mjs.map +1 -1
- package/dist/eve/invoke.mjs +24 -11
- package/dist/eve/invoke.mjs.map +1 -1
- package/dist/eve/session-client.d.mts +122 -4
- package/dist/eve/session-client.mjs +127 -13
- package/dist/eve/session-client.mjs.map +1 -1
- package/dist/eve/task-execution.d.mts +380 -0
- package/dist/eve/task-execution.mjs +57 -2
- package/dist/eve/task-execution.mjs.map +1 -1
- package/dist/eve/task-session.d.mts +44 -2
- package/dist/eve/task-session.mjs +44 -2
- package/dist/eve/task-session.mjs.map +1 -1
- package/dist/eve/transcript.mjs +5 -1
- package/dist/eve/transcript.mjs.map +1 -1
- package/dist/execution.d.mts +117 -9
- package/dist/execution.mjs +76 -6
- package/dist/execution.mjs.map +1 -1
- package/dist/finding-remediation/admission.d.mts +2 -0
- package/dist/finding-remediation/admission.mjs +4 -1
- package/dist/finding-remediation/admission.mjs.map +1 -1
- package/dist/findings.d.mts +1 -0
- package/dist/github-publication.d.mts +1 -0
- package/dist/github-publication.mjs +97 -84
- package/dist/github-publication.mjs.map +1 -1
- package/dist/github-transfer.d.mts +15 -6
- package/dist/github-transfer.mjs +214 -66
- package/dist/github-transfer.mjs.map +1 -1
- package/dist/github.d.mts +51 -11
- package/dist/github.mjs +126 -24
- package/dist/github.mjs.map +1 -1
- package/dist/inbox-activity.d.mts +53 -0
- package/dist/inbox-activity.mjs +41 -0
- package/dist/inbox-activity.mjs.map +1 -0
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -2
- package/dist/intake-contracts.d.mts +0 -1
- package/dist/integrations/deepsec.d.mts +1 -0
- package/dist/integrations/github.d.mts +2 -2
- package/dist/integrations/github.mjs +2 -2
- package/dist/integrations/slack.d.mts +3 -1
- package/dist/integrations/slack.mjs +3 -1
- package/dist/integrations/vercel.d.mts +4 -2
- package/dist/integrations/vercel.mjs +3 -2
- package/dist/merge-resolution/eve-tools.d.mts +1 -0
- package/dist/merge-resolution/eve-tools.mjs +7 -2
- package/dist/merge-resolution/eve-tools.mjs.map +1 -1
- package/dist/model-settings.d.mts +41 -0
- package/dist/model-settings.mjs +35 -0
- package/dist/model-settings.mjs.map +1 -0
- package/dist/planning/reconcile.mjs +6 -0
- package/dist/planning/reconcile.mjs.map +1 -1
- package/dist/postgres/index.d.mts +43 -2
- package/dist/postgres/index.mjs +40 -2
- package/dist/postgres/index.mjs.map +1 -1
- package/dist/presets/software-development/dispatch.d.mts +4 -1
- package/dist/presets/software-development/dispatch.mjs +2 -1
- package/dist/presets/software-development/dispatch.mjs.map +1 -1
- package/dist/presets/software-development/task-communication.d.mts +1 -0
- package/dist/presets/software-development/task-communication.mjs +48 -11
- package/dist/presets/software-development/task-communication.mjs.map +1 -1
- package/dist/presets/software-development.d.mts +1 -0
- package/dist/pull-requests/github-publisher.d.mts +15 -1
- package/dist/pull-requests/github-publisher.mjs +61 -1
- package/dist/pull-requests/github-publisher.mjs.map +1 -1
- package/dist/pull-requests.d.mts +1 -0
- package/dist/sandbox/index.d.mts +1 -0
- package/dist/schema/agent-route.d.mts +19 -1
- package/dist/schema/agent-route.mjs +19 -1
- package/dist/schema/agent-route.mjs.map +1 -1
- package/dist/schema/factory-config.d.mts +27 -0
- package/dist/schema/factory-config.mjs +33 -3
- package/dist/schema/factory-config.mjs.map +1 -1
- package/dist/schema/repository.d.mts +4 -0
- package/dist/schema/repository.mjs +5 -1
- package/dist/schema/repository.mjs.map +1 -1
- package/dist/schema/session.d.mts +1 -0
- package/dist/schema/session.mjs +1 -0
- package/dist/schema/session.mjs.map +1 -1
- package/dist/schema/slack-pr-notifications.d.mts +12 -0
- package/dist/schema/slack-pr-notifications.mjs +11 -0
- package/dist/schema/slack-pr-notifications.mjs.map +1 -0
- package/dist/schema/task-graph.d.mts +39 -0
- package/dist/schema/task.d.mts +1 -0
- package/dist/schema/task.mjs +2 -1
- package/dist/schema/task.mjs.map +1 -1
- package/dist/schema/transcript.d.mts +6 -0
- package/dist/schema/transcript.mjs +2 -1
- package/dist/schema/transcript.mjs.map +1 -1
- package/dist/schema/work.d.mts +52 -3
- package/dist/schema/work.mjs.map +1 -1
- package/dist/session-previews.d.mts +76 -0
- package/dist/session-previews.mjs +55 -0
- package/dist/session-previews.mjs.map +1 -0
- package/dist/session-review.d.mts +120 -0
- package/dist/session-review.mjs +79 -0
- package/dist/session-review.mjs.map +1 -0
- package/dist/signal-triage.mjs +1 -1
- package/dist/signals.d.mts +1 -0
- package/dist/stall.d.mts +4 -1
- package/dist/stall.mjs +6 -2
- package/dist/stall.mjs.map +1 -1
- package/dist/store/driver.d.mts +1 -1
- package/dist/store/driver.mjs.map +1 -1
- package/dist/store/engine.d.mts +206 -8
- package/dist/store/engine.mjs +147 -13
- package/dist/store/engine.mjs.map +1 -1
- package/dist/store/memory.d.mts +18 -1
- package/dist/store/memory.mjs +18 -1
- package/dist/store/memory.mjs.map +1 -1
- package/dist/store/slack-pr-notifications.d.mts +44 -0
- package/dist/store/slack-pr-notifications.mjs +121 -0
- package/dist/store/slack-pr-notifications.mjs.map +1 -0
- package/dist/store/task-work.d.mts +121 -6
- package/dist/store/task-work.mjs +7 -4
- package/dist/store/task-work.mjs.map +1 -1
- package/dist/sweep.d.mts +28 -6
- package/dist/sweep.mjs +34 -6
- package/dist/sweep.mjs.map +1 -1
- package/dist/task-graph-view.d.mts +3 -0
- package/dist/tasks.d.mts +3 -3
- package/dist/tasks.mjs +3 -3
- package/dist/vercel-git.d.mts +35 -3
- package/dist/vercel-git.mjs +265 -33
- package/dist/vercel-git.mjs.map +1 -1
- package/dist/vercel-github-api.d.mts +103 -0
- package/dist/vercel-github-api.mjs +363 -0
- package/dist/vercel-github-api.mjs.map +1 -0
- package/dist/vercel.d.mts +3 -2
- package/dist/vercel.mjs +3 -2
- package/dist/vercel.mjs.map +1 -1
- package/dist/work-triage.d.mts +1 -0
- package/dist/workflows.d.mts +102 -4
- package/dist/workflows.mjs +55 -2
- package/dist/workflows.mjs.map +1 -1
- package/dist/workspace-files-git.d.mts +15 -0
- package/dist/workspace-files-git.mjs +61 -0
- package/dist/workspace-files-git.mjs.map +1 -0
- package/dist/workspace-files.d.mts +107 -0
- package/dist/workspace-files.mjs +74 -0
- package/dist/workspace-files.mjs.map +1 -0
- package/docs/getting-started.md +104 -0
- package/docs/index.md +100 -0
- package/docs/recipes/cancellation.md +215 -0
- package/docs/recipes/custom-workflow.md +153 -0
- package/docs/recipes/dependent-tasks.md +207 -0
- package/docs/recipes/eve-agent.md +277 -0
- package/docs/recipes/human-input.md +204 -0
- package/docs/recipes/persistence-recovery.md +268 -0
- package/docs/recipes/retry-recovery.md +241 -0
- package/docs/recipes/task-messaging.md +215 -0
- package/docs/recipes/typed-eve-result.md +161 -0
- package/docs/runtime-integration.md +137 -0
- package/package.json +17 -6
package/dist/store/memory.d.mts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { StoreDriver } from "./driver.mjs";
|
|
2
2
|
//#region src/store/memory.d.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Creates an isolated process-local StoreDriver for development, examples, and tests.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Every call creates a new empty store. Records survive only for the lifetime of the returned
|
|
8
|
+
* object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate
|
|
9
|
+
* stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or
|
|
10
|
+
* `@vercel/factory/storage/postgres` in deployed runtimes.
|
|
11
|
+
*
|
|
12
|
+
* @returns A new empty driver whose `name` is `memory`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
|
|
17
|
+
*
|
|
18
|
+
* const stores = createStores({ driver: createInMemoryDriver() });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
4
21
|
declare function createInMemoryDriver(): StoreDriver;
|
|
5
22
|
//#endregion
|
|
6
23
|
export { createInMemoryDriver };
|
package/dist/store/memory.mjs
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { RecordExistsError, RecordNotFoundError, VersionConflictError } from "./driver.mjs";
|
|
2
2
|
//#region src/store/memory.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Creates an isolated process-local StoreDriver for development, examples, and tests.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Every call creates a new empty store. Records survive only for the lifetime of the returned
|
|
8
|
+
* object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate
|
|
9
|
+
* stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or
|
|
10
|
+
* `@vercel/factory/storage/postgres` in deployed runtimes.
|
|
11
|
+
*
|
|
12
|
+
* @returns A new empty driver whose `name` is `memory`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { createInMemoryDriver, createStores } from "@vercel/factory/storage";
|
|
17
|
+
*
|
|
18
|
+
* const stores = createStores({ driver: createInMemoryDriver() });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
4
21
|
function createInMemoryDriver() {
|
|
5
22
|
const collections = /* @__PURE__ */ new Map();
|
|
6
23
|
function collection(name) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.mjs","names":[],"sources":["../../src/store/memory.ts"],"sourcesContent":["import {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoredRecord,\n type StoreDriver,\n} from \"./driver\";\n\n
|
|
1
|
+
{"version":3,"file":"memory.mjs","names":[],"sources":["../../src/store/memory.ts"],"sourcesContent":["import {\n RecordExistsError,\n RecordNotFoundError,\n VersionConflictError,\n type CollectionName,\n type StoredEntry,\n type StoredRecord,\n type StoreDriver,\n} from \"./driver\";\n\n/**\n * Creates an isolated process-local StoreDriver for development, examples, and tests.\n *\n * @remarks\n * Every call creates a new empty store. Records survive only for the lifetime of the returned\n * object and are cloned at the driver boundary, so mutating caller-owned values cannot mutate\n * stored state. Use a persistent adapter from `@vercel/factory/storage/blob` or\n * `@vercel/factory/storage/postgres` in deployed runtimes.\n *\n * @returns A new empty driver whose `name` is `memory`.\n *\n * @example\n * ```ts\n * import { createInMemoryDriver, createStores } from \"@vercel/factory/storage\";\n *\n * const stores = createStores({ driver: createInMemoryDriver() });\n * ```\n */\nexport function createInMemoryDriver(): StoreDriver {\n const collections = new Map<CollectionName, Map<string, StoredRecord>>();\n\n function collection(name: CollectionName): Map<string, StoredRecord> {\n let existing = collections.get(name);\n if (existing === undefined) {\n existing = new Map();\n collections.set(name, existing);\n }\n return existing;\n }\n\n return {\n name: \"memory\",\n\n get(name, id) {\n const record = collection(name).get(id);\n return Promise.resolve(record === undefined ? null : structuredClone(record));\n },\n\n insert({ collection: name, id, value }) {\n const records = collection(name);\n if (records.has(id)) {\n return Promise.reject(new RecordExistsError(name, id));\n }\n records.set(id, { value: structuredClone(value), version: 1 });\n return Promise.resolve();\n },\n\n update({ collection: name, id, value, expectedVersion }) {\n const records = collection(name);\n const existing = records.get(id);\n if (existing === undefined) {\n return Promise.reject(new RecordNotFoundError(name, id));\n }\n if (existing.version !== expectedVersion) {\n return Promise.reject(new VersionConflictError(name, id, expectedVersion));\n }\n records.set(id, { value: structuredClone(value), version: existing.version + 1 });\n return Promise.resolve();\n },\n\n list(name) {\n const entries: StoredEntry[] = [...collection(name)].map(([id, record]) => ({\n id,\n record: structuredClone(record),\n }));\n return Promise.resolve(entries);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,uBAAoC;CAClD,MAAM,8BAAc,IAAI,IAA+C;CAEvE,SAAS,WAAW,MAAiD;EACnE,IAAI,WAAW,YAAY,IAAI,IAAI;EACnC,IAAI,aAAa,KAAA,GAAW;GAC1B,2BAAW,IAAI,IAAI;GACnB,YAAY,IAAI,MAAM,QAAQ;EAChC;EACA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EAEN,IAAI,MAAM,IAAI;GACZ,MAAM,SAAS,WAAW,IAAI,CAAC,CAAC,IAAI,EAAE;GACtC,OAAO,QAAQ,QAAQ,WAAW,KAAA,IAAY,OAAO,gBAAgB,MAAM,CAAC;EAC9E;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,SAAS;GACtC,MAAM,UAAU,WAAW,IAAI;GAC/B,IAAI,QAAQ,IAAI,EAAE,GAChB,OAAO,QAAQ,OAAO,IAAI,kBAAkB,MAAM,EAAE,CAAC;GAEvD,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS;GAAE,CAAC;GAC7D,OAAO,QAAQ,QAAQ;EACzB;EAEA,OAAO,EAAE,YAAY,MAAM,IAAI,OAAO,mBAAmB;GACvD,MAAM,UAAU,WAAW,IAAI;GAC/B,MAAM,WAAW,QAAQ,IAAI,EAAE;GAC/B,IAAI,aAAa,KAAA,GACf,OAAO,QAAQ,OAAO,IAAI,oBAAoB,MAAM,EAAE,CAAC;GAEzD,IAAI,SAAS,YAAY,iBACvB,OAAO,QAAQ,OAAO,IAAI,qBAAqB,MAAM,IAAI,eAAe,CAAC;GAE3E,QAAQ,IAAI,IAAI;IAAE,OAAO,gBAAgB,KAAK;IAAG,SAAS,SAAS,UAAU;GAAE,CAAC;GAChF,OAAO,QAAQ,QAAQ;EACzB;EAEA,KAAK,MAAM;GACT,MAAM,UAAyB,CAAC,GAAG,WAAW,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,aAAa;IAC1E;IACA,QAAQ,gBAAgB,MAAM;GAChC,EAAE;GACF,OAAO,QAAQ,QAAQ,OAAO;EAChC;CACF;AACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { RepositorySlug } from "../schema/repository.mjs";
|
|
2
|
+
import { StoreDriver } from "./driver.mjs";
|
|
3
|
+
import { SlackPrNotifications } from "../schema/slack-pr-notifications.mjs";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/store/slack-pr-notifications.d.ts
|
|
6
|
+
declare const commandSchema: z.ZodObject<{
|
|
7
|
+
operationId: z.ZodString;
|
|
8
|
+
actor: z.ZodString;
|
|
9
|
+
preference: z.ZodUnion<readonly [z.ZodReadonly<z.ZodObject<{
|
|
10
|
+
channelId: z.ZodString;
|
|
11
|
+
mentionUserIds: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
12
|
+
}, z.core.$strict>>, z.ZodNull, z.ZodLiteral<"config">]>;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
/** Effective PR notification settings and the source of the current choice. */
|
|
15
|
+
interface SlackPrNotificationSnapshot {
|
|
16
|
+
readonly revision: number;
|
|
17
|
+
readonly source: "config" | "slack";
|
|
18
|
+
readonly settings: SlackPrNotifications | null;
|
|
19
|
+
readonly updatedBy?: string;
|
|
20
|
+
readonly updatedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
/** A trusted Slack command: an object enables notifications, null disables them, and config resets. */
|
|
23
|
+
type UpdateSlackPrNotificationsInput = z.infer<typeof commandSchema>;
|
|
24
|
+
/** Durable PR notification preferences scoped to a connector, workspace, and optional repository. */
|
|
25
|
+
interface SlackPrNotificationStore {
|
|
26
|
+
get(): Promise<SlackPrNotificationSnapshot>;
|
|
27
|
+
/** The caller authenticates the operator; operationId must identify the original Slack message. */
|
|
28
|
+
update(input: UpdateSlackPrNotificationsInput): Promise<SlackPrNotificationSnapshot>;
|
|
29
|
+
}
|
|
30
|
+
/** Persistence scope and Git-configured fallback for PR notification preferences. */
|
|
31
|
+
interface SlackPrNotificationStoreOptions {
|
|
32
|
+
readonly driver: StoreDriver;
|
|
33
|
+
readonly connector: string;
|
|
34
|
+
readonly workspaceId: string;
|
|
35
|
+
/** Omit for the factory-wide default; supply a slug to isolate one repository's preferences. */
|
|
36
|
+
readonly repository?: RepositorySlug;
|
|
37
|
+
readonly defaults?: SlackPrNotifications | null;
|
|
38
|
+
readonly now?: () => string;
|
|
39
|
+
}
|
|
40
|
+
/** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */
|
|
41
|
+
declare function createSlackPrNotificationStore({ driver, connector, workspaceId, repository, defaults, now }: SlackPrNotificationStoreOptions): SlackPrNotificationStore;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { SlackPrNotificationSnapshot, SlackPrNotificationStore, SlackPrNotificationStoreOptions, UpdateSlackPrNotificationsInput, createSlackPrNotificationStore };
|
|
44
|
+
//# sourceMappingURL=slack-pr-notifications.d.mts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { repositorySlugSchema } from "../schema/repository.mjs";
|
|
2
|
+
import { slackPrNotificationsSchema } from "../schema/slack-pr-notifications.mjs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/store/slack-pr-notifications.ts
|
|
6
|
+
const preferenceSchema = z.union([
|
|
7
|
+
slackPrNotificationsSchema,
|
|
8
|
+
z.null(),
|
|
9
|
+
z.literal("config")
|
|
10
|
+
]);
|
|
11
|
+
const commandSchema = z.strictObject({
|
|
12
|
+
operationId: z.string().min(1).max(512).describe("Stable identity of the authenticated Slack message."),
|
|
13
|
+
actor: z.string().regex(/^slack:T[A-Z0-9]+:[UW][A-Z0-9]+$/u).describe("Authenticated Slack operator making this choice."),
|
|
14
|
+
preference: preferenceSchema.describe("Destination override, null to disable, or config to restore the file default.")
|
|
15
|
+
});
|
|
16
|
+
const recordSchema = commandSchema.extend({
|
|
17
|
+
schemaVersion: z.literal(1).describe("Persisted settings format version."),
|
|
18
|
+
expectedRevision: z.number().int().nonnegative().describe("Settings revision observed when this command was first recorded."),
|
|
19
|
+
updatedAt: z.iso.datetime().describe("Time the command was first recorded.")
|
|
20
|
+
});
|
|
21
|
+
function isConflict(error, ...names) {
|
|
22
|
+
return error instanceof Error && names.includes(error.name);
|
|
23
|
+
}
|
|
24
|
+
/** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */
|
|
25
|
+
function createSlackPrNotificationStore({ driver, connector, workspaceId, repository, defaults, now = () => (/* @__PURE__ */ new Date()).toISOString() }) {
|
|
26
|
+
z.string().regex(/^slack\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u).parse(connector);
|
|
27
|
+
z.string().regex(/^T[A-Z0-9]+$/u).parse(workspaceId);
|
|
28
|
+
if (repository !== void 0) repositorySlugSchema.parse(repository);
|
|
29
|
+
const fallback = defaults == null ? null : slackPrNotificationsSchema.parse(defaults);
|
|
30
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
31
|
+
const id = `slack-pr-${hash(JSON.stringify(repository === void 0 ? [connector, workspaceId] : [
|
|
32
|
+
connector,
|
|
33
|
+
workspaceId,
|
|
34
|
+
repository
|
|
35
|
+
]))}`;
|
|
36
|
+
const collection = "communicationSettings";
|
|
37
|
+
async function read() {
|
|
38
|
+
const record = await driver.get(collection, id);
|
|
39
|
+
return record === null ? null : {
|
|
40
|
+
version: record.version,
|
|
41
|
+
value: recordSchema.parse(record.value)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function snapshot(record) {
|
|
45
|
+
const preference = record === null ? "config" : record.value.preference;
|
|
46
|
+
return {
|
|
47
|
+
revision: record?.version ?? 0,
|
|
48
|
+
source: preference === "config" ? "config" : "slack",
|
|
49
|
+
settings: preference === "config" ? fallback : preference,
|
|
50
|
+
...record && {
|
|
51
|
+
updatedBy: record.value.actor,
|
|
52
|
+
updatedAt: record.value.updatedAt
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function update(value) {
|
|
57
|
+
const input = commandSchema.parse(value);
|
|
58
|
+
if (!input.actor.startsWith(`slack:${workspaceId}:`)) throw new Error("PR notification operator belongs to another Slack workspace.");
|
|
59
|
+
const commandId = `${id}-command-${hash(input.operationId)}`;
|
|
60
|
+
let command = await driver.get(collection, commandId);
|
|
61
|
+
if (command === null) {
|
|
62
|
+
const current = await read();
|
|
63
|
+
const intent = recordSchema.parse({
|
|
64
|
+
...input,
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
expectedRevision: current?.version ?? 0,
|
|
67
|
+
updatedAt: now()
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
await driver.insert({
|
|
71
|
+
collection,
|
|
72
|
+
id: commandId,
|
|
73
|
+
value: intent
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (!isConflict(error, "RecordExistsError")) throw error;
|
|
77
|
+
}
|
|
78
|
+
command = await driver.get(collection, commandId);
|
|
79
|
+
}
|
|
80
|
+
if (command === null) throw new Error("PR notification command was not saved.");
|
|
81
|
+
const intent = recordSchema.parse(command.value);
|
|
82
|
+
if (JSON.stringify({
|
|
83
|
+
operationId: intent.operationId,
|
|
84
|
+
actor: intent.actor,
|
|
85
|
+
preference: intent.preference
|
|
86
|
+
}) !== JSON.stringify(input)) throw new Error("This Slack message already requested different PR notification settings.");
|
|
87
|
+
const current = await read();
|
|
88
|
+
if (current?.value.operationId === input.operationId) return snapshot(current);
|
|
89
|
+
if ((current?.version ?? 0) !== intent.expectedRevision) throw new Error("PR notification settings changed after this command. Send a new Slack message to change them again.");
|
|
90
|
+
try {
|
|
91
|
+
if (current === null) await driver.insert({
|
|
92
|
+
collection,
|
|
93
|
+
id,
|
|
94
|
+
value: intent
|
|
95
|
+
});
|
|
96
|
+
else await driver.update({
|
|
97
|
+
collection,
|
|
98
|
+
id,
|
|
99
|
+
value: intent,
|
|
100
|
+
expectedVersion: current.version
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!isConflict(error, "RecordExistsError", "VersionConflictError")) throw error;
|
|
104
|
+
const winner = await read();
|
|
105
|
+
if (winner?.value.operationId === input.operationId) return snapshot(winner);
|
|
106
|
+
throw new Error("Another command changed PR notification settings. Send a new Slack message to change them again.", { cause: error });
|
|
107
|
+
}
|
|
108
|
+
return snapshot({
|
|
109
|
+
value: intent,
|
|
110
|
+
version: intent.expectedRevision + 1
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
get: async () => snapshot(await read()),
|
|
115
|
+
update
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { createSlackPrNotificationStore };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=slack-pr-notifications.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack-pr-notifications.mjs","names":[],"sources":["../../src/store/slack-pr-notifications.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { z } from \"zod\";\nimport {\n slackPrNotificationsSchema,\n type SlackPrNotifications,\n} from \"../schema/slack-pr-notifications\";\nimport type { StoreDriver } from \"./driver\";\nimport { repositorySlugSchema, type RepositorySlug } from \"../schema/repository\";\n\nconst preferenceSchema = z.union([slackPrNotificationsSchema, z.null(), z.literal(\"config\")]);\nconst commandSchema = z.strictObject({\n operationId: z\n .string()\n .min(1)\n .max(512)\n .describe(\"Stable identity of the authenticated Slack message.\"),\n actor: z\n .string()\n .regex(/^slack:T[A-Z0-9]+:[UW][A-Z0-9]+$/u)\n .describe(\"Authenticated Slack operator making this choice.\"),\n preference: preferenceSchema.describe(\n \"Destination override, null to disable, or config to restore the file default.\",\n ),\n});\nconst recordSchema = commandSchema.extend({\n schemaVersion: z.literal(1).describe(\"Persisted settings format version.\"),\n expectedRevision: z\n .number()\n .int()\n .nonnegative()\n .describe(\"Settings revision observed when this command was first recorded.\"),\n updatedAt: z.iso.datetime().describe(\"Time the command was first recorded.\"),\n});\n\n// Drivers loaded through separate package entry points have distinct error constructors.\nfunction isConflict(error: unknown, ...names: string[]): boolean {\n return error instanceof Error && names.includes(error.name);\n}\n\n/** Effective PR notification settings and the source of the current choice. */\nexport interface SlackPrNotificationSnapshot {\n readonly revision: number;\n readonly source: \"config\" | \"slack\";\n readonly settings: SlackPrNotifications | null;\n readonly updatedBy?: string;\n readonly updatedAt?: string;\n}\n\n/** A trusted Slack command: an object enables notifications, null disables them, and config resets. */\nexport type UpdateSlackPrNotificationsInput = z.infer<typeof commandSchema>;\n\n/** Durable PR notification preferences scoped to a connector, workspace, and optional repository. */\nexport interface SlackPrNotificationStore {\n get(): Promise<SlackPrNotificationSnapshot>;\n /** The caller authenticates the operator; operationId must identify the original Slack message. */\n update(input: UpdateSlackPrNotificationsInput): Promise<SlackPrNotificationSnapshot>;\n}\n\n/** Persistence scope and Git-configured fallback for PR notification preferences. */\nexport interface SlackPrNotificationStoreOptions {\n readonly driver: StoreDriver;\n readonly connector: string;\n readonly workspaceId: string;\n /** Omit for the factory-wide default; supply a slug to isolate one repository's preferences. */\n readonly repository?: RepositorySlug;\n readonly defaults?: SlackPrNotifications | null;\n readonly now?: () => string;\n}\n\n/** Creates directly addressed settings with immutable command intents and compare-and-swap updates. */\nexport function createSlackPrNotificationStore({\n driver,\n connector,\n workspaceId,\n repository,\n defaults,\n now = () => new Date().toISOString(),\n}: SlackPrNotificationStoreOptions): SlackPrNotificationStore {\n z.string()\n .regex(/^slack\\/[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u)\n .parse(connector);\n z.string()\n .regex(/^T[A-Z0-9]+$/u)\n .parse(workspaceId);\n if (repository !== undefined) repositorySlugSchema.parse(repository);\n const fallback = defaults == null ? null : slackPrNotificationsSchema.parse(defaults);\n const hash = (value: string) => createHash(\"sha256\").update(value).digest(\"hex\");\n const scope =\n repository === undefined ? [connector, workspaceId] : [connector, workspaceId, repository];\n const id = `slack-pr-${hash(JSON.stringify(scope))}`;\n const collection = \"communicationSettings\";\n\n async function read() {\n const record = await driver.get(collection, id);\n return record === null\n ? null\n : { version: record.version, value: recordSchema.parse(record.value) };\n }\n\n function snapshot(record: Awaited<ReturnType<typeof read>>): SlackPrNotificationSnapshot {\n const preference = record === null ? \"config\" : record.value.preference;\n return {\n revision: record?.version ?? 0,\n source: preference === \"config\" ? \"config\" : \"slack\",\n settings: preference === \"config\" ? fallback : preference,\n ...(record && { updatedBy: record.value.actor, updatedAt: record.value.updatedAt }),\n };\n }\n\n async function update(value: UpdateSlackPrNotificationsInput) {\n const input = commandSchema.parse(value);\n if (!input.actor.startsWith(`slack:${workspaceId}:`))\n throw new Error(\"PR notification operator belongs to another Slack workspace.\");\n const commandId = `${id}-command-${hash(input.operationId)}`;\n let command = await driver.get(collection, commandId);\n if (command === null) {\n const current = await read();\n const intent = recordSchema.parse({\n ...input,\n schemaVersion: 1,\n expectedRevision: current?.version ?? 0,\n updatedAt: now(),\n });\n try {\n await driver.insert({ collection, id: commandId, value: intent });\n } catch (error) {\n if (!isConflict(error, \"RecordExistsError\")) throw error;\n }\n command = await driver.get(collection, commandId);\n }\n if (command === null) throw new Error(\"PR notification command was not saved.\");\n const intent = recordSchema.parse(command.value);\n if (\n JSON.stringify({\n operationId: intent.operationId,\n actor: intent.actor,\n preference: intent.preference,\n }) !== JSON.stringify(input)\n )\n throw new Error(\"This Slack message already requested different PR notification settings.\");\n\n const current = await read();\n if (current?.value.operationId === input.operationId) return snapshot(current);\n if ((current?.version ?? 0) !== intent.expectedRevision)\n throw new Error(\n \"PR notification settings changed after this command. Send a new Slack message to change them again.\",\n );\n try {\n if (current === null) await driver.insert({ collection, id, value: intent });\n else await driver.update({ collection, id, value: intent, expectedVersion: current.version });\n } catch (error) {\n if (!isConflict(error, \"RecordExistsError\", \"VersionConflictError\")) throw error;\n const winner = await read();\n if (winner?.value.operationId === input.operationId) return snapshot(winner);\n throw new Error(\n \"Another command changed PR notification settings. Send a new Slack message to change them again.\",\n { cause: error },\n );\n }\n return snapshot({ value: intent, version: intent.expectedRevision + 1 });\n }\n\n return { get: async () => snapshot(await read()), update };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TaskId } from "../schema/id.mjs";
|
|
2
2
|
import { JsonValue, WorkflowBinding } from "../schema/work.mjs";
|
|
3
|
-
import { Task } from "../schema/task.mjs";
|
|
3
|
+
import { ExecutionReference, Task, TaskState } from "../schema/task.mjs";
|
|
4
4
|
import { WorkflowRegistry } from "../workflows.mjs";
|
|
5
5
|
import { IsoDateTimeClock } from "../schema/common.mjs";
|
|
6
6
|
import { TaskAttemptRef } from "../schema/execution.mjs";
|
|
@@ -9,57 +9,172 @@ import { FactoryStores } from "./engine.mjs";
|
|
|
9
9
|
//#region src/store/task-work.d.ts
|
|
10
10
|
/** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */
|
|
11
11
|
interface TaskWorkStore {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Completes through the dynamic registry boundary, which validates unknown output at runtime.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* The exact Task attempt is mandatory. The engine validates output against the Task's stored
|
|
17
|
+
* workflow, persists it with the `succeeded` transition, and returns only after that write. A
|
|
18
|
+
* replay with the same validated output returns the existing succeeded Task; different output
|
|
19
|
+
* is refused. This method does not authenticate a provider callback—use a fence and establish
|
|
20
|
+
* caller authority before invoking it.
|
|
21
|
+
*
|
|
22
|
+
* @param input - Exact Task attempt, unknown candidate output, and optional execution fence.
|
|
23
|
+
* @returns The durably succeeded Task containing its validated `workResult`.
|
|
24
|
+
* @throws When the attempt, lifecycle state, execution fence, or runtime output validation fails.
|
|
25
|
+
*/
|
|
13
26
|
complete(input: CompleteTaskWorkInput): Promise<Task>;
|
|
14
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Completes work while statically checking output against its exact workflow binding.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* In addition to `complete` semantics, the supplied binding must equal the Task's persisted
|
|
32
|
+
* workflow ID and version. Retain historical workflow definitions in the registry while stored
|
|
33
|
+
* Tasks may still use them. For Eve tools, authenticate first with `requireTaskExecution` and
|
|
34
|
+
* pass its attempt and execution as the completion fence.
|
|
35
|
+
*
|
|
36
|
+
* @param input - Exact Task attempt, typed workflow binding and output, and optional fence.
|
|
37
|
+
* @returns The durably succeeded Task containing its validated `workResult`.
|
|
38
|
+
* @throws When the workflow, attempt, lifecycle state, execution fence, or output is invalid.
|
|
39
|
+
*/
|
|
15
40
|
completeWorkflow<Output extends JsonValue>(input: CompleteWorkflowTaskWorkInput<Output>): Promise<Task>;
|
|
41
|
+
/**
|
|
42
|
+
* Marks a running exact Task attempt as intentionally inactive while awaiting messages.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* This keeps the Task in `running`; it does not stall, cancel, or create a new attempt. The
|
|
46
|
+
* waiting marker and optional workflow phase are durable when the promise resolves. Repeating
|
|
47
|
+
* the same target state is a no-op. A stale attempt or non-running Task is refused, so callers
|
|
48
|
+
* should reread the Task before deciding whether a later attempt still needs the message.
|
|
49
|
+
*
|
|
50
|
+
* @param input - Exact running attempt and optional workflow phase.
|
|
51
|
+
* @returns The updated running Task with `waitingForMessages` set.
|
|
52
|
+
* @throws When the Task is absent, no longer running, or on a different attempt.
|
|
53
|
+
*/
|
|
16
54
|
waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;
|
|
55
|
+
/**
|
|
56
|
+
* Resumes an exact message-waiting Task attempt and records fresh progress.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* The Task remains `running`, its waiting marker is cleared, and `lastProgressAt` advances. The
|
|
60
|
+
* write is durable when the promise resolves and repeating it for an already-resumed attempt is
|
|
61
|
+
* a no-op. Resuming does not claim or acknowledge messages; the workflow owns that ordering.
|
|
62
|
+
*
|
|
63
|
+
* @param input - Exact running attempt to resume.
|
|
64
|
+
* @returns The updated running Task with message waiting cleared.
|
|
65
|
+
* @throws When the Task is absent, no longer running, or on a different attempt.
|
|
66
|
+
*/
|
|
17
67
|
resume(input: ResumeTaskWorkInput): Promise<Task>;
|
|
18
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Repairs admitted creation snapshots by direct graph IDs, never by scanning the bucket.
|
|
70
|
+
*
|
|
71
|
+
* @remarks
|
|
72
|
+
* The canonical graph contains immutable admission snapshots. Recovery validates every
|
|
73
|
+
* admission against the exact registered workflow, restores only missing Task records, and
|
|
74
|
+
* returns existing records with their current mutable progress. It is safe to repeat after an
|
|
75
|
+
* interrupted Task creation.
|
|
76
|
+
*
|
|
77
|
+
* This operation repairs Factory persistence only. It does not start or reattach a provider
|
|
78
|
+
* execution, rerun workflow code, retry a Task, or migrate old Task JSON. After recovery, the
|
|
79
|
+
* caller must inspect each Task's lifecycle and recorded execution and apply workflow-owned
|
|
80
|
+
* reconciliation policy.
|
|
81
|
+
*
|
|
82
|
+
* @param rootTaskId - Root of the canonical Task graph to repair and read.
|
|
83
|
+
* @returns Every validated Task admitted to that graph.
|
|
84
|
+
* @throws When the graph is absent, a stored workflow version is not registered, persisted data
|
|
85
|
+
* is invalid, or a current Task no longer matches its immutable admission identity.
|
|
86
|
+
*/
|
|
19
87
|
recover(rootTaskId: TaskId): Promise<readonly Task[]>;
|
|
20
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* Aggregates Task-local measured usage once without charging parent roll-ups again.
|
|
90
|
+
*
|
|
91
|
+
* @remarks
|
|
92
|
+
* This first performs the same persistence repair and validation as {@link recover}. For each
|
|
93
|
+
* Task, recorded step usage is authoritative; otherwise a settled amount is used, then zero.
|
|
94
|
+
* The result is a current snapshot, not a reservation or billing transaction, and calling it
|
|
95
|
+
* does not write settlement receipts.
|
|
96
|
+
*
|
|
97
|
+
* @param rootTaskId - Root of the canonical Task graph to aggregate.
|
|
98
|
+
* @returns Cost, token, and Task counts for the recovered graph.
|
|
99
|
+
* @throws The same persistence and validation errors as {@link recover}.
|
|
100
|
+
*/
|
|
21
101
|
usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;
|
|
22
102
|
}
|
|
23
103
|
/** Exact Task attempt and candidate workflow output used to complete work. */
|
|
24
104
|
interface CompleteTaskWorkInput {
|
|
105
|
+
/** Task ID and current positive attempt; stale attempts are refused. */
|
|
25
106
|
task: TaskAttemptRef;
|
|
107
|
+
/** Candidate value validated against the Task's exact registered output schema. */
|
|
26
108
|
output: unknown;
|
|
109
|
+
/**
|
|
110
|
+
* Optional source-state and execution fence for provider callbacks. Omit only when trusted
|
|
111
|
+
* workflow code already owns lifecycle authority.
|
|
112
|
+
*/
|
|
113
|
+
fence?: TaskWorkCompletionFence;
|
|
27
114
|
}
|
|
28
115
|
/** Exact Task attempt, workflow contract, and statically checked output used to complete work. */
|
|
29
116
|
interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {
|
|
117
|
+
/** Task ID and current positive attempt; stale attempts are refused. */
|
|
30
118
|
task: TaskAttemptRef;
|
|
119
|
+
/** Exact binding required to match the Task's persisted workflow. */
|
|
31
120
|
workflow: WorkflowBinding<JsonValue, Output>;
|
|
121
|
+
/** Candidate output statically carried by the binding and validated again at runtime. */
|
|
32
122
|
output: NoInfer<Output>;
|
|
123
|
+
/**
|
|
124
|
+
* Optional source-state and execution fence for provider callbacks. Omit only when trusted
|
|
125
|
+
* workflow code already owns lifecycle authority.
|
|
126
|
+
*/
|
|
127
|
+
fence?: TaskWorkCompletionFence;
|
|
128
|
+
}
|
|
129
|
+
/** Atomic source-state and provider-execution conditions for a workflow completion write. */
|
|
130
|
+
interface TaskWorkCompletionFence {
|
|
131
|
+
/** Lifecycle state required atomically with completion, normally `running`. */
|
|
132
|
+
readonly from: TaskState;
|
|
133
|
+
/** Provider execution reference required to remain current at the completion write. */
|
|
134
|
+
readonly execution: ExecutionReference;
|
|
33
135
|
}
|
|
34
136
|
/** Aggregated measured usage across one canonical Task graph. */
|
|
35
137
|
interface TaskWorkUsage {
|
|
138
|
+
/** Sum of measured Task usage, falling back to settlement only when no usage is recorded. */
|
|
36
139
|
costUsd: number;
|
|
140
|
+
/** Sum of recorded provider input tokens across Tasks. */
|
|
37
141
|
inputTokens: number;
|
|
142
|
+
/** Sum of recorded provider output tokens across Tasks. */
|
|
38
143
|
outputTokens: number;
|
|
144
|
+
/** Number of Tasks admitted to the recovered canonical graph. */
|
|
39
145
|
taskCount: number;
|
|
40
146
|
}
|
|
41
147
|
/** Exact running Task attempt and optional workflow phase used to await messages. */
|
|
42
148
|
interface WaitForTaskMessagesInput {
|
|
149
|
+
/** Task ID and current positive attempt; stale attempts are refused. */
|
|
43
150
|
task: TaskAttemptRef;
|
|
151
|
+
/** Optional durable workflow-owned phase label retained across waiting and resumption. */
|
|
44
152
|
phase?: string;
|
|
45
153
|
}
|
|
46
154
|
/** Exact running Task attempt to resume after message delivery. */
|
|
47
155
|
interface ResumeTaskWorkInput {
|
|
156
|
+
/** Task ID and current positive attempt; stale attempts are refused. */
|
|
48
157
|
task: TaskAttemptRef;
|
|
49
158
|
}
|
|
50
159
|
/** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */
|
|
51
160
|
type TaskWorkStores = Pick<FactoryStores, "graphs" | "receipts" | "tasks">;
|
|
52
161
|
/** Engine-owned dependencies used to construct workflow-facing Task operations. */
|
|
53
162
|
interface CreateTaskWorkStoreOptions {
|
|
163
|
+
/** Policy-free persistence driver used for direct canonical Task reads. */
|
|
54
164
|
driver: StoreDriver;
|
|
165
|
+
/** ISO timestamp source shared with the surrounding store engine. */
|
|
55
166
|
now: IsoDateTimeClock;
|
|
167
|
+
/** Lazily resolves graph, receipt, and Task operations after engine construction. */
|
|
56
168
|
stores: () => TaskWorkStores;
|
|
169
|
+
/** Exact workflow versions used to validate stored input and completion output. */
|
|
57
170
|
workflows: WorkflowRegistry;
|
|
171
|
+
/** Persists a validated Task with compare-and-swap semantics. */
|
|
58
172
|
update(task: Task, version: number): Promise<void>;
|
|
173
|
+
/** Recreates a missing canonical Task snapshot from its immutable graph admission. */
|
|
59
174
|
restore(task: Task): Promise<Task>;
|
|
60
175
|
}
|
|
61
176
|
/** Creates workflow-facing Task operations backed by canonical graph and Task stores. */
|
|
62
177
|
declare function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore;
|
|
63
178
|
//#endregion
|
|
64
|
-
export { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore };
|
|
179
|
+
export { CompleteTaskWorkInput, CompleteWorkflowTaskWorkInput, CreateTaskWorkStoreOptions, ResumeTaskWorkInput, TaskWorkCompletionFence, TaskWorkStore, TaskWorkStores, TaskWorkUsage, WaitForTaskMessagesInput, createTaskWorkStore };
|
|
65
180
|
//# sourceMappingURL=task-work.d.mts.map
|
package/dist/store/task-work.mjs
CHANGED
|
@@ -76,10 +76,11 @@ function createTaskWorkStore(options) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
async function complete(input, expectedWorkflow) {
|
|
79
|
-
const { task: { taskId: id, attempt: expectAttempt }, output: value } = input;
|
|
79
|
+
const { task: { taskId: id, attempt: expectAttempt }, output: value, fence } = input;
|
|
80
80
|
for (;;) {
|
|
81
81
|
const { task, work } = await read(id);
|
|
82
82
|
assertAttempt(task, expectAttempt);
|
|
83
|
+
if (fence !== void 0 && !isDeepStrictEqual(task.execution, fence.execution)) throw new Error(`Task ${id} execution has been replaced`);
|
|
83
84
|
if (expectedWorkflow !== void 0 && !isDeepStrictEqual(work.workflow, expectedWorkflow)) throw new Error(`Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`);
|
|
84
85
|
if (task.state === "succeeded") {
|
|
85
86
|
const output = workflows.validateOutput(work.workflow, value);
|
|
@@ -90,7 +91,8 @@ function createTaskWorkStore(options) {
|
|
|
90
91
|
return await stores().tasks.transition(id, "succeeded", {
|
|
91
92
|
output: value,
|
|
92
93
|
expectAttempt,
|
|
93
|
-
expectFrom: task.state,
|
|
94
|
+
expectFrom: fence?.from ?? task.state,
|
|
95
|
+
...fence === void 0 ? {} : { expectExecution: fence.execution },
|
|
94
96
|
phase: "workflow"
|
|
95
97
|
});
|
|
96
98
|
} catch (error) {
|
|
@@ -102,9 +104,10 @@ function createTaskWorkStore(options) {
|
|
|
102
104
|
}
|
|
103
105
|
return {
|
|
104
106
|
complete,
|
|
105
|
-
completeWorkflow: ({ task, workflow, output }) => complete({
|
|
107
|
+
completeWorkflow: ({ task, workflow, output, fence }) => complete({
|
|
106
108
|
task,
|
|
107
|
-
output
|
|
109
|
+
output,
|
|
110
|
+
fence
|
|
108
111
|
}, workflow),
|
|
109
112
|
waitForMessages: ({ task: { taskId, attempt }, phase }) => setWaiting(taskId, attempt, true, phase),
|
|
110
113
|
resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-work.mjs","names":[],"sources":["../../src/store/task-work.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\nimport { z } from \"zod\";\nimport type { IsoDateTimeClock } from \"../schema/common\";\nimport type { TaskAttemptRef } from \"../schema/execution\";\nimport type { TaskId } from \"../schema/id\";\nimport { taskSchema, type Task } from \"../schema/task\";\nimport { InvalidTransitionError } from \"../schema/transitions\";\nimport type { JsonValue, WorkflowBinding } from \"../schema/work\";\nimport type { WorkflowRegistry } from \"../workflows\";\nimport { RecordNotFoundError, VersionConflictError, type StoreDriver } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport { assertTaskAdmission } from \"./task-graphs\";\n\n/** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */\nexport interface TaskWorkStore {\n /** Complete through the dynamic registry boundary, which validates unknown output at runtime. */\n complete(input: CompleteTaskWorkInput): Promise<Task>;\n /** Complete work while statically checking output against its exact workflow binding. */\n completeWorkflow<Output extends JsonValue>(\n input: CompleteWorkflowTaskWorkInput<Output>,\n ): Promise<Task>;\n waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;\n resume(input: ResumeTaskWorkInput): Promise<Task>;\n /** Repair admitted creation snapshots by direct graph IDs, never by scanning the bucket. */\n recover(rootTaskId: TaskId): Promise<readonly Task[]>;\n /** Each Task contributes local measured usage once; no parent roll-up is charged again. */\n usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;\n}\n\n/** Exact Task attempt and candidate workflow output used to complete work. */\nexport interface CompleteTaskWorkInput {\n task: TaskAttemptRef;\n output: unknown;\n}\n\n/** Exact Task attempt, workflow contract, and statically checked output used to complete work. */\nexport interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {\n task: TaskAttemptRef;\n workflow: WorkflowBinding<JsonValue, Output>;\n output: NoInfer<Output>;\n}\n\n/** Aggregated measured usage across one canonical Task graph. */\nexport interface TaskWorkUsage {\n costUsd: number;\n inputTokens: number;\n outputTokens: number;\n taskCount: number;\n}\n\n/** Exact running Task attempt and optional workflow phase used to await messages. */\nexport interface WaitForTaskMessagesInput {\n task: TaskAttemptRef;\n phase?: string;\n}\n\n/** Exact running Task attempt to resume after message delivery. */\nexport interface ResumeTaskWorkInput {\n task: TaskAttemptRef;\n}\n\n/** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */\nexport type TaskWorkStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/** Engine-owned dependencies used to construct workflow-facing Task operations. */\nexport interface CreateTaskWorkStoreOptions {\n driver: StoreDriver;\n now: IsoDateTimeClock;\n stores: () => TaskWorkStores;\n workflows: WorkflowRegistry;\n update(task: Task, version: number): Promise<void>;\n restore(task: Task): Promise<Task>;\n}\n\n/** Creates workflow-facing Task operations backed by canonical graph and Task stores. */\nexport function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore {\n const { driver, now, stores, workflows, update, restore } = options;\n function parseTask(value: unknown): Task {\n const task = taskSchema.parse(value);\n workflows.validateStoredInput(task.work.workflow, task.work.input);\n if (task.workResult) workflows.validateStoredOutput(task.work.workflow, task.workResult.output);\n return task;\n }\n async function read(id: TaskId) {\n const record = await driver.get(\"tasks\", id);\n if (!record) throw new RecordNotFoundError(\"tasks\", id);\n const task = parseTask(record.value);\n if (task.id !== id) throw new Error(`Task ${id} does not match its storage key`);\n return { task, version: record.version, work: task.work };\n }\n async function recover(rootTaskId: TaskId): Promise<readonly Task[]> {\n const graph = await stores().graphs.get(rootTaskId);\n if (!graph) throw new RecordNotFoundError(\"taskGraphs\", rootTaskId);\n const tasks: Task[] = [];\n for (const node of graph.nodes) {\n const admission = parseTask(node.admission);\n const existing = await stores().tasks.get(node.taskId);\n const task = parseTask(existing ?? (await restore(admission)));\n assertTaskAdmission({ task, admission });\n tasks.push(task);\n }\n return tasks;\n }\n function assertAttempt(task: Task, expectAttempt: number) {\n z.number().int().positive().parse(expectAttempt);\n if (task.attempt !== expectAttempt)\n throw new Error(`Task ${task.id} is on attempt ${task.attempt}, not ${expectAttempt}`);\n }\n async function setWaiting(\n id: TaskId,\n expectAttempt: number,\n waiting: boolean,\n phase?: string,\n ): Promise<Task> {\n for (;;) {\n const { task, version } = await read(id);\n assertAttempt(task, expectAttempt);\n if (task.state !== \"running\")\n throw new Error(`Task ${id} must be running to ${waiting ? \"wait\" : \"resume\"}`);\n const next = taskSchema.parse({\n ...task,\n waitingForMessages: waiting,\n workflowPhase: phase ?? task.workflowPhase,\n lastProgressAt: waiting ? task.lastProgressAt : now(),\n updatedAt: now(),\n });\n if (task.waitingForMessages === waiting && next.workflowPhase === task.workflowPhase)\n return task;\n await stores().receipts.append({\n taskId: id,\n repositoryIds: task.repositoryIds,\n kind: task.kind,\n state: waiting ? \"waiting\" : \"resumed\",\n phase: \"workflow\",\n attempt: task.attempt,\n reason: waiting\n ? \"Workflow is waiting for Task messages\"\n : \"Workflow resumed message processing\",\n dedupeKey: `work:${id}:${version}:${waiting ? \"wait\" : \"resume\"}`,\n });\n try {\n await update(next, version);\n return next;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n }\n async function complete(\n input: CompleteTaskWorkInput,\n expectedWorkflow?: WorkflowBinding,\n ): Promise<Task> {\n const {\n task: { taskId: id, attempt: expectAttempt },\n output: value,\n } = input;\n for (;;) {\n const { task, work } = await read(id);\n assertAttempt(task, expectAttempt);\n if (expectedWorkflow !== undefined && !isDeepStrictEqual(work.workflow, expectedWorkflow)) {\n throw new Error(\n `Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`,\n );\n }\n if (task.state === \"succeeded\") {\n const output = workflows.validateOutput(work.workflow, value);\n if (!isDeepStrictEqual(task.workResult?.output, output)) {\n throw new Error(`Task ${id} already recorded a different workflow output`);\n }\n return task;\n }\n try {\n return await stores().tasks.transition(id, \"succeeded\", {\n output: value,\n expectAttempt,\n expectFrom: task.state,\n phase: \"workflow\",\n });\n } catch (error) {\n if (error instanceof VersionConflictError) continue;\n if (error instanceof InvalidTransitionError && (await read(id)).task.state === \"succeeded\")\n continue;\n throw error;\n }\n }\n }\n return {\n complete,\n completeWorkflow: ({ task, workflow, output }) => complete({ task, output }, workflow),\n waitForMessages: ({ task: { taskId, attempt }, phase }) =>\n setWaiting(taskId, attempt, true, phase),\n resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),\n recover,\n async usage(rootTaskId) {\n const tasks = await recover(rootTaskId);\n let micros = 0;\n let inputTokens = 0;\n let outputTokens = 0;\n for (const task of tasks) {\n micros += Math.round((task.usage?.costUsd ?? task.settledUsd ?? 0) * 1_000_000);\n inputTokens += task.usage?.inputTokens ?? 0;\n outputTokens += task.usage?.outputTokens ?? 0;\n }\n return { costUsd: micros / 1_000_000, inputTokens, outputTokens, taskCount: tasks.length };\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"task-work.mjs","names":[],"sources":["../../src/store/task-work.ts"],"sourcesContent":["import { isDeepStrictEqual } from \"node:util\";\nimport { z } from \"zod\";\nimport type { IsoDateTimeClock } from \"../schema/common\";\nimport type { TaskAttemptRef } from \"../schema/execution\";\nimport type { TaskId } from \"../schema/id\";\nimport { taskSchema, type ExecutionReference, type Task, type TaskState } from \"../schema/task\";\nimport { InvalidTransitionError } from \"../schema/transitions\";\nimport type { JsonValue, WorkflowBinding } from \"../schema/work\";\nimport type { WorkflowRegistry } from \"../workflows\";\nimport { RecordNotFoundError, VersionConflictError, type StoreDriver } from \"./driver\";\nimport type { FactoryStores } from \"./engine\";\nimport { assertTaskAdmission } from \"./task-graphs\";\n\n/** Workflow-facing Task operations for completion, message waiting, recovery, and usage aggregation. */\nexport interface TaskWorkStore {\n /**\n * Completes through the dynamic registry boundary, which validates unknown output at runtime.\n *\n * @remarks\n * The exact Task attempt is mandatory. The engine validates output against the Task's stored\n * workflow, persists it with the `succeeded` transition, and returns only after that write. A\n * replay with the same validated output returns the existing succeeded Task; different output\n * is refused. This method does not authenticate a provider callback—use a fence and establish\n * caller authority before invoking it.\n *\n * @param input - Exact Task attempt, unknown candidate output, and optional execution fence.\n * @returns The durably succeeded Task containing its validated `workResult`.\n * @throws When the attempt, lifecycle state, execution fence, or runtime output validation fails.\n */\n complete(input: CompleteTaskWorkInput): Promise<Task>;\n /**\n * Completes work while statically checking output against its exact workflow binding.\n *\n * @remarks\n * In addition to `complete` semantics, the supplied binding must equal the Task's persisted\n * workflow ID and version. Retain historical workflow definitions in the registry while stored\n * Tasks may still use them. For Eve tools, authenticate first with `requireTaskExecution` and\n * pass its attempt and execution as the completion fence.\n *\n * @param input - Exact Task attempt, typed workflow binding and output, and optional fence.\n * @returns The durably succeeded Task containing its validated `workResult`.\n * @throws When the workflow, attempt, lifecycle state, execution fence, or output is invalid.\n */\n completeWorkflow<Output extends JsonValue>(\n input: CompleteWorkflowTaskWorkInput<Output>,\n ): Promise<Task>;\n /**\n * Marks a running exact Task attempt as intentionally inactive while awaiting messages.\n *\n * @remarks\n * This keeps the Task in `running`; it does not stall, cancel, or create a new attempt. The\n * waiting marker and optional workflow phase are durable when the promise resolves. Repeating\n * the same target state is a no-op. A stale attempt or non-running Task is refused, so callers\n * should reread the Task before deciding whether a later attempt still needs the message.\n *\n * @param input - Exact running attempt and optional workflow phase.\n * @returns The updated running Task with `waitingForMessages` set.\n * @throws When the Task is absent, no longer running, or on a different attempt.\n */\n waitForMessages(input: WaitForTaskMessagesInput): Promise<Task>;\n /**\n * Resumes an exact message-waiting Task attempt and records fresh progress.\n *\n * @remarks\n * The Task remains `running`, its waiting marker is cleared, and `lastProgressAt` advances. The\n * write is durable when the promise resolves and repeating it for an already-resumed attempt is\n * a no-op. Resuming does not claim or acknowledge messages; the workflow owns that ordering.\n *\n * @param input - Exact running attempt to resume.\n * @returns The updated running Task with message waiting cleared.\n * @throws When the Task is absent, no longer running, or on a different attempt.\n */\n resume(input: ResumeTaskWorkInput): Promise<Task>;\n /**\n * Repairs admitted creation snapshots by direct graph IDs, never by scanning the bucket.\n *\n * @remarks\n * The canonical graph contains immutable admission snapshots. Recovery validates every\n * admission against the exact registered workflow, restores only missing Task records, and\n * returns existing records with their current mutable progress. It is safe to repeat after an\n * interrupted Task creation.\n *\n * This operation repairs Factory persistence only. It does not start or reattach a provider\n * execution, rerun workflow code, retry a Task, or migrate old Task JSON. After recovery, the\n * caller must inspect each Task's lifecycle and recorded execution and apply workflow-owned\n * reconciliation policy.\n *\n * @param rootTaskId - Root of the canonical Task graph to repair and read.\n * @returns Every validated Task admitted to that graph.\n * @throws When the graph is absent, a stored workflow version is not registered, persisted data\n * is invalid, or a current Task no longer matches its immutable admission identity.\n */\n recover(rootTaskId: TaskId): Promise<readonly Task[]>;\n /**\n * Aggregates Task-local measured usage once without charging parent roll-ups again.\n *\n * @remarks\n * This first performs the same persistence repair and validation as {@link recover}. For each\n * Task, recorded step usage is authoritative; otherwise a settled amount is used, then zero.\n * The result is a current snapshot, not a reservation or billing transaction, and calling it\n * does not write settlement receipts.\n *\n * @param rootTaskId - Root of the canonical Task graph to aggregate.\n * @returns Cost, token, and Task counts for the recovered graph.\n * @throws The same persistence and validation errors as {@link recover}.\n */\n usage(rootTaskId: TaskId): Promise<TaskWorkUsage>;\n}\n\n/** Exact Task attempt and candidate workflow output used to complete work. */\nexport interface CompleteTaskWorkInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Candidate value validated against the Task's exact registered output schema. */\n output: unknown;\n /**\n * Optional source-state and execution fence for provider callbacks. Omit only when trusted\n * workflow code already owns lifecycle authority.\n */\n fence?: TaskWorkCompletionFence;\n}\n\n/** Exact Task attempt, workflow contract, and statically checked output used to complete work. */\nexport interface CompleteWorkflowTaskWorkInput<Output extends JsonValue = JsonValue> {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Exact binding required to match the Task's persisted workflow. */\n workflow: WorkflowBinding<JsonValue, Output>;\n /** Candidate output statically carried by the binding and validated again at runtime. */\n output: NoInfer<Output>;\n /**\n * Optional source-state and execution fence for provider callbacks. Omit only when trusted\n * workflow code already owns lifecycle authority.\n */\n fence?: TaskWorkCompletionFence;\n}\n\n/** Atomic source-state and provider-execution conditions for a workflow completion write. */\nexport interface TaskWorkCompletionFence {\n /** Lifecycle state required atomically with completion, normally `running`. */\n readonly from: TaskState;\n /** Provider execution reference required to remain current at the completion write. */\n readonly execution: ExecutionReference;\n}\n\n/** Aggregated measured usage across one canonical Task graph. */\nexport interface TaskWorkUsage {\n /** Sum of measured Task usage, falling back to settlement only when no usage is recorded. */\n costUsd: number;\n /** Sum of recorded provider input tokens across Tasks. */\n inputTokens: number;\n /** Sum of recorded provider output tokens across Tasks. */\n outputTokens: number;\n /** Number of Tasks admitted to the recovered canonical graph. */\n taskCount: number;\n}\n\n/** Exact running Task attempt and optional workflow phase used to await messages. */\nexport interface WaitForTaskMessagesInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n /** Optional durable workflow-owned phase label retained across waiting and resumption. */\n phase?: string;\n}\n\n/** Exact running Task attempt to resume after message delivery. */\nexport interface ResumeTaskWorkInput {\n /** Task ID and current positive attempt; stale attempts are refused. */\n task: TaskAttemptRef;\n}\n\n/** Canonical Task graph, receipt, and Task capabilities used by workflow-facing operations. */\nexport type TaskWorkStores = Pick<FactoryStores, \"graphs\" | \"receipts\" | \"tasks\">;\n\n/** Engine-owned dependencies used to construct workflow-facing Task operations. */\nexport interface CreateTaskWorkStoreOptions {\n /** Policy-free persistence driver used for direct canonical Task reads. */\n driver: StoreDriver;\n /** ISO timestamp source shared with the surrounding store engine. */\n now: IsoDateTimeClock;\n /** Lazily resolves graph, receipt, and Task operations after engine construction. */\n stores: () => TaskWorkStores;\n /** Exact workflow versions used to validate stored input and completion output. */\n workflows: WorkflowRegistry;\n /** Persists a validated Task with compare-and-swap semantics. */\n update(task: Task, version: number): Promise<void>;\n /** Recreates a missing canonical Task snapshot from its immutable graph admission. */\n restore(task: Task): Promise<Task>;\n}\n\n/** Creates workflow-facing Task operations backed by canonical graph and Task stores. */\nexport function createTaskWorkStore(options: CreateTaskWorkStoreOptions): TaskWorkStore {\n const { driver, now, stores, workflows, update, restore } = options;\n function parseTask(value: unknown): Task {\n const task = taskSchema.parse(value);\n workflows.validateStoredInput(task.work.workflow, task.work.input);\n if (task.workResult) workflows.validateStoredOutput(task.work.workflow, task.workResult.output);\n return task;\n }\n async function read(id: TaskId) {\n const record = await driver.get(\"tasks\", id);\n if (!record) throw new RecordNotFoundError(\"tasks\", id);\n const task = parseTask(record.value);\n if (task.id !== id) throw new Error(`Task ${id} does not match its storage key`);\n return { task, version: record.version, work: task.work };\n }\n async function recover(rootTaskId: TaskId): Promise<readonly Task[]> {\n const graph = await stores().graphs.get(rootTaskId);\n if (!graph) throw new RecordNotFoundError(\"taskGraphs\", rootTaskId);\n const tasks: Task[] = [];\n for (const node of graph.nodes) {\n const admission = parseTask(node.admission);\n const existing = await stores().tasks.get(node.taskId);\n const task = parseTask(existing ?? (await restore(admission)));\n assertTaskAdmission({ task, admission });\n tasks.push(task);\n }\n return tasks;\n }\n function assertAttempt(task: Task, expectAttempt: number) {\n z.number().int().positive().parse(expectAttempt);\n if (task.attempt !== expectAttempt)\n throw new Error(`Task ${task.id} is on attempt ${task.attempt}, not ${expectAttempt}`);\n }\n async function setWaiting(\n id: TaskId,\n expectAttempt: number,\n waiting: boolean,\n phase?: string,\n ): Promise<Task> {\n for (;;) {\n const { task, version } = await read(id);\n assertAttempt(task, expectAttempt);\n if (task.state !== \"running\")\n throw new Error(`Task ${id} must be running to ${waiting ? \"wait\" : \"resume\"}`);\n const next = taskSchema.parse({\n ...task,\n waitingForMessages: waiting,\n workflowPhase: phase ?? task.workflowPhase,\n lastProgressAt: waiting ? task.lastProgressAt : now(),\n updatedAt: now(),\n });\n if (task.waitingForMessages === waiting && next.workflowPhase === task.workflowPhase)\n return task;\n await stores().receipts.append({\n taskId: id,\n repositoryIds: task.repositoryIds,\n kind: task.kind,\n state: waiting ? \"waiting\" : \"resumed\",\n phase: \"workflow\",\n attempt: task.attempt,\n reason: waiting\n ? \"Workflow is waiting for Task messages\"\n : \"Workflow resumed message processing\",\n dedupeKey: `work:${id}:${version}:${waiting ? \"wait\" : \"resume\"}`,\n });\n try {\n await update(next, version);\n return next;\n } catch (error) {\n if (!(error instanceof VersionConflictError)) throw error;\n }\n }\n }\n async function complete(\n input: CompleteTaskWorkInput,\n expectedWorkflow?: WorkflowBinding,\n ): Promise<Task> {\n const {\n task: { taskId: id, attempt: expectAttempt },\n output: value,\n fence,\n } = input;\n for (;;) {\n const { task, work } = await read(id);\n assertAttempt(task, expectAttempt);\n if (fence !== undefined && !isDeepStrictEqual(task.execution, fence.execution)) {\n throw new Error(`Task ${id} execution has been replaced`);\n }\n if (expectedWorkflow !== undefined && !isDeepStrictEqual(work.workflow, expectedWorkflow)) {\n throw new Error(\n `Task ${id} uses workflow ${work.workflow.id}@${work.workflow.version}, not ${expectedWorkflow.id}@${expectedWorkflow.version}`,\n );\n }\n if (task.state === \"succeeded\") {\n const output = workflows.validateOutput(work.workflow, value);\n if (!isDeepStrictEqual(task.workResult?.output, output)) {\n throw new Error(`Task ${id} already recorded a different workflow output`);\n }\n return task;\n }\n try {\n return await stores().tasks.transition(id, \"succeeded\", {\n output: value,\n expectAttempt,\n expectFrom: fence?.from ?? task.state,\n ...(fence === undefined ? {} : { expectExecution: fence.execution }),\n phase: \"workflow\",\n });\n } catch (error) {\n if (error instanceof VersionConflictError) continue;\n if (error instanceof InvalidTransitionError && (await read(id)).task.state === \"succeeded\")\n continue;\n throw error;\n }\n }\n }\n return {\n complete,\n completeWorkflow: ({ task, workflow, output, fence }) =>\n complete({ task, output, fence }, workflow),\n waitForMessages: ({ task: { taskId, attempt }, phase }) =>\n setWaiting(taskId, attempt, true, phase),\n resume: ({ task: { taskId, attempt } }) => setWaiting(taskId, attempt, false),\n recover,\n async usage(rootTaskId) {\n const tasks = await recover(rootTaskId);\n let micros = 0;\n let inputTokens = 0;\n let outputTokens = 0;\n for (const task of tasks) {\n micros += Math.round((task.usage?.costUsd ?? task.settledUsd ?? 0) * 1_000_000);\n inputTokens += task.usage?.inputTokens ?? 0;\n outputTokens += task.usage?.outputTokens ?? 0;\n }\n return { costUsd: micros / 1_000_000, inputTokens, outputTokens, taskCount: tasks.length };\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/sweep.d.mts
CHANGED
|
@@ -10,6 +10,10 @@ type SweepQueuedStores = Pick<FactoryStores, "graphs" | "receipts" | "repositori
|
|
|
10
10
|
/**
|
|
11
11
|
* Single-repository adapter for an exact agent route. Supplied by the app, which knows its
|
|
12
12
|
* agents' addresses. Returns the provider's execution reference when it has one.
|
|
13
|
+
*
|
|
14
|
+
* @param task - Running Task already admitted by the dispatch gate.
|
|
15
|
+
* @param repository - Exact enabled repository selected from the Task scope.
|
|
16
|
+
* @returns Provider execution reference that dispatch can persist for recovery.
|
|
13
17
|
*/
|
|
14
18
|
type TaskLauncher = (task: Task, repository: Repository) => Promise<ExecutionReference>;
|
|
15
19
|
/** Exact-route launchers, budget gate, and pass bounds for queued Task dispatch. */
|
|
@@ -25,6 +29,7 @@ interface SweepQueuedOptions {
|
|
|
25
29
|
}
|
|
26
30
|
/** Tasks dispatched, deferred by a gate, or failed by one queued-work pass. */
|
|
27
31
|
interface SweepQueuedResult {
|
|
32
|
+
/** Task IDs whose provider execution reference was durably recorded. */
|
|
28
33
|
readonly dispatched: readonly TaskId[];
|
|
29
34
|
/** Tasks left in the queue because a declared gate may become eligible later. */
|
|
30
35
|
readonly skipped: readonly {
|
|
@@ -44,12 +49,29 @@ type SweepSkipReasonCode = DispatchRefusalCode | "workflow_owned" | "awaiting_ap
|
|
|
44
49
|
/** Stable reason a queued Task could not be routed or launched. */
|
|
45
50
|
type SweepFailureReasonCode = "invalid_repository_scope" | "repository_not_found" | "launcher_not_registered" | "launch_failed";
|
|
46
51
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
52
|
+
* Runs one bounded pass over queued Tasks and dispatches each eligible exact route.
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* Candidates are ordered by newest attempt first and then creation time. Route-less work remains
|
|
56
|
+
* queued for its workflow owner; unmet dependencies, approvals, and pass limits appear in
|
|
57
|
+
* `skipped`. Invalid repository scope, missing repositories or launchers, and launch failures
|
|
58
|
+
* appear in `failed`, with unrouteable Tasks transitioned to `failed` when this sweep wins the
|
|
59
|
+
* race. A result entry describes this pass rather than guaranteeing the Task's lifecycle state:
|
|
60
|
+
* an ambiguously accepted launch is reported as `launch_failed` while its Task intentionally stays
|
|
61
|
+
* running for recovery. Callers should surface every failure, reread those Tasks before deciding
|
|
62
|
+
* remediation, and schedule another pass for skipped work.
|
|
63
|
+
*
|
|
64
|
+
* Overlapping passes are safe: {@link dispatchTask} atomically admits one winner before invoking a
|
|
65
|
+
* launcher. A supplied `tasks` snapshot bounds reads and candidate selection for this pass; it can
|
|
66
|
+
* become stale, and corresponding races are returned as `task_changed` rather than hidden.
|
|
67
|
+
* `maxDispatches` counts provider launches started during the pass, including launches that later
|
|
68
|
+
* fail, so `dispatched.length` may be smaller than the limit.
|
|
69
|
+
*
|
|
70
|
+
* @param stores - Graph, receipt, repository, and Task stores used during the pass.
|
|
71
|
+
* @param options - Exact-route launchers plus optional budget, Task snapshot, and dispatch limit.
|
|
72
|
+
* @returns IDs dispatched and structured skipped/failed outcomes for every considered candidate.
|
|
73
|
+
* @throws When `maxDispatches` is not a positive integer or an unexpected persistence error occurs.
|
|
74
|
+
* @see The shipped `docs/recipes/retry-recovery.md` recipe for scheduled recovery.
|
|
53
75
|
*/
|
|
54
76
|
declare function sweepQueued(stores: SweepQueuedStores, options: SweepQueuedOptions): Promise<SweepQueuedResult>;
|
|
55
77
|
//#endregion
|