octaflow 0.24.1 → 0.26.0

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 CHANGED
@@ -230,6 +230,9 @@ what the layers you import need:
230
230
  # Postgres store / gate / event sink
231
231
  pnpm add pg
232
232
 
233
+ # the same tables as Drizzle column sets, for a host that lets drizzle-kit own its migrations
234
+ pnpm add drizzle-orm
235
+
233
236
  # pg-boss dispatcher, workers, cron scheduler
234
237
  pnpm add pg-boss
235
238
 
@@ -240,6 +243,67 @@ pnpm add ai @ai-sdk/provider
240
243
  > Pure in-memory usage (great for tests and single-process apps) needs **nothing** beyond
241
244
  > `zod` — the engine, `defineStep`/`buildWorkflow`, and the in-memory store are all in the core.
242
245
 
246
+ ### The Postgres tables
247
+
248
+ `octaflow/postgres` owns five tables and ships their DDL: `flowStoreDdl()` (`flow_workflow`,
249
+ `flow_workflow_step`), `flowEventDdl()` (`flow_step_event`) and `flowGateDdl()`
250
+ (`flow_rate_bucket`, `flow_step_lease`). Each is idempotent `CREATE TABLE IF NOT EXISTS`, takes
251
+ a `schema`, and is meant to be pasted into your own migration system; `applySchema(pool, ddl)`
252
+ is the dev and test shortcut.
253
+
254
+ #### The Drizzle columns
255
+
256
+ The rule for every table-owning capability on the platform: `./postgres` ships the store over
257
+ the structural executor plus its `xDdl()`, and `./drizzle` ships the same table as a spreadable
258
+ column set, so a host on Drizzle declares it in its own schema and drizzle-kit owns the
259
+ migration. Where the store's queries are one SQL statement by nature — every one of octaflow's
260
+ is — `./drizzle` ships the columns only, and the store stays the SQL one. So
261
+ `octaflow/drizzle` exports `flowWorkflowColumns`, `flowWorkflowStepColumns`,
262
+ `flowStepEventColumns`, `flowRateBucketColumns` and `flowStepLeaseColumns` (`drizzle-orm` an
263
+ optional peer) and nothing that runs a query. The foreign keys, the composite keys, the unique
264
+ constraint and the indexes are yours to declare on the `pgTable`; each column set's doc lists
265
+ the ones its DDL creates, and two of them the store relies on:
266
+
267
+ ```ts
268
+ import { sql } from 'drizzle-orm';
269
+ import { foreignKey, index, pgTable, primaryKey, unique, uniqueIndex } from 'drizzle-orm/pg-core';
270
+ import { flowWorkflowColumns, flowWorkflowStepColumns, flowStepLeaseColumns } from 'octaflow/drizzle';
271
+
272
+ export const flowWorkflow = pgTable('flow_workflow', flowWorkflowColumns, (t) => [
273
+ index('flow_workflow_partition_status_idx').on(t.partitionKey, t.status),
274
+ index('flow_workflow_partition_type_idx').on(t.partitionKey, t.type),
275
+ index('flow_workflow_partition_entity_idx').on(t.partitionKey, t.entityRef),
276
+ index('flow_workflow_parent_idx').on(t.parentWorkflowId),
277
+ // The deadline sweep reads only live runs that have one.
278
+ index('flow_workflow_deadline_idx').on(t.deadlineAt).where(sql`deadline_at IS NOT NULL`),
279
+ // `start` with an idempotency key upserts against this index: without it a repeated start is a second run.
280
+ uniqueIndex('flow_workflow_idempotency_idx').on(t.partitionKey, t.idempotencyKey).where(sql`idempotency_key IS NOT NULL`),
281
+ ]);
282
+
283
+ export const flowWorkflowStep = pgTable('flow_workflow_step', flowWorkflowStepColumns, (t) => [
284
+ foreignKey({ columns: [t.workflowId], foreignColumns: [flowWorkflow.id] }).onDelete('cascade'),
285
+ foreignKey({ columns: [t.parentStepId], foreignColumns: [t.id] }).onDelete('cascade'),
286
+ unique('flow_workflow_step_workflow_id_key_unique').on(t.workflowId, t.key),
287
+ index('flow_workflow_step_workflow_idx').on(t.workflowId),
288
+ index('flow_workflow_step_status_idx').on(t.workflowId, t.status),
289
+ index('flow_workflow_step_parent_idx').on(t.parentStepId),
290
+ ]);
291
+
292
+ export const flowStepLease = pgTable('flow_step_lease', flowStepLeaseColumns, (t) => [
293
+ primaryKey({ columns: [t.partitionKey, t.stepType, t.stepId] }),
294
+ index('flow_step_lease_active_idx').on(t.partitionKey, t.stepType, t.expiresAt),
295
+ ]);
296
+ ```
297
+
298
+ The names are yours: the store's `ON CONFLICT` clauses target column lists, never a
299
+ constraint by name, so what must match is each index's columns and its partial `WHERE`,
300
+ not what you call it.
301
+
302
+ The store stays plain SQL, bound to the `schema` you hand `createPgWorkflowStore`,
303
+ `createPgStepGate` and `createPgEventSink` — and, for the gate, to the `tables` names — so
304
+ declare the tables under the same schema and names. A test in the package holds every column
305
+ set and its DDL equal, column for column: a column added to one side alone fails the build.
306
+
243
307
  ---
244
308
 
245
309
  ---
@@ -66,7 +66,7 @@ export interface LazyEmbeddingModelMeta {
66
66
  * next call rather than cached.
67
67
  */
68
68
  export declare function createLazyEmbeddingModel(meta: LazyEmbeddingModelMeta, load: () => Promise<EmbeddingModelV4>): EmbeddingModelV4;
69
- export type AiProviderConfigErrorKey = 'ai_residency_undeclared' | 'ai_residency_invalid' | 'ai_base_url_invalid' | 'ai_model_required' | 'ai_embedding_dimensions_invalid';
69
+ export type AiProviderConfigErrorKey = 'ai_residency_undeclared' | 'ai_residency_invalid' | 'ai_base_url_invalid' | 'ai_model_required' | 'ai_model_undeclared' | 'ai_embedding_dimensions_invalid';
70
70
  /**
71
71
  * A provider configuration that cannot work. Thrown before any SDK is loaded
72
72
  * or any request is made: it is a deployment mistake, found at wiring time.
@@ -143,4 +143,66 @@ export interface OpenAiCompatibleEmbeddingProvider {
143
143
  export declare function createOpenAiCompatibleModelProvider(config: OpenAiCompatibleModelProviderConfig): Promise<OpenAiCompatibleModelProvider>;
144
144
  /** An embedding model on an OpenAI-compatible endpoint, with its declared width. */
145
145
  export declare function createOpenAiCompatibleEmbeddingProvider(config: OpenAiCompatibleEmbeddingProviderConfig): Promise<OpenAiCompatibleEmbeddingProvider>;
146
+ export interface OpenAiCompatibleGatewayModel {
147
+ /** Where the gateway sends this alias. Validated like an endpoint's residency. */
148
+ residency: AiEndpointResidency;
149
+ }
150
+ /**
151
+ * `residency` is where the gateway itself runs, usually `{ region: 'self-hosted' }`.
152
+ * It decides whether plain http is acceptable for the hop to the gateway; where a
153
+ * call ends up is the alias's residency in `models`.
154
+ */
155
+ export interface OpenAiCompatibleGatewayConfig extends OpenAiCompatibleEndpointConfig {
156
+ /** Every alias this deployment may call, keyed by the model name the gateway knows. */
157
+ models: Record<string, OpenAiCompatibleGatewayModel>;
158
+ /** As for {@link OpenAiCompatibleModelProviderConfig.structuredOutputs}. */
159
+ structuredOutputs?: boolean;
160
+ /** As for {@link OpenAiCompatibleModelProviderConfig.includeUsage}. */
161
+ includeUsage?: boolean;
162
+ }
163
+ export interface GatewayCallOptions {
164
+ /**
165
+ * Sent as the request body's `metadata`, which LiteLLM keeps in its spend log,
166
+ * so a call there can be matched to the host's own record: a workflow id, a
167
+ * scope key. It leaves the house as written and is never redacted: put ids
168
+ * here, never content. Added innermost, so a disclosure gate wrapped around the
169
+ * model neither digests nor redacts it.
170
+ */
171
+ metadata?: Record<string, string>;
172
+ }
173
+ export interface OpenAiCompatibleGatewayProvider {
174
+ providerId: OpenAiCompatibleProviderId;
175
+ /** Where the gateway itself runs. */
176
+ residency: AiEndpointResidency;
177
+ /** Every declared alias with its validated residency: what a settings page or a subprocessor annex lists. */
178
+ models: Readonly<Record<string, AiEndpointResidency>>;
179
+ /** The residency of one alias. Throws `ai_model_undeclared` for an alias the declaration does not name. */
180
+ residencyOf(alias: string): AiEndpointResidency;
181
+ /** A Chat Completions model for a declared alias, wrapped for refusal detection. */
182
+ languageModel(alias: string, options?: GatewayCallOptions): LanguageModelV4;
183
+ }
184
+ /**
185
+ * A model gateway — LiteLLM, or any router with the OpenAI-compatible API — as
186
+ * refusal-aware models, one per declared alias, each with the residency of
187
+ * where the gateway sends it.
188
+ *
189
+ * ```ts
190
+ * const gateway = await createOpenAiCompatibleGatewayProvider({
191
+ * baseUrl: 'http://litellm.ai.svc:4000/v1',
192
+ * apiKey: process.env.LITELLM_KEY,
193
+ * residency: { region: 'self-hosted' },
194
+ * models: {
195
+ * 'approval-confidential': { residency: { region: 'self-hosted' } },
196
+ * 'approval-public': { residency: { region: 'eu', legalName: 'Example Inference GmbH', country: 'DE' } },
197
+ * },
198
+ * });
199
+ * const model = gateway.languageModel('approval-confidential', { metadata: { workflowId } });
200
+ * ```
201
+ *
202
+ * The declaration is what the gateway is configured to do, stated by the
203
+ * deployment: this provider cannot see the gateway's routing, and a fallback
204
+ * the gateway adds on its own breaks the declaration silently. Keep the two
205
+ * in the same review.
206
+ */
207
+ export declare function createOpenAiCompatibleGatewayProvider(config: OpenAiCompatibleGatewayConfig): Promise<OpenAiCompatibleGatewayProvider>;
146
208
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { r as createRefusalAwareModel } from "../../refusal-BsL2HPDb.js";
2
2
  import { a as EMBEDDING_PROVIDERS, c as OPENAI_COMPATIBLE_PROVIDER_ID, d as resolveEndpointResidency, t as AI_PROVIDERS } from "../../provider-registry-BRwjC6_3.js";
3
+ import { wrapLanguageModel } from "ai";
3
4
  //#region src/ai/providers/index.ts
4
5
  const LLM_LOADERS = {
5
6
  anthropic: async (apiKey, baseURL) => {
@@ -186,5 +187,90 @@ async function createOpenAiCompatibleEmbeddingProvider(config) {
186
187
  embeddingModel: () => provider.embeddingModel(modelId)
187
188
  };
188
189
  }
190
+ /** Puts `metadata` into the request body under the provider's own options key, merged over any the call carries. */
191
+ function withGatewayMetadata(model, optionsKey, metadata) {
192
+ const fixed = { ...metadata };
193
+ return wrapLanguageModel({
194
+ model,
195
+ middleware: {
196
+ specificationVersion: "v4",
197
+ transformParams: async ({ params }) => {
198
+ const own = params.providerOptions?.[optionsKey] ?? {};
199
+ const existing = own.metadata !== null && typeof own.metadata === "object" && !Array.isArray(own.metadata) ? own.metadata : {};
200
+ const merged = {
201
+ ...own,
202
+ metadata: {
203
+ ...existing,
204
+ ...fixed
205
+ }
206
+ };
207
+ return {
208
+ ...params,
209
+ providerOptions: {
210
+ ...params.providerOptions,
211
+ [optionsKey]: merged
212
+ }
213
+ };
214
+ }
215
+ }
216
+ });
217
+ }
218
+ /**
219
+ * A model gateway — LiteLLM, or any router with the OpenAI-compatible API — as
220
+ * refusal-aware models, one per declared alias, each with the residency of
221
+ * where the gateway sends it.
222
+ *
223
+ * ```ts
224
+ * const gateway = await createOpenAiCompatibleGatewayProvider({
225
+ * baseUrl: 'http://litellm.ai.svc:4000/v1',
226
+ * apiKey: process.env.LITELLM_KEY,
227
+ * residency: { region: 'self-hosted' },
228
+ * models: {
229
+ * 'approval-confidential': { residency: { region: 'self-hosted' } },
230
+ * 'approval-public': { residency: { region: 'eu', legalName: 'Example Inference GmbH', country: 'DE' } },
231
+ * },
232
+ * });
233
+ * const model = gateway.languageModel('approval-confidential', { metadata: { workflowId } });
234
+ * ```
235
+ *
236
+ * The declaration is what the gateway is configured to do, stated by the
237
+ * deployment: this provider cannot see the gateway's routing, and a fallback
238
+ * the gateway adds on its own breaks the declaration silently. Keep the two
239
+ * in the same review.
240
+ */
241
+ async function createOpenAiCompatibleGatewayProvider(config) {
242
+ const residency = checkEndpoint(config);
243
+ const declared = config.models;
244
+ if (declared === null || typeof declared !== "object" || Object.keys(declared).length === 0) throw new AiProviderConfigError("ai_model_required", "a gateway needs at least one declared model alias in models");
245
+ const models = {};
246
+ for (const [alias, model] of Object.entries(declared)) {
247
+ requireModel(alias, "a model alias");
248
+ const resolved = resolveEndpointResidency(model?.residency);
249
+ if (!resolved.ok) throw new AiProviderConfigError(resolved.error.key, `model ${JSON.stringify(alias)}: ${resolved.error.message}`);
250
+ models[alias] = resolved.value;
251
+ }
252
+ Object.freeze(models);
253
+ const provider = await loadOpenAiCompatible(config, {
254
+ structuredOutputs: config.structuredOutputs,
255
+ includeUsage: config.includeUsage
256
+ });
257
+ const optionsKey = config.name ?? "openai-compatible";
258
+ const residencyOf = (alias) => {
259
+ const found = Object.hasOwn(models, alias) ? models[alias] : void 0;
260
+ if (found === void 0) throw new AiProviderConfigError("ai_model_undeclared", `model ${JSON.stringify(alias)} is not declared for this gateway; declared: ${Object.keys(models).join(", ")}`);
261
+ return found;
262
+ };
263
+ return {
264
+ providerId: OPENAI_COMPATIBLE_PROVIDER_ID,
265
+ residency,
266
+ models,
267
+ residencyOf,
268
+ languageModel: (alias, options) => {
269
+ residencyOf(alias);
270
+ const chat = provider.chatModel(alias);
271
+ return createRefusalAwareModel(options?.metadata ? withGatewayMetadata(chat, optionsKey, options.metadata) : chat);
272
+ }
273
+ };
274
+ }
189
275
  //#endregion
190
- export { AiProviderConfigError, createEmbeddingModelFactory, createEmbeddingModelProvider, createLanguageModelFactory, createLanguageModelProvider, createLazyEmbeddingModel, createOpenAiCompatibleEmbeddingProvider, createOpenAiCompatibleModelProvider };
276
+ export { AiProviderConfigError, createEmbeddingModelFactory, createEmbeddingModelProvider, createLanguageModelFactory, createLanguageModelProvider, createLazyEmbeddingModel, createOpenAiCompatibleEmbeddingProvider, createOpenAiCompatibleGatewayProvider, createOpenAiCompatibleModelProvider };
@@ -0,0 +1,173 @@
1
+ //#region src/drizzle/index.d.ts
2
+ /**
3
+ * `flow_workflow` — one row per run. `partition_key` is the engine's scoping
4
+ * column: the store is bound to one partition and every query filters by it.
5
+ *
6
+ * The DDL adds, and the host declares:
7
+ *
8
+ * ```ts
9
+ * import { index, pgTable, uniqueIndex } from 'drizzle-orm/pg-core';
10
+ * import { sql } from 'drizzle-orm';
11
+ * import { flowWorkflowColumns } from 'octaflow/drizzle';
12
+ *
13
+ * export const flowWorkflow = pgTable('flow_workflow', flowWorkflowColumns, (t) => [
14
+ * index('flow_workflow_partition_status_idx').on(t.partitionKey, t.status),
15
+ * index('flow_workflow_parent_idx').on(t.parentWorkflowId),
16
+ * index('flow_workflow_partition_type_idx').on(t.partitionKey, t.type),
17
+ * index('flow_workflow_partition_entity_idx').on(t.partitionKey, t.entityRef),
18
+ * // Load-bearing: the deadline sweep reads only live runs that have one.
19
+ * index('flow_workflow_deadline_idx').on(t.deadlineAt).where(sql`deadline_at IS NOT NULL`),
20
+ * // Load-bearing: `start` with an idempotency key relies on this unique index
21
+ * // to make a repeated start return the existing run instead of a second one.
22
+ * uniqueIndex('flow_workflow_idempotency_idx').on(t.partitionKey, t.idempotencyKey).where(sql`idempotency_key IS NOT NULL`),
23
+ * ]);
24
+ * ```
25
+ *
26
+ * `parent_workflow_id` and `parent_step_id` carry no foreign key in the DDL
27
+ * (a parent may outlive or predate its child in a migration); add one if you
28
+ * want the constraint.
29
+ */
30
+ export declare const flowWorkflowColumns: {
31
+ id: import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigSerial53BuilderInitial<"id">>>;
32
+ partitionKey: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"partition_key", [string, ...string[]]>>;
33
+ /** The workflow definition's name. */
34
+ type: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"type", [string, ...string[]]>>;
35
+ status: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"status", [string, ...string[]]>>;
36
+ input: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"input">>>;
37
+ output: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"output">;
38
+ error: import("drizzle-orm/pg-core").PgTextBuilderInitial<"error", [string, ...string[]]>;
39
+ /** The host's handle for what the run is about (`'record:88'`), for lookups by entity. */
40
+ entityRef: import("drizzle-orm/pg-core").PgTextBuilderInitial<"entity_ref", [string, ...string[]]>;
41
+ totalSteps: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"total_steps">>;
42
+ completedSteps: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"completed_steps">>>;
43
+ failedSteps: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"failed_steps">>>;
44
+ metadata: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"metadata">;
45
+ /** Set on a `start` that asked for one; unique per partition where present. */
46
+ idempotencyKey: import("drizzle-orm/pg-core").PgTextBuilderInitial<"idempotency_key", [string, ...string[]]>;
47
+ /** The run's deadline, when the definition set one; the sweep reads it. */
48
+ deadlineAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
49
+ /** For a sub-workflow: the run that started it, and the step in that run. */
50
+ parentWorkflowId: import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"parent_workflow_id">;
51
+ parentStepId: import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"parent_step_id">;
52
+ createdAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>>;
53
+ startedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
54
+ completedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
55
+ };
56
+ /**
57
+ * `flow_workflow_step` — one row per step of a run, including the children a
58
+ * fan-out creates at runtime.
59
+ *
60
+ * The DDL adds, and the host declares:
61
+ *
62
+ * ```ts
63
+ * import { foreignKey, index, pgTable, unique, type AnyPgColumn } from 'drizzle-orm/pg-core';
64
+ * import { flowWorkflowStepColumns } from 'octaflow/drizzle';
65
+ *
66
+ * export const flowWorkflowStep = pgTable('flow_workflow_step', flowWorkflowStepColumns, (t) => [
67
+ * foreignKey({ columns: [t.workflowId], foreignColumns: [flowWorkflow.id] }).onDelete('cascade'),
68
+ * foreignKey({ columns: [t.parentStepId], foreignColumns: [t.id] }).onDelete('cascade'),
69
+ * // `(workflow_id, key)` is the step's identity within a run; the constraint
70
+ * // is what refuses a second step under the same key.
71
+ * unique('flow_workflow_step_workflow_id_key_unique').on(t.workflowId, t.key),
72
+ * index('flow_workflow_step_workflow_idx').on(t.workflowId),
73
+ * index('flow_workflow_step_status_idx').on(t.workflowId, t.status),
74
+ * index('flow_workflow_step_parent_idx').on(t.parentStepId),
75
+ * ]);
76
+ * ```
77
+ */
78
+ export declare const flowWorkflowStepColumns: {
79
+ id: import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigSerial53BuilderInitial<"id">>>;
80
+ workflowId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"workflow_id">>;
81
+ partitionKey: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"partition_key", [string, ...string[]]>>;
82
+ /** The step's key within its workflow — unique per run. */
83
+ key: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"key", [string, ...string[]]>>;
84
+ /** The step handler's name. */
85
+ type: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"type", [string, ...string[]]>>;
86
+ status: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"status", [string, ...string[]]>>;
87
+ /** The keys of the steps this one waits for. */
88
+ dependencies: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"dependencies">>>;
89
+ input: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"input">;
90
+ output: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"output">;
91
+ error: import("drizzle-orm/pg-core").PgTextBuilderInitial<"error", [string, ...string[]]>;
92
+ metadata: import("drizzle-orm/pg-core").PgJsonbBuilderInitial<"metadata">;
93
+ attempts: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"attempts">>>;
94
+ /** For a fan-out child: the step that spawned it. */
95
+ parentStepId: import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"parent_step_id">;
96
+ /** The last heartbeat a long step sent; the sweep uses it to tell stalled from slow. */
97
+ heartbeatAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
98
+ startedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
99
+ completedAt: import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>;
100
+ };
101
+ /**
102
+ * `flow_step_event` — the append-only run history `createPgEventSink` writes,
103
+ * one row per engine event.
104
+ *
105
+ * The DDL adds, and the host declares:
106
+ *
107
+ * ```ts
108
+ * export const flowStepEvent = pgTable('flow_step_event', flowStepEventColumns, (t) => [
109
+ * index('flow_step_event_workflow_idx').on(t.partitionKey, t.workflowId, t.id),
110
+ * ]);
111
+ * ```
112
+ *
113
+ * `workflow_id` carries no foreign key on purpose: the history outlives the run.
114
+ */
115
+ export declare const flowStepEventColumns: {
116
+ id: import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigSerial53BuilderInitial<"id">>>;
117
+ partitionKey: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"partition_key", [string, ...string[]]>>;
118
+ workflowId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"workflow_id">>;
119
+ workflowType: import("drizzle-orm/pg-core").PgTextBuilderInitial<"workflow_type", [string, ...string[]]>;
120
+ /** Null for a workflow-level event. */
121
+ stepId: import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"step_id">;
122
+ stepKey: import("drizzle-orm/pg-core").PgTextBuilderInitial<"step_key", [string, ...string[]]>;
123
+ stepType: import("drizzle-orm/pg-core").PgTextBuilderInitial<"step_type", [string, ...string[]]>;
124
+ eventType: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"event_type", [string, ...string[]]>>;
125
+ attempt: import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"attempt">;
126
+ durationMs: import("drizzle-orm/pg-core").PgIntegerBuilderInitial<"duration_ms">;
127
+ error: import("drizzle-orm/pg-core").PgTextBuilderInitial<"error", [string, ...string[]]>;
128
+ at: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>;
129
+ };
130
+ /**
131
+ * `flow_rate_bucket` — one token bucket per `(partition_key, step_type)` for
132
+ * `createPgStepGate`'s rate limits. The table name is the DDL's default and
133
+ * the gate's `tables.rateBucket` option; the two must agree.
134
+ *
135
+ * The DDL adds a composite primary key, and the host declares it:
136
+ *
137
+ * ```ts
138
+ * export const flowRateBucket = pgTable('flow_rate_bucket', flowRateBucketColumns, (t) => [
139
+ * primaryKey({ columns: [t.partitionKey, t.stepType] }),
140
+ * ]);
141
+ * ```
142
+ */
143
+ export declare const flowRateBucketColumns: {
144
+ partitionKey: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"partition_key", [string, ...string[]]>>;
145
+ stepType: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"step_type", [string, ...string[]]>>;
146
+ /** The bucket's fill, refilled from `updated_at` on every read. */
147
+ tokens: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgDoublePrecisionBuilderInitial<"tokens">>;
148
+ updatedAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>>;
149
+ };
150
+ /**
151
+ * `flow_step_lease` — one row per running step under a concurrency cap, for
152
+ * `createPgStepGate`'s `maxConcurrent`. The table name is the DDL's default
153
+ * and the gate's `tables.lease` option; the two must agree.
154
+ *
155
+ * The DDL adds a composite primary key and one index, and the host declares
156
+ * them — the index name is derived from the table name in the DDL:
157
+ *
158
+ * ```ts
159
+ * export const flowStepLease = pgTable('flow_step_lease', flowStepLeaseColumns, (t) => [
160
+ * primaryKey({ columns: [t.partitionKey, t.stepType, t.stepId] }),
161
+ * index('flow_step_lease_active_idx').on(t.partitionKey, t.stepType, t.expiresAt),
162
+ * ]);
163
+ * ```
164
+ */
165
+ export declare const flowStepLeaseColumns: {
166
+ partitionKey: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"partition_key", [string, ...string[]]>>;
167
+ stepType: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTextBuilderInitial<"step_type", [string, ...string[]]>>;
168
+ stepId: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgBigInt53BuilderInitial<"step_id">>;
169
+ acquiredAt: import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>>;
170
+ /** A lease past this instant is reaped on the next acquire. */
171
+ expiresAt: import("drizzle-orm").NotNull<import("drizzle-orm/pg-core").PgTimestampBuilderInitial<string>>;
172
+ };
173
+ //#endregion
@@ -0,0 +1,199 @@
1
+ import { bigint, bigserial, doublePrecision, integer, jsonb, text, timestamp } from "drizzle-orm/pg-core";
2
+ //#region src/drizzle/index.ts
3
+ /**
4
+ * `octaflow/drizzle` — the engine's tables as Drizzle column sets.
5
+ *
6
+ * One spreadable column set per table the Postgres adapters create: the
7
+ * workflow store's two (`flowStoreDdl`), the event sink's one (`flowEventDdl`)
8
+ * and the gate's two (`flowGateDdl`). A host on Drizzle declares the tables in
9
+ * its own schema from these columns and lets drizzle-kit own the migrations;
10
+ * the stores themselves stay plain SQL over the `schema` (and, for the gate,
11
+ * the table names) the host hands them. Nothing here runs a query: no store
12
+ * is rewritten on the ORM, because every one of the engine's writes is one
13
+ * SQL statement by nature.
14
+ *
15
+ * What the DDL declares beyond the columns is the host's to mirror in its
16
+ * `pgTable(...)` call — the foreign keys, the composite keys, the unique
17
+ * constraint and the indexes. Each set's doc lists them; two of the indexes
18
+ * are load-bearing for the store, not an optimisation, and are marked so.
19
+ * A test holds these sets and the DDL equal column for column.
20
+ *
21
+ * `drizzle-orm` is an optional peer, loaded by this subpath alone.
22
+ */
23
+ const timestamptz = (name) => timestamp(name, {
24
+ withTimezone: true,
25
+ mode: "date"
26
+ });
27
+ /**
28
+ * `flow_workflow` — one row per run. `partition_key` is the engine's scoping
29
+ * column: the store is bound to one partition and every query filters by it.
30
+ *
31
+ * The DDL adds, and the host declares:
32
+ *
33
+ * ```ts
34
+ * import { index, pgTable, uniqueIndex } from 'drizzle-orm/pg-core';
35
+ * import { sql } from 'drizzle-orm';
36
+ * import { flowWorkflowColumns } from 'octaflow/drizzle';
37
+ *
38
+ * export const flowWorkflow = pgTable('flow_workflow', flowWorkflowColumns, (t) => [
39
+ * index('flow_workflow_partition_status_idx').on(t.partitionKey, t.status),
40
+ * index('flow_workflow_parent_idx').on(t.parentWorkflowId),
41
+ * index('flow_workflow_partition_type_idx').on(t.partitionKey, t.type),
42
+ * index('flow_workflow_partition_entity_idx').on(t.partitionKey, t.entityRef),
43
+ * // Load-bearing: the deadline sweep reads only live runs that have one.
44
+ * index('flow_workflow_deadline_idx').on(t.deadlineAt).where(sql`deadline_at IS NOT NULL`),
45
+ * // Load-bearing: `start` with an idempotency key relies on this unique index
46
+ * // to make a repeated start return the existing run instead of a second one.
47
+ * uniqueIndex('flow_workflow_idempotency_idx').on(t.partitionKey, t.idempotencyKey).where(sql`idempotency_key IS NOT NULL`),
48
+ * ]);
49
+ * ```
50
+ *
51
+ * `parent_workflow_id` and `parent_step_id` carry no foreign key in the DDL
52
+ * (a parent may outlive or predate its child in a migration); add one if you
53
+ * want the constraint.
54
+ */
55
+ const flowWorkflowColumns = {
56
+ id: bigserial("id", { mode: "number" }).primaryKey(),
57
+ partitionKey: text("partition_key").notNull(),
58
+ /** The workflow definition's name. */
59
+ type: text("type").notNull(),
60
+ status: text("status").notNull(),
61
+ input: jsonb("input").notNull().default({}),
62
+ output: jsonb("output"),
63
+ error: text("error"),
64
+ /** The host's handle for what the run is about (`'record:88'`), for lookups by entity. */
65
+ entityRef: text("entity_ref"),
66
+ totalSteps: integer("total_steps").notNull(),
67
+ completedSteps: integer("completed_steps").notNull().default(0),
68
+ failedSteps: integer("failed_steps").notNull().default(0),
69
+ metadata: jsonb("metadata"),
70
+ /** Set on a `start` that asked for one; unique per partition where present. */
71
+ idempotencyKey: text("idempotency_key"),
72
+ /** The run's deadline, when the definition set one; the sweep reads it. */
73
+ deadlineAt: timestamptz("deadline_at"),
74
+ /** For a sub-workflow: the run that started it, and the step in that run. */
75
+ parentWorkflowId: bigint("parent_workflow_id", { mode: "number" }),
76
+ parentStepId: bigint("parent_step_id", { mode: "number" }),
77
+ createdAt: timestamptz("created_at").notNull().defaultNow(),
78
+ startedAt: timestamptz("started_at"),
79
+ completedAt: timestamptz("completed_at")
80
+ };
81
+ /**
82
+ * `flow_workflow_step` — one row per step of a run, including the children a
83
+ * fan-out creates at runtime.
84
+ *
85
+ * The DDL adds, and the host declares:
86
+ *
87
+ * ```ts
88
+ * import { foreignKey, index, pgTable, unique, type AnyPgColumn } from 'drizzle-orm/pg-core';
89
+ * import { flowWorkflowStepColumns } from 'octaflow/drizzle';
90
+ *
91
+ * export const flowWorkflowStep = pgTable('flow_workflow_step', flowWorkflowStepColumns, (t) => [
92
+ * foreignKey({ columns: [t.workflowId], foreignColumns: [flowWorkflow.id] }).onDelete('cascade'),
93
+ * foreignKey({ columns: [t.parentStepId], foreignColumns: [t.id] }).onDelete('cascade'),
94
+ * // `(workflow_id, key)` is the step's identity within a run; the constraint
95
+ * // is what refuses a second step under the same key.
96
+ * unique('flow_workflow_step_workflow_id_key_unique').on(t.workflowId, t.key),
97
+ * index('flow_workflow_step_workflow_idx').on(t.workflowId),
98
+ * index('flow_workflow_step_status_idx').on(t.workflowId, t.status),
99
+ * index('flow_workflow_step_parent_idx').on(t.parentStepId),
100
+ * ]);
101
+ * ```
102
+ */
103
+ const flowWorkflowStepColumns = {
104
+ id: bigserial("id", { mode: "number" }).primaryKey(),
105
+ workflowId: bigint("workflow_id", { mode: "number" }).notNull(),
106
+ partitionKey: text("partition_key").notNull(),
107
+ /** The step's key within its workflow — unique per run. */
108
+ key: text("key").notNull(),
109
+ /** The step handler's name. */
110
+ type: text("type").notNull(),
111
+ status: text("status").notNull(),
112
+ /** The keys of the steps this one waits for. */
113
+ dependencies: jsonb("dependencies").notNull().default([]),
114
+ input: jsonb("input"),
115
+ output: jsonb("output"),
116
+ error: text("error"),
117
+ metadata: jsonb("metadata"),
118
+ attempts: integer("attempts").notNull().default(0),
119
+ /** For a fan-out child: the step that spawned it. */
120
+ parentStepId: bigint("parent_step_id", { mode: "number" }),
121
+ /** The last heartbeat a long step sent; the sweep uses it to tell stalled from slow. */
122
+ heartbeatAt: timestamptz("heartbeat_at"),
123
+ startedAt: timestamptz("started_at"),
124
+ completedAt: timestamptz("completed_at")
125
+ };
126
+ /**
127
+ * `flow_step_event` — the append-only run history `createPgEventSink` writes,
128
+ * one row per engine event.
129
+ *
130
+ * The DDL adds, and the host declares:
131
+ *
132
+ * ```ts
133
+ * export const flowStepEvent = pgTable('flow_step_event', flowStepEventColumns, (t) => [
134
+ * index('flow_step_event_workflow_idx').on(t.partitionKey, t.workflowId, t.id),
135
+ * ]);
136
+ * ```
137
+ *
138
+ * `workflow_id` carries no foreign key on purpose: the history outlives the run.
139
+ */
140
+ const flowStepEventColumns = {
141
+ id: bigserial("id", { mode: "number" }).primaryKey(),
142
+ partitionKey: text("partition_key").notNull(),
143
+ workflowId: bigint("workflow_id", { mode: "number" }).notNull(),
144
+ workflowType: text("workflow_type"),
145
+ /** Null for a workflow-level event. */
146
+ stepId: bigint("step_id", { mode: "number" }),
147
+ stepKey: text("step_key"),
148
+ stepType: text("step_type"),
149
+ eventType: text("event_type").notNull(),
150
+ attempt: integer("attempt"),
151
+ durationMs: integer("duration_ms"),
152
+ error: text("error"),
153
+ at: timestamptz("at").notNull()
154
+ };
155
+ /**
156
+ * `flow_rate_bucket` — one token bucket per `(partition_key, step_type)` for
157
+ * `createPgStepGate`'s rate limits. The table name is the DDL's default and
158
+ * the gate's `tables.rateBucket` option; the two must agree.
159
+ *
160
+ * The DDL adds a composite primary key, and the host declares it:
161
+ *
162
+ * ```ts
163
+ * export const flowRateBucket = pgTable('flow_rate_bucket', flowRateBucketColumns, (t) => [
164
+ * primaryKey({ columns: [t.partitionKey, t.stepType] }),
165
+ * ]);
166
+ * ```
167
+ */
168
+ const flowRateBucketColumns = {
169
+ partitionKey: text("partition_key").notNull(),
170
+ stepType: text("step_type").notNull(),
171
+ /** The bucket's fill, refilled from `updated_at` on every read. */
172
+ tokens: doublePrecision("tokens").notNull(),
173
+ updatedAt: timestamptz("updated_at").notNull().defaultNow()
174
+ };
175
+ /**
176
+ * `flow_step_lease` — one row per running step under a concurrency cap, for
177
+ * `createPgStepGate`'s `maxConcurrent`. The table name is the DDL's default
178
+ * and the gate's `tables.lease` option; the two must agree.
179
+ *
180
+ * The DDL adds a composite primary key and one index, and the host declares
181
+ * them — the index name is derived from the table name in the DDL:
182
+ *
183
+ * ```ts
184
+ * export const flowStepLease = pgTable('flow_step_lease', flowStepLeaseColumns, (t) => [
185
+ * primaryKey({ columns: [t.partitionKey, t.stepType, t.stepId] }),
186
+ * index('flow_step_lease_active_idx').on(t.partitionKey, t.stepType, t.expiresAt),
187
+ * ]);
188
+ * ```
189
+ */
190
+ const flowStepLeaseColumns = {
191
+ partitionKey: text("partition_key").notNull(),
192
+ stepType: text("step_type").notNull(),
193
+ stepId: bigint("step_id", { mode: "number" }).notNull(),
194
+ acquiredAt: timestamptz("acquired_at").notNull().defaultNow(),
195
+ /** A lease past this instant is reaped on the next acquire. */
196
+ expiresAt: timestamptz("expires_at").notNull()
197
+ };
198
+ //#endregion
199
+ export { flowRateBucketColumns, flowStepEventColumns, flowStepLeaseColumns, flowWorkflowColumns, flowWorkflowStepColumns };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { $ as computeReadiness, A as StepConditionHandler, B as StepStatus, C as defineWaitStep, D as StartOptions, E as RetryPolicy, F as StepHandlerNotFoundError, G as WorkflowNotFoundError, H as WorkflowCreatedResult, I as StepHandlerRegistry, J as WorkflowStatus, K as WorkflowNotRetryableError, L as StepId, M as StepError, N as StepExecutionContext, O as StepCompensateHandler, P as StepHandler, Q as ReadinessPlan, R as StepRecord, S as defineSubWorkflowStep, T as InvalidWorkflowDefinitionError, U as WorkflowDefinition, V as WaitTimeoutPolicy, W as WorkflowId, X as JoinRule, Y as WorkflowWithSteps, Z as PlannedSkip, _ as WorkflowOutput, a as STEP_DISPLAY_STATUS, b as defineSleepStep, c as WORKFLOW_STATUS_SCHEMA, d as toPublicWorkflow, et as isTerminalStepStatus, f as StepDeps, g as TypedWorkflow, h as TypedStepContext, i as PublicWorkflowStep, it as ok, j as StepDefinition, k as StepCompensationContext, l as toDisplayStepStatus, m as TypedStep, n as PUBLIC_WORKFLOW_STEP_SCHEMA, nt as Result, o as STEP_DISPLAY_STATUS_SCHEMA, p as StepOutput, q as WorkflowRecord, r as PublicWorkflow, rt as err, s as StepDisplayStatus, t as PUBLIC_WORKFLOW_SCHEMA, tt as FlowErrorShape, u as toPublicStep, v as buildWorkflow, w as FlowError, x as defineStep, y as defineMapStep, z as StepRegistration } from "./index-BURAYZui.js";
2
2
  import { n as Logger, r as noopLogger, t as LogAttributes } from "./logger-BCus3AOa.js";
3
- import { A as ListWorkflowsFilters, C as AddChildStep, D as CreatedWorkflow, E as CreateWorkflowStep, M as TransactionalScope, N as WorkflowStore, O as FailStepParams, S as createInMemoryStepGate, T as CreateWorkflowParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ReopenWorkflowParams, k as FinishWorkflowParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as CompleteStepParams, x as StepGateRequest, y as StepGate } from "./observability-CR9dEdLW.js";
3
+ import { A as ListWorkflowsFilters, C as AddChildStep, D as CreatedWorkflow, E as CreateWorkflowStep, M as TransactionalScope, N as WorkflowStore, O as FailStepParams, S as createInMemoryStepGate, T as CreateWorkflowParams, _ as InMemoryStepGateConfig, a as FlowSpan, b as StepGateDecision, c as RecordingObserver, d as createMeterObserver, f as createRecordingObserver, g as ConcurrencyRule, h as noopTracer, i as FlowObserver, j as ReopenWorkflowParams, k as FinishWorkflowParams, l as RecordingTracer, m as noopObserver, n as FlowEventType, o as FlowTracer, p as createRecordingTracer, r as FlowMeter, s as RecordedSpan, t as FlowEvent, u as combineObservers, v as RateRule, w as CompleteStepParams, x as StepGateRequest, y as StepGate } from "./observability-3wVqt3tZ.js";
4
4
  import { i as EnqueueOptions, n as DispatchStepPayload, r as Dispatcher, t as DispatchKind } from "./dispatcher-g7Ct0J6w.js";
5
5
  import { a as WorkflowCompletedArgs, i as BuildStepContextArgs, n as BeforeStartArgs, o as WorkflowHooks, r as BeforeStartResult, t as AfterStepArgs } from "./hooks-BdxIcc92.js";
6
6
  //#region src/core/registry.d.ts
@@ -320,7 +320,7 @@ declare function createRecordingObserver(): RecordingObserver;
320
320
  * The slice of a metrics API the engine's meter observer needs. Structural:
321
321
  * `@octabits-io/telemetry`'s meter and an OpenTelemetry `Meter`
322
322
  * both fit (the SDK's `createCounter`/`createHistogram` need a two-line
323
- * adapter, the framework's meter none).
323
+ * adapter).
324
324
  */
325
325
  interface FlowMeter {
326
326
  counter(name: string, options?: {
@@ -1,5 +1,5 @@
1
1
  import { W as WorkflowId } from "../index-BURAYZui.js";
2
- import { N as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-CR9dEdLW.js";
2
+ import { N as WorkflowStore, g as ConcurrencyRule, i as FlowObserver, t as FlowEvent, v as RateRule, y as StepGate } from "../observability-3wVqt3tZ.js";
3
3
  import { Pool } from "pg";
4
4
  //#region src/postgres/ddl.d.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octaflow",
3
- "version": "0.24.1",
3
+ "version": "0.26.0",
4
4
  "description": "Durable DAG workflow engine: Zod-typed steps over Postgres + pg-boss, with an optional AI add-on (token/cost/quota). Layered single package.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,11 @@
20
20
  "import": "./dist/postgres/index.js",
21
21
  "default": "./dist/postgres/index.js"
22
22
  },
23
+ "./drizzle": {
24
+ "types": "./dist/drizzle/index.d.ts",
25
+ "import": "./dist/drizzle/index.js",
26
+ "default": "./dist/drizzle/index.js"
27
+ },
23
28
  "./pg-boss": {
24
29
  "types": "./dist/pg-boss/index.d.ts",
25
30
  "import": "./dist/pg-boss/index.js",
@@ -65,7 +70,8 @@
65
70
  "llm",
66
71
  "pg-boss",
67
72
  "postgres",
68
- "zod"
73
+ "zod",
74
+ "drizzle"
69
75
  ],
70
76
  "peerDependencies": {
71
77
  "@ai-sdk/anthropic": "^4.0.0",
@@ -74,6 +80,7 @@
74
80
  "@ai-sdk/openai-compatible": "^3.0.0",
75
81
  "@ai-sdk/provider": "^4.0.0",
76
82
  "ai": "^7.0.0",
83
+ "drizzle-orm": ">=0.36.0 <1",
77
84
  "pg": "^8.0.0",
78
85
  "pg-boss": "^12.21.0",
79
86
  "typescript": "^5 || ^6 || ^7",
@@ -98,6 +105,9 @@
98
105
  "ai": {
99
106
  "optional": true
100
107
  },
108
+ "drizzle-orm": {
109
+ "optional": true
110
+ },
101
111
  "pg": {
102
112
  "optional": true
103
113
  },
@@ -106,22 +116,23 @@
106
116
  }
107
117
  },
108
118
  "devDependencies": {
109
- "@ai-sdk/anthropic": "^4.0.52",
110
- "@ai-sdk/mistral": "^4.0.42",
111
- "@ai-sdk/openai": "^4.0.65",
112
- "@ai-sdk/openai-compatible": "^3.0.48",
113
- "@ai-sdk/provider": "^4.0.13",
114
- "@testcontainers/postgresql": "^12.1.0",
115
- "@types/node": "^26.5.1",
119
+ "@ai-sdk/anthropic": "^4.0.69",
120
+ "@ai-sdk/mistral": "^4.0.55",
121
+ "@ai-sdk/openai": "^4.0.82",
122
+ "@ai-sdk/openai-compatible": "^3.0.60",
123
+ "@ai-sdk/provider": "^4.0.20",
124
+ "@testcontainers/postgresql": "^12.2.0",
125
+ "@types/node": "^26.6.3",
116
126
  "@types/pg": "^8.23.1",
117
- "ai": "^7.0.97",
118
- "pg": "^8.23.0",
119
- "pg-boss": "^12.30.0",
120
- "testcontainers": "^12.1.0",
127
+ "ai": "^7.0.123",
128
+ "drizzle-orm": "^0.45.3",
129
+ "pg": "^8.23.1",
130
+ "pg-boss": "^12.35.0",
131
+ "testcontainers": "^12.2.0",
121
132
  "tsdown": "^0.23.0",
122
133
  "typescript": "^7.0.2",
123
- "vitest": "^5.0.0",
124
- "zod": "^4.6.0"
134
+ "vitest": "^5.0.3",
135
+ "zod": "^4.6.5"
125
136
  },
126
137
  "scripts": {
127
138
  "build": "tsdown",