@voltro/plugin-ai-flows 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -39,6 +39,96 @@ _Changes staged for the next release accumulate here (rolled up from
39
39
 
40
40
  ---
41
41
 
42
+ ## [0.11.2] — 2026-07-24
43
+
44
+ ### Added
45
+
46
+ - **@voltro/i18n** — Two escapes for adopting typed messages (`createTypedMessages`, #16) app-wide (#19):
47
+
48
+ - **`t.dynamic(runtimeKey, values?)`** — a first-class escape for a genuinely runtime-computed key, on both `useT` and the `useTFn()` result. It takes a plain string with NO forced ICU args, so it doesn't fight the strict literal-key surface. Until now the natural escape — casting a computed key to the catalog key union — made things WORSE: that union spans placeholder-bearing keys, so the call then demanded a spurious 2nd ICU arg. `t.dynamic` is the documented, discoverable alternative. - **`LooseTFunction`** — the widened `(id: string, values?) => string` signature to type a `t` pass-through across a package boundary that can't import the app catalog, instead of falling back to `(...args: any[]) => string`. A strict `TypedTFunction` is deliberately NOT assignable to it (a narrowed key param can't satisfy a wider one — that would erase the checking); pass `t.dynamic` at the boundary, which IS a `LooseTFunction`.
49
+
50
+ Additive: `TypedTFunction<C>` gains a `.dynamic` member (the callable surface is unchanged, so `Parameters<TypedTFunction<C>>[0]` and existing typed call sites still resolve). `createTypedMessages` attaches `.dynamic` in place on the two translate functions — no new per-render closure, so a captured `t`'s identity stays stable.
51
+ - **@voltro/testing, @voltro/database** — `fixtureRow(table, overrides)` (`@voltro/testing`) completes a partial test row so it satisfies the 0.11.1 required-column insert validation — WITHOUT disabling the check. It fills every NOT-NULL, no-default, non-auto-stamped column the payload omits with a schema-typed placeholder (a `oneOf` column takes its first allowed value; a `unique` column gets a distinct value per call so two fixtures don't collide; `timestamp`/`date` get a fixed epoch), then merges your overrides on top (an explicit value always wins). It leaves out exactly what a caller may omit — nullable, defaulted, and framework auto-stamped columns (id / tenant / audit) — and refuses to guess a structured type (`json` / `bytes` / `vector` / `array` / `interval` / `raw`), throwing a message that names the column and says to pass it explicitly.
52
+
53
+ The motivating case: 0.11.1 made the in-memory/test store reject the same partial inserts real Postgres always would (correct — it surfaced a latent prod bug), which turned lean fixtures (`insert(users, { id })`, an omitted required FK) into `TableValidationFailed`. The wrong fix is a `validateInserts: false` knob — it re-hides that bug class, and a test store laxer than production is a fake testing itself. `fixtureRow` is the right one: it makes the fixture COMPLETE.
54
+
55
+ ```ts
56
+ await ctx.store.insert('journal_entries', fixtureRow(journalEntries, {
57
+ tenantId, amount: '100.00', // the columns THIS test cares about
58
+ })) // entryNumber, postedAt, … auto-filled + unique
59
+ ```
60
+
61
+ It is a runtime filler for the loose `store.insert(name, row)` path (what fixtures use). For COMPILE-time payload typing, use `insertRow` / `upsertRow` from `@voltro/database`. The auto-stamped column set it skips is now exported as `AUTO_FILLED_COLUMNS` from `@voltro/database` — the same list `InferInsertRow` derives its optional columns from, single-sourced so the two can't drift.
62
+
63
+ ### Changed
64
+
65
+ - **@voltro/cli** — `voltro build` now emits **directly-executable** boot bundles for BOTH app kinds: the web start bundle (`.framework/dist-web/startBundle/startEntry.js`) and the api serve bundle (`.framework/dist-api/serveBundle/serveEntry.js`) each carry a main-guard that boots the app when run as `node <entry>.js`, and stays inert when imported (the `voltro start` / `voltro serve` dev fast paths are unchanged). Production containers can now use `CMD ["node", "…/startEntry.js"]` (or `serveEntry.js`) instead of `pnpm voltro start` / `pnpm voltro serve` — no pnpm process, no `@voltro/cli` bin at runtime — which is what makes `voltro prune-runtime` safe to enable on both: with the self-contained bundle as the real entrypoint, the @vercel/nft trace roots there and legitimately drops `@voltro/cli` and the whole inlined framework tree (a static site's `node_modules` collapses to ~0; a memory api's 146 MB → 11 MB). `prune-runtime` now also roots the trace at the serve bundle. The serve entry chdir's to the app root BEFORE its app-module registry keys are computed from cwd, preserving relocation-safety. Existing `pnpm voltro start` / `pnpm voltro serve` entrypoints keep working. The standalone Dockerfiles gain a build-time boot smoke that fails the build unless the pruned tree reaches ready.
66
+
67
+ ### Fixed
68
+
69
+ - **@voltro/i18n** — `createTypedMessages` (#16) no longer extracts phantom required vars from a nested plural/select message (#19). For `'{count, plural, one {# day} other {# days total duration}}'`, the type-level `ICUVars` parse was reading a branch's TEXT (`"# days total duration"`) as a bogus required arg name, so `useT('key', { count })` failed to typecheck even though it renders perfectly — and a real var nested inside a branch was dropped. `ICUArgName` now resolves to `never` for any candidate that isn't a valid ICU identifier (`^[A-Za-z0-9_]+$`), so branch text — which contains spaces / `#` / `—` — is never mistaken for a var. Only the top-level arg (`count`) is required, matching what the message actually needs.
70
+
71
+ Scope note: a REAL var nested inside a plural branch (`other {# — {discipline}}`) is still not collected, so it reads as not-required rather than wrongly-required — the safe direction. Apps that pluralise in JS over simple `{count}` messages (the Voltro idiom) were already fully typed and are unaffected.
72
+ - **@voltro/database, @voltro/runtime** — `InferInsertRow` (and thus `insertRow` / `upsertRow`, #15) no longer requires a non-nullable DB-generated (`generatedAs`) column (#20). A stored/virtual generated column declared without `.nullable()` and without a default was typed **required**, but MariaDB/Postgres REJECT an explicit value for a generated column — so the type forced the caller to pass a value the database refuses at runtime. `.generatedAs()` now marks the column optional-for-insert exactly like a `.default()` column (the DB supplies it), so it may be omitted; the whole payload guard on every real column stays intact.
73
+
74
+ Two runtime halves complete it, so the loose `store.insert(name, row)` path agrees: the required-column validation (`missingRequiredColumns`) skips generated columns — omitting one is correct, never a missing-column error — and the store write path now STRIPS any value a caller supplied for a generated column before the INSERT reaches the dialect (tracked on the schema registry as `generatedColumns`), so a value from an untyped insert can't blow up on MariaDB. A generated column is never caller-supplied; the framework and the DB own it end to end.
75
+
76
+ The `.generatedAs()` return type narrows from `this` to `ColumnBuilder<…, true>` (the HasDefault flag) — a purely more-permissive refinement: it only makes the column omittable, so no existing code stops compiling.
77
+
78
+ ---
79
+
80
+ ## [0.11.1] — 2026-07-23
81
+
82
+ ### Added
83
+
84
+ - **@voltro/runtime** — `defineExecutor(descriptor, fn)` type-checks a query/mutation/action handler's return against the descriptor's `output` schema Type — so returning a `number` where `output` is `timestampMs` (Type = `Date`) is a COMPILE error at the handler, not a runtime encode failure that Dies the subscription. The gap it closes: the executor is a separate default export whose return was never tied to `output`, so a handler that builds a plain object with a leftover `.getTime()` compiled green and only threw `Expected DateFromSelf, actual 1784…` at encode time — invisible while a nullable date was null, exploding the instant it became non-null.
85
+
86
+ Opt-in and zero-cost: it's a runtime identity (returns `fn` unchanged, so the codegen wires it exactly as the bare default export), and the compile check is the whole value. Wrap the handler and import the descriptor into its `*.server.ts`:
87
+
88
+ ```ts
89
+ export default defineExecutor(getRoadmapsByYear, (input, ctx) => …)
90
+ ```
91
+
92
+ The Effect error and requirement channels stay inferred from the handler; only the success value is constrained. A reactive query that returns a `{ descriptor }` builder is allowed through unchecked — the store produces its rows, so a value-level return type can't express that row-vs-output check.
93
+ - **@voltro/database, @voltro/runtime** — An `.encrypted()` column that can't be decrypted with the active key now fails as a typed, readable `FieldDecryptionError` naming the `table.column`, instead of a raw `Error: field cipher: malformed ciphertext` with no context. It carries a `_tag` (the same tagged shape `storeErrors` uses, so `Effect.catchTag` matches), and never includes the ciphertext. The common trigger is restoring a prod/staging snapshot into a dev DB whose `VOLTRO_FIELD_ENCRYPTION_KEY` differs.
94
+
95
+ New dev/migration escape hatch: `VOLTRO_FIELD_DECRYPT_ON_ERROR=null` degrades an undecryptable column to `null` (with one deduped warning per `table.column`, scope `store.fieldEncryption`) instead of letting one bad row nuke the whole read — its readable siblings still decrypt. Default stays `'throw'`; never set `null` in production, where a key mismatch must fail loud. `decryptFieldsOnRead` gains an optional `{ onError, warn }` argument (additive); the raw throw is replaced by the typed one, which existing `catch (e: Error)` handlers still catch.
96
+ - **@voltro/i18n** — `assertCatalogParity({ en, de })` checks every locale uses the SAME ICU `{var}` set per key. `defineLocale` enforces KEY parity but not PLACEHOLDER parity — a translation that drops or renames a `{var}` (`'Published on {date}'` → `'Veröffentlicht'`) compiles and boots, then throws `The intl string context variable "date" was not provided` only in that locale, only when the message renders. Call it in a test or at boot; it throws listing every drift (or warns with `onMismatch: 'warn'`). Plural argument names are extracted; a plural's inner `{# item}` branches are not mistaken for placeholders.
97
+ - **@voltro/i18n** — `createTypedMessages<typeof en>()` binds a catalog's LITERAL message types to `useT` / `useTFn` / `<T>`, so a missing ICU placeholder is a COMPILE error instead of a runtime throw at format time. Until now `defineCatalog` / `defineLocale` enforced key PARITY across locales, but the call site `t('key', values)` was untyped — a message like `'Published on {date}'` called as `t('roadmap.publishedAt')` (or via the `t('key').replace('{{date}}', …)` idiom from other i18n systems) threw `The intl string context variable "date" was not provided` only when it rendered. Now `useT('roadmap.publishedAt')` demands `{ date }` at compile time, and a wrong/missing key is caught too.
98
+
99
+ Opt-in and purely additive: call `createTypedMessages` once with your base catalog (`as const`) and re-export the returned `useT`/`useTFn`/`T`; the bare hooks keep their existing loose signatures. Scope: simple `{name}` and single-argument `{count, number}` forms are extracted; messages with nested inline ICU (`{n, plural, one {…} other {…}}` / `select`) accept a loose values bag rather than a wrong strict one — apps that pluralise in JS over simple `{count}` messages stay fully typed. `<T>` gets a typed key with loose values, because its rich-text `<tag>` renderers can't be modelled by `{var}` extraction.
100
+ - **@voltro/plugin-ai-flows** — An AI-flow `MediaGenerator` (and the `makeMediaGenerator` persistence seam) now receives the run it executes within — `run: { runId, tenantId }` — resolved from the durable run row rather than the caller subject. This is what a resume needs: a BOOTSTRAP/crash resume runs under a tenant-less `SYSTEM_SUBJECT`, so a host that persists artifacts per tenant could not read the tenant from `ctx.request.subject` (it isn't there) and would either fail closed or, under an old anonymous fallback, write into the wrong tenant. The engine already loads the run row (for `ownerId`); it now reads `tenantId` from the same row and hands it down to both the deterministic and agentic media steps, and `MediaPersist.put` / `ingestUrl` forward it to `storage.put` / `ingestUrl` so persistence continues in the run's own tenant on replay.
101
+
102
+ Additive: `run` is appended to the generator/persist arguments, so a host that ignores it keeps compiling; the tenant is simply available when it doesn't. No `@voltro/web` change — the engine is server-only and not re-exported to the browser surface.
103
+ - **@voltro/client, @voltro/web** — A sequence step's `undo` now receives the accumulated context as a second argument — `undo: (result, ctx) => …` — alongside the step's own result. An inverse usually needs an id from an EARLIER step as well as this one's (`deleteJiraDraftTicket({ jiraKey: created.key, draftId: ctx.draft.id })`), and until now the only way to reach it was to re-return that id from the step purely so the undo could read it back. `ctx` is typed as of the step's definition — the steps before it, the same context `covers` and `when` already see — so a later step's result is deliberately not visible (it is rolled back before this one).
104
+
105
+ Additive, not breaking: `StepUndo<Result>` became `StepUndo<Result, Ctx = Record<string, unknown>>` with the context parameter defaulted and appended, so a named `StepUndo<T>` still resolves and an existing single-argument `undo: (r) => …` stays assignable. `@voltro/web` re-exports the client surface, which is why it moves too.
106
+ - **@voltro/cli** — SSR `ctx.query` now has a first-class, server-only api origin for split web/api deployments (#18). Previously, a `renderMode:'ssr'` page reloaded in a split deployment 500'd: the web pod's `POST <origin>/rpc` fell back to the DEV proxy target (`http://localhost:4000`), which nothing serves in production → `ECONNREFUSED` buried in a render error. Browser-reachability and SSR-reachability were conflated into the one `url` field.
107
+
108
+ New `apis.<name>.serverUrl` (and env overrides `VOLTRO_API_ORIGIN_<NAME>` / `VOLTRO_API_ORIGIN`) set the origin the WEB POD uses for SSR — the api's internal cluster DNS (`http://api.<ns>.svc.cluster.local`) — distinct from the browser's relative wsPath, and NEVER emitted into the browser bundle. Resolution: env > `serverUrl` > (dev only) the vite proxy target > an external api's absolute url. Under `voltro start` a package api with none resolves to `undefined` and the loader query FAILS LOUD naming the api and the config to set — it never dials the dev localhost port. A transport failure is wrapped naming the api and the origin attempted, instead of a bare `fetch failed`.
109
+ - **@voltro/database, @voltro/runtime** — `store.insert` / `upsert` / `insertIgnore` now raise a clear, typed `TableValidationFailed` naming the column when the payload omits one that is NOT NULL, has no default, and isn't auto-stamped — instead of a raw dialect `SqlError: Failed to execute statement` (`Field '…' doesn't have a default value`) surfaced only on the INSERT path (so it lay dormant until the first row with no existing cache entry). An upsert / insertIgnore whose payload is missing one of its own `conflictColumns` is likewise named at the call (an absent conflict key can't match its target). The check runs AFTER stamping, so auto-id / tenant / audit columns never trip it, and skips nullable, defaulted, and id (`idScheme`) columns — exactly the ones a caller may legitimately omit.
110
+
111
+ Two pure helpers back it — `missingRequiredColumns(table, row)` and `missingConflictColumns(conflictColumns, row)` (exported from `@voltro/database`). This is the runtime half of the "handler data silently disagrees with the schema" class; a compile-time payload type needs the column DSL to track `hasDefault` at the type level, which is a separate change.
112
+
113
+ **Migration impact — behaviour-breaking for lenient test fixtures.** The in-memory/test store now rejects the same partial inserts a real Postgres always would, so it stops being laxer than production — which is the point (it surfaced at least one latent prod bug where a NOT-NULL `text().unique()` column was written without a value). But a fixture that inserted a partial row (`{ id }` parents, an omitted required FK) and passed against the old lenient memory store now throws `TableValidationFailed`. There is no code-level codemod — the fix is fixture DATA: fill the required columns. Use the new `fixtureRow(table, overrides)` helper in `@voltro/testing`, which fills every NOT-NULL-no-default column with a schema-typed placeholder and merges your overrides on top, so a fixture complies without disabling the check. There is deliberately no opt-out to turn the validation off: a test store that accepts rows production rejects is a fake testing itself.
114
+ - **@voltro/database, @voltro/plugin-ai-flows, @voltro/plugin-audit, @voltro/plugin-deactivation, @voltro/plugin-soft-delete** — Compile-time payload typing for writes (#15) — the type-level half that the runtime `TableValidationFailed` guard flagged as a separate change. `insertRow` / `upsertRow` take the TABLE OBJECT (not a string name), so the payload is checked against `InferInsertRow<T>`: every column is required EXCEPT nullable ones, columns with a default, and the framework-filled id/tenant/audit columns. A missing NOT-NULL-no-default column — the exact `lastRefreshedAt` / `teamId` omission from the report — is now a COMPILE error at the call, not a runtime SqlError only on the INSERT path; `upsertRow`'s `conflictColumns` are constrained to the table's own columns too.
115
+
116
+ import { insertRow } from '@voltro/database' await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
117
+
118
+ Enabled by a type-level default flag: `ColumnDefinition` / `ColumnBuilder` gained a third `HasDefault` parameter that `.default()` narrows to `true`. It defaults to `boolean`, so every existing `ColumnDefinition<unknown>` (mixins, query builder, migrate, plugins) is unaffected — the only golden churn is the additive third parameter rendering (e.g. an audit mixin's defaulted `createdAt` now shows `ColumnDefinition<Date, "timestamp", true>`). The string-keyed `store.insert` / `upsert` are unchanged; the typed seam is opt-in.
119
+ - **@voltro/runtime, @voltro/cli** — `voltro dev` now warns, once per tenant-scoped table, when a request reads it with the empty-string tenant sentinel — an authenticated subject that has no resolved org (`tenantId === ''`). The auto-merged tenant filter becomes `eq('tenantId', '')`, which matches no real row, so every such read returns empty WITH NO ERROR — indistinguishable, from the response alone, between "no such row", "filtered by an empty tenant", and "auth half-resolved". The warning names the cause and the fix. `voltro serve` deliberately stays silent (a prod diagnostic on every scoped read is noise). The decision is a pure predicate, `isEmptyTenantScopedRead`, exported from `@voltro/runtime` so it is unit-tested apart from the 6k-line dev boot; `applyTenantScope` itself stays pure.
120
+ - **@voltro/runtime, @voltro/cli** — `voltro dev` now warns once, the first time an authenticated subject resolves with NO active org (a `user` carrying the empty-string tenant sentinel) — "authenticated, but no active org → all tenant-scoped reads will be empty". Broader and earlier than the per-table empty-tenant read warning: it catches the whole class at the door instead of on a specific read. Backed by the pure `isOrglessUserSubject` predicate in `@voltro/runtime`; dev owns the one-time log.
121
+ - **@voltro/cli** — `voltro start` (web) now reports boot timing, matching `voltro serve`. It always logs a structured `start: ready in <n>ms` line (with `bootMs`), counted from PROCESS start so the module-graph load — the phase that dominates a scale-to-zero cold start — is included instead of being missed by a mid-boot baseline; the banner's `bootMs` uses the same total. Under `VOLTRO_BOOT_TIMING=1` the line also carries a per-phase `phases` breakdown (`modules`, `config`, `scan`, `provider`, `routes`, `cdc`, `ready`). Because the total is a structured log record, it is retrievable from the container's `/_voltro/inspect/logs` endpoint, not only from stdout.
122
+ - **@voltro/cli** — Production web images are now self-contained and dramatically smaller. `voltro build` makes every runtime artefact framework-inlined + tree-shaken — the SSR bundle (vite `noExternal: true`), the start bundle, and the precompiled `appConfig` — so a booted `voltro start` needs from `node_modules` only the runtime-external NATIVE leaves it actually reaches (a SQL driver an ISR/config path touches). A new hidden `voltro prune-runtime <deploy-dir>` command (`@vercel/nft`, a new build-time optionalDependency) traces the real reachable set from those bundles + the app's installed native leaves and drops the rest — the whole `@voltro`/effect/react tree that's now dead weight. The standalone web Dockerfiles run it after `pnpm --prod deploy`; for a static/SSR site with no native runtime dep, `node_modules` collapses to nothing. Measured on a real marketing app: the app tree drops from ~210 MB to ~65 MB (node_modules ~145 MB → 0) and still boots + renders. Fully automatic — a used native driver is traced + kept, an unused one dropped, no per-app allow-list — and non-fatal: any trace failure keeps the fuller tree (a bigger image, never a broken one). The appConfig `@voltro/*` imports are now inlined (were external); this is safe because `runEnvGate` consumes the env schema structurally (`isEnvContract` duck-types `{ vars }`, no `instanceof`/Symbol), so a schema built by the config's inlined `@voltro/env` still validates.
123
+ - **@voltro/cli** — `voltro build` now precompiles the web START runtime into a single bundle (`.framework/dist-web/startBundle/startEntry.js`, framework inlined) — the web counterpart to the api serve bundle. `bin/voltro.mjs`'s `voltro start` fast path prefers it, so a cold scale-from-zero web boot loads ONE artefact instead of resolving the whole `@voltro`/effect module graph. Measured on the web-spa-shell fixture: the `modules` boot phase collapses from ~1130 ms to ~67 ms (~17×), directly cutting the phase measured to dominate a scale-to-zero web cold start. Non-fatal — if the bundle build or its import fails, `voltro start` falls back to the per-module CLI entry (correct, just slower). No app or Dockerfile change is needed; the bundle rides in `.framework`, which `pnpm deploy` already copies.
124
+
125
+ ### Fixed
126
+
127
+ - **@voltro/runtime** — An undeclared infra error no longer reaches the browser as an `ExitEncoded` schema-tree dump (a wall of text that leaks internals and no client can pattern-match). `FieldDecryptionError` — a `.encrypted()` column that can't be decrypted with the active key — now collapses to a generic `InternalError` on both the unary and the subscription paths (it was previously only `SqlError` / `ResultLengthMismatch`, and only on the unary path). Its message names an internal `table.column`, so this also stops that leak; the real cause is logged server-side with the traceId. `TableValidationFailed` is deliberately left through — its summary/issues are meant to be shown to a user. Typed app errors are untouched.
128
+ - **@voltro/cli** — Security: the `/_voltro/inspect/logs` endpoint no longer bypasses the inspect gate. It dumps the process LogBuffer (request URLs + error payloads), but it returned early — above the `isInspectDisabled` / `VOLTRO_INSPECT_TOKEN` checks — so `VOLTRO_INSPECT=off` closed the manifest and metrics (503) while leaving the log buffer publicly readable, and a configured `VOLTRO_INSPECT_TOKEN` was ignored for it. The gate now lives inside `handleLogsRequest`, single-sourced across the three call sites (`voltro start` / `voltro dev` / the web-dev server) that each drifted (start ungated, dev gated nothing, web-dev gated disabled-but-not-token): disabled → 503, missing/bad bearer → 401.
129
+
130
+ ---
131
+
42
132
  ## [0.11.0] — 2026-07-22
43
133
 
44
134
  ### ⚠ BREAKING
package/dist/index.d.ts CHANGED
@@ -20,23 +20,23 @@ export declare interface AgentResolution {
20
20
  /** `ai_flow_runs` — one execution. `.reactive()`: the engine patches this row
21
21
  * as it progresses and the client's subscription streams the live timeline. */
22
22
  export declare const aiFlowRunsTable: Table<"ai_flow_runs", FieldDefinitions<{
23
- readonly id: ColumnBuilder<string, "id">;
23
+ readonly id: ColumnBuilder<string, "id", boolean>;
24
24
  /** Name (code flow) or id (data flow). Not an FK — no single target. */
25
- readonly flowRef: ColumnBuilder<string, "text">;
25
+ readonly flowRef: ColumnBuilder<string, "text", boolean>;
26
26
  /** Denormalized flow name — survives a definition delete. */
27
- readonly flowName: ColumnBuilder<string | null, "text">;
27
+ readonly flowName: ColumnBuilder<string | null, "text", boolean>;
28
28
  /** Snapshot of the mode this run executes in. */
29
- readonly mode: ColumnBuilder<string, "text">;
29
+ readonly mode: ColumnBuilder<string, "text", true>;
30
30
  /** pending | running | waiting | succeeded | failed | cancelled. */
31
- readonly status: ColumnBuilder<string, "text">;
31
+ readonly status: ColumnBuilder<string, "text", true>;
32
32
  /** manual | cron. */
33
- readonly source: ColumnBuilder<string | null, "text">;
33
+ readonly source: ColumnBuilder<string | null, "text", boolean>;
34
34
  /** Correlation + idempotency key (→ the workflow's idempotencyKey). */
35
- readonly requestId: ColumnBuilder<string, "text">;
35
+ readonly requestId: ColumnBuilder<string, "text", boolean>;
36
36
  /** Brief values, keyed by `BriefField.key`. */
37
- readonly input: ColumnBuilder<Record<string, unknown> | null, "json">;
37
+ readonly input: ColumnBuilder<Record<string, unknown> | null, "json", boolean>;
38
38
  /** The result bundle: `outputKey → value` (what the UI's result dialog reads). */
39
- readonly output: ColumnBuilder<Record<string, unknown> | null, "json">;
39
+ readonly output: ColumnBuilder<Record<string, unknown> | null, "json", boolean>;
40
40
  /** Live per-step timeline (`RunStep[]`); the engine writes it on every step. */
41
41
  readonly steps: ColumnBuilder<readonly {
42
42
  readonly id: number;
@@ -61,16 +61,16 @@ export declare const aiFlowRunsTable: Table<"ai_flow_runs", FieldDefinitions<{
61
61
  }[] | undefined;
62
62
  readonly mode: "text" | "approve" | "choice";
63
63
  } | undefined;
64
- }[] | null, "json">;
65
- readonly currentStep: ColumnBuilder<number, "integer">;
66
- readonly totalSteps: ColumnBuilder<number, "integer">;
64
+ }[] | null, "json", boolean>;
65
+ readonly currentStep: ColumnBuilder<number, "integer", true>;
66
+ readonly totalSteps: ColumnBuilder<number, "integer", true>;
67
67
  /** The HITL answer, written by `respond`. */
68
68
  readonly humanResponse: ColumnBuilder<{
69
69
  readonly value?: string | undefined;
70
70
  readonly text?: string | undefined;
71
71
  readonly decision?: "approve" | "reject" | undefined;
72
72
  readonly respondedAt?: string | undefined;
73
- } | null, "json">;
73
+ } | null, "json", boolean>;
74
74
  /** Staged follow-up when the chain requires confirmation. */
75
75
  readonly chainPending: ColumnBuilder<{
76
76
  readonly input: {
@@ -78,28 +78,28 @@ export declare const aiFlowRunsTable: Table<"ai_flow_runs", FieldDefinitions<{
78
78
  };
79
79
  readonly status: "pending" | "confirmed" | "dismissed";
80
80
  readonly flowRef: string;
81
- } | null, "json">;
81
+ } | null, "json", boolean>;
82
82
  /** Accumulated cost in micro-USD (app view projects to €/cents). */
83
- readonly costMicroUsd: ColumnBuilder<number, "integer">;
84
- readonly durationMs: ColumnBuilder<number | null, "integer">;
85
- readonly error: ColumnBuilder<string | null, "text">;
86
- readonly errorMessage: ColumnBuilder<string | null, "text">;
87
- readonly startedAt: ColumnBuilder<Date | null, "timestamp">;
88
- readonly completedAt: ColumnBuilder<Date | null, "timestamp">;
89
- readonly cancelledAt: ColumnBuilder<Date | null, "timestamp">;
83
+ readonly costMicroUsd: ColumnBuilder<number, "integer", true>;
84
+ readonly durationMs: ColumnBuilder<number | null, "integer", boolean>;
85
+ readonly error: ColumnBuilder<string | null, "text", boolean>;
86
+ readonly errorMessage: ColumnBuilder<string | null, "text", boolean>;
87
+ readonly startedAt: ColumnBuilder<Date | null, "timestamp", boolean>;
88
+ readonly completedAt: ColumnBuilder<Date | null, "timestamp", boolean>;
89
+ readonly cancelledAt: ColumnBuilder<Date | null, "timestamp", boolean>;
90
90
  /** Studio session grouping (app concept; soft ref). */
91
- readonly sessionId: ColumnBuilder<string | null, "text">;
92
- readonly sessionName: ColumnBuilder<string | null, "text">;
91
+ readonly sessionId: ColumnBuilder<string | null, "text", boolean>;
92
+ readonly sessionName: ColumnBuilder<string | null, "text", boolean>;
93
93
  /** App subject the run is for/by. */
94
- readonly ownerId: ColumnBuilder<string | null, "text">;
95
- readonly metadata: ColumnBuilder<Record<string, unknown> | null, "json">;
94
+ readonly ownerId: ColumnBuilder<string | null, "text", boolean>;
95
+ readonly metadata: ColumnBuilder<Record<string, unknown> | null, "json", boolean>;
96
96
  }> & {
97
97
  tenantId: ColumnDefinition<string>;
98
98
  } & {
99
- readonly createdAt: ColumnDefinition<Date, "timestamp">;
100
- readonly updatedAt: ColumnDefinition<Date, "timestamp">;
101
- readonly createdBy: ColumnDefinition<string | null, "reference">;
102
- readonly updatedBy: ColumnDefinition<string | null, "reference">;
99
+ readonly createdAt: ColumnDefinition<Date, "timestamp", true>;
100
+ readonly updatedAt: ColumnDefinition<Date, "timestamp", true>;
101
+ readonly createdBy: ColumnDefinition<string | null, "reference", boolean>;
102
+ readonly updatedBy: ColumnDefinition<string | null, "reference", boolean>;
103
103
  }, true, never>;
104
104
 
105
105
  /**
@@ -122,19 +122,19 @@ export declare interface AiFlowsPluginOptions {
122
122
  /** `ai_flows` — a reusable flow DEFINITION (the data front door persists these;
123
123
  * code-first `defineFlow` flows are NOT rows — they register in-process). */
124
124
  export declare const aiFlowsTable: Table<"ai_flows", FieldDefinitions<{
125
- readonly id: ColumnBuilder<string, "id">;
126
- readonly name: ColumnBuilder<string, "text">;
127
- readonly description: ColumnBuilder<string | null, "text">;
125
+ readonly id: ColumnBuilder<string, "id", boolean>;
126
+ readonly name: ColumnBuilder<string, "text", boolean>;
127
+ readonly description: ColumnBuilder<string | null, "text", boolean>;
128
128
  /** 'deterministic' | 'agentic' — the execution mode (replaces allowDeviation). */
129
- readonly mode: ColumnBuilder<string, "text">;
129
+ readonly mode: ColumnBuilder<string, "text", true>;
130
130
  /** 'draft' | 'active' | 'archived'. */
131
- readonly status: ColumnBuilder<string, "text">;
131
+ readonly status: ColumnBuilder<string, "text", true>;
132
132
  /** 'private' | 'organization' | 'shared'. */
133
- readonly visibility: ColumnBuilder<string, "text">;
134
- readonly orchestratorModel: ColumnBuilder<string | null, "text">;
135
- readonly orchestratorInstructions: ColumnBuilder<string | null, "text">;
133
+ readonly visibility: ColumnBuilder<string, "text", true>;
134
+ readonly orchestratorModel: ColumnBuilder<string | null, "text", boolean>;
135
+ readonly orchestratorInstructions: ColumnBuilder<string | null, "text", boolean>;
136
136
  /** Agentic loop bound; clamped [1,100] at run (default 30). */
137
- readonly maxSteps: ColumnBuilder<number, "integer">;
137
+ readonly maxSteps: ColumnBuilder<number, "integer", true>;
138
138
  /** The ordered plan (`FlowStep[]`); null ≡ empty. */
139
139
  readonly steps: ColumnBuilder<readonly {
140
140
  readonly id: string;
@@ -166,7 +166,7 @@ export declare const aiFlowsTable: Table<"ai_flows", FieldDefinitions<{
166
166
  readonly value: string;
167
167
  readonly label: string;
168
168
  }[] | undefined;
169
- }[] | null, "json">;
169
+ }[] | null, "json", boolean>;
170
170
  /** The launch-form brief (`BriefField[]`). */
171
171
  readonly inputSchema: ColumnBuilder<readonly {
172
172
  readonly key: string;
@@ -174,7 +174,7 @@ export declare const aiFlowsTable: Table<"ai_flows", FieldDefinitions<{
174
174
  readonly required?: boolean | undefined;
175
175
  readonly label: string;
176
176
  readonly placeholder?: string | undefined;
177
- }[] | null, "json">;
177
+ }[] | null, "json", boolean>;
178
178
  readonly chainTo: ColumnBuilder<{
179
179
  readonly flowRef: string;
180
180
  readonly mappings: readonly {
@@ -182,7 +182,7 @@ export declare const aiFlowsTable: Table<"ai_flows", FieldDefinitions<{
182
182
  readonly targetKey: string;
183
183
  }[];
184
184
  readonly requireConfirmation?: boolean | undefined;
185
- } | null, "json">;
185
+ } | null, "json", boolean>;
186
186
  readonly cadence: ColumnBuilder<{
187
187
  readonly frequency?: "weekly" | "monthly" | undefined;
188
188
  readonly repeats: readonly number[];
@@ -194,28 +194,28 @@ export declare const aiFlowsTable: Table<"ai_flows", FieldDefinitions<{
194
194
  readonly defaultInput?: {
195
195
  readonly [x: string]: unknown;
196
196
  } | undefined;
197
- } | null, "json">;
197
+ } | null, "json", boolean>;
198
198
  /** Master on/off — gates scheduling. */
199
- readonly isEnabled: ColumnBuilder<boolean, "boolean">;
200
- readonly metadata: ColumnBuilder<Record<string, unknown> | null, "json">;
201
- readonly icon: ColumnBuilder<string | null, "text">;
202
- readonly color: ColumnBuilder<string | null, "text">;
199
+ readonly isEnabled: ColumnBuilder<boolean, "boolean", true>;
200
+ readonly metadata: ColumnBuilder<Record<string, unknown> | null, "json", boolean>;
201
+ readonly icon: ColumnBuilder<string | null, "text", boolean>;
202
+ readonly color: ColumnBuilder<string | null, "text", boolean>;
203
203
  /** Flow avatar as a plain URL (avoids a hard storage-plugin schema dep). */
204
- readonly avatarUrl: ColumnBuilder<string | null, "text">;
204
+ readonly avatarUrl: ColumnBuilder<string | null, "text", boolean>;
205
205
  /** Run counter (incremented on launch). */
206
- readonly usageCount: ColumnBuilder<number, "integer">;
206
+ readonly usageCount: ColumnBuilder<number, "integer", true>;
207
207
  /** App subject the flow belongs to (distinct from audit `createdBy` → actors). */
208
- readonly ownerId: ColumnBuilder<string | null, "text">;
208
+ readonly ownerId: ColumnBuilder<string | null, "text", boolean>;
209
209
  }> & {
210
210
  tenantId: ColumnDefinition<string>;
211
211
  } & {
212
- readonly createdAt: ColumnDefinition<Date, "timestamp">;
213
- readonly updatedAt: ColumnDefinition<Date, "timestamp">;
214
- readonly createdBy: ColumnDefinition<string | null, "reference">;
215
- readonly updatedBy: ColumnDefinition<string | null, "reference">;
212
+ readonly createdAt: ColumnDefinition<Date, "timestamp", true>;
213
+ readonly updatedAt: ColumnDefinition<Date, "timestamp", true>;
214
+ readonly createdBy: ColumnDefinition<string | null, "reference", boolean>;
215
+ readonly updatedBy: ColumnDefinition<string | null, "reference", boolean>;
216
216
  } & {
217
- readonly deletedAt: ColumnDefinition<Date | null, "timestamp">;
218
- readonly deletedBy: ColumnDefinition<string | null, "reference">;
217
+ readonly deletedAt: ColumnDefinition<Date | null, "timestamp", boolean>;
218
+ readonly deletedBy: ColumnDefinition<string | null, "reference", boolean>;
219
219
  }, true, never>;
220
220
 
221
221
  /** Every table this plugin contributes (fed to `extendSchema`). */
@@ -669,6 +669,9 @@ export declare type MediaGenerator = (args: {
669
669
  readonly prompt: string;
670
670
  readonly params?: Record<string, unknown>;
671
671
  readonly inputs?: Record<string, unknown>;
672
+ /** The run this artifact belongs to — pin persistence to `run.tenantId` rather
673
+ * than the current subject, so a system/bootstrap resume stores it correctly. */
674
+ readonly run: MediaRunContext;
672
675
  }) => Effect.Effect<MediaResult, FlowCapabilityMissing>;
673
676
 
674
677
  export declare const MediaInputs: Schema.Record$<typeof Schema.String, Schema.Union<[Schema.Struct<{
@@ -694,16 +697,22 @@ export declare const MediaInputSlot: Schema.Union<[Schema.Struct<{
694
697
 
695
698
  export declare type MediaInputSlot = typeof MediaInputSlot.Type;
696
699
 
697
- /** Host-supplied persistence (from @voltro/plugin-storage's StorageService). */
700
+ /** Host-supplied persistence (from @voltro/plugin-storage's StorageService).
701
+ *
702
+ * Both hooks receive the `run` — its `tenantId` is the durable, resume-safe
703
+ * tenant (see `MediaRunContext`). Pass it to `storage.put`/`ingestUrl` as the
704
+ * scope so a BOOTSTRAP resume, whose subject has no tenant, still writes into
705
+ * the run's own tenant instead of failing or defaulting. */
698
706
  export declare interface MediaPersist {
699
707
  /** Store raw bytes / base64 → a hosted URL (StorageService.put). */
700
708
  readonly put: (args: {
701
709
  data: Uint8Array | string;
702
710
  mediaType: string;
711
+ run: MediaRunContext;
703
712
  }) => Promise<StoredMedia>;
704
713
  /** Re-host a provider URL (StorageService.ingestUrl). If absent, a provider
705
714
  * video URL is used as-is (no re-hosting). */
706
- readonly ingestUrl?: (url: string) => Promise<StoredMedia>;
715
+ readonly ingestUrl?: (url: string, run: MediaRunContext) => Promise<StoredMedia>;
707
716
  }
708
717
 
709
718
  /** Media generation result (the host generator persists the artifact + returns
@@ -714,6 +723,21 @@ export declare interface MediaResult {
714
723
  readonly requestId?: string;
715
724
  }
716
725
 
726
+ /** The run a media step executes within, resolved from the durable RUN ROW —
727
+ * not the caller subject. This is the difference between a normal call and a
728
+ * resume: a BOOTSTRAP/crash resume runs under a tenant-less `SYSTEM_SUBJECT`,
729
+ * so a host that persists artifacts per tenant cannot read the tenant from
730
+ * `ctx.request.subject` (it isn't there) and would either fail closed or, worse
731
+ * under the old fallback, write into the wrong tenant. The engine reads the run
732
+ * row it already loads and hands the original tenant down here, so persistence
733
+ * continues in the right place on replay. */
734
+ declare interface MediaRunContext {
735
+ readonly runId: string;
736
+ /** The run's tenant as stamped when it was created, or `null` for a
737
+ * single-tenant app. Authoritative on resume, where the subject has none. */
738
+ readonly tenantId: string | null;
739
+ }
740
+
717
741
  export declare const Modality: Schema.Literal<["text", "image", "video", "audio"]>;
718
742
 
719
743
  export declare type Modality = typeof Modality.Type;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BriefField as e, ChainPending as t, FlowCadence as n, FlowChain as r, FlowMode as i, FlowStatus as a, FlowStep as o, HumanResponse as s, Json as c, MAX_STEPS_DEFAULT as l, MAX_STEPS_MAX as u, MAX_STEPS_MIN as d, MediaInputSlot as f, MediaInputs as p, Modality as m, ReviewMode as h, ReviewOption as ee, RunReview as te, RunSource as ne, RunStatus as re, RunStep as ie, RunStepStatus as ae, StepType as oe, Visibility as se, clampMaxSteps as ce, normalizeFlow as g, sanitizeOutputKey as le } from "./ir.js";
1
+ import { BriefField as e, ChainPending as t, FlowCadence as n, FlowChain as r, FlowMode as i, FlowStatus as a, FlowStep as o, HumanResponse as s, Json as c, MAX_STEPS_DEFAULT as l, MAX_STEPS_MAX as u, MAX_STEPS_MIN as d, MediaInputSlot as f, MediaInputs as p, Modality as ee, ReviewMode as m, ReviewOption as h, RunReview as te, RunSource as ne, RunStatus as re, RunStep as ie, RunStepStatus as ae, StepType as oe, Visibility as se, clampMaxSteps as ce, normalizeFlow as g, sanitizeOutputKey as le } from "./ir.js";
2
2
  import { FLOW_RUN_WORKFLOW as _, HUMAN_RESPONSE_SIGNAL as v } from "./workflow.js";
3
3
  import { FlowBriefIncomplete as ue, FlowCapabilityMissing as y, FlowDependencyError as de, FlowNotAwaitingResponse as fe, FlowNotFound as pe, FlowRunNotFound as b, FlowValidationError as me } from "./errors.js";
4
4
  import { n as he, t as ge } from "./cadence-Bt6dPelT.js";
@@ -99,12 +99,12 @@ var R = D("ai_flows", {
99
99
  flowRef: t,
100
100
  message: `no flow registered or stored for "${t}"`
101
101
  }));
102
- }), Fe = (e, t, n, r) => x.gen(function* () {
103
- let e = H(n.prompt ?? "", r), i = De(n.prompt ?? "", r);
104
- if (i.length > 0) return { error: new de({
102
+ }), Fe = (e, t, n, r, i) => x.gen(function* () {
103
+ let e = H(n.prompt ?? "", r), a = De(n.prompt ?? "", r);
104
+ if (a.length > 0) return { error: new de({
105
105
  stepId: n.id,
106
- unresolvedRef: i[0],
107
- message: `step "${n.id}" references unresolved {{${i[0]}}}`
106
+ unresolvedRef: a[0],
107
+ message: `step "${n.id}" references unresolved {{${a[0]}}}`
108
108
  }).message };
109
109
  switch (n.type) {
110
110
  case "note": return {
@@ -187,11 +187,12 @@ var R = D("ai_flows", {
187
187
  capability: `media:${r}`,
188
188
  message: `no media generator configured for ${r}`
189
189
  }).message };
190
- let i = n.model ?? t.defaultModels?.[r], a = yield* t.generateMedia({
190
+ let a = n.model ?? t.defaultModels?.[r], o = yield* t.generateMedia({
191
191
  modality: r,
192
- ...i ? { model: i } : {},
192
+ ...a ? { model: a } : {},
193
193
  prompt: e,
194
- ...n.params ? { params: n.params } : {}
194
+ ...n.params ? { params: n.params } : {},
195
+ run: i
195
196
  }).pipe(x.map((e) => ({
196
197
  ok: !0,
197
198
  m: e
@@ -199,12 +200,12 @@ var R = D("ai_flows", {
199
200
  ok: !1,
200
201
  e
201
202
  })));
202
- return a.ok ? {
203
- value: a.m.url,
204
- url: a.m.url,
205
- ...a.m.requestId ? { requestId: a.m.requestId } : {},
206
- costMicroUsd: a.m.costMicroUsd ?? 0
207
- } : { error: a.e.message };
203
+ return o.ok ? {
204
+ value: o.m.url,
205
+ url: o.m.url,
206
+ ...o.m.requestId ? { requestId: o.m.requestId } : {},
207
+ costMicroUsd: o.m.costMicroUsd ?? 0
208
+ } : { error: o.e.message };
208
209
  }
209
210
  case "human": return { error: new y({
210
211
  capability: "human",
@@ -318,7 +319,10 @@ var R = D("ai_flows", {
318
319
  name: `flow-${i}-${u.id}`,
319
320
  input: { index: i },
320
321
  success: Me,
321
- execute: Fe(e, t, u, o)
322
+ execute: Fe(e, t, u, o, {
323
+ runId: n,
324
+ tenantId: a.tenantId
325
+ })
322
326
  });
323
327
  if (d.error) return c[i] = {
324
328
  ...c[i],
@@ -416,36 +420,36 @@ var R = D("ai_flows", {
416
420
  toolCostMicroUsd: S.Number,
417
421
  steps: S.Array(ie),
418
422
  error: S.optional(S.String)
419
- }), He = (e, t, n, r, i, a) => x.gen(function* () {
423
+ }), He = (e, t, n, r, i, a, o) => x.gen(function* () {
420
424
  yield* q(e, n, {
421
425
  status: "running",
422
426
  startedAt: W(),
423
427
  totalSteps: r.steps.length
424
428
  });
425
- let o = yield* M({
429
+ let s = yield* M({
426
430
  name: "flow-agentic",
427
431
  input: { flowRef: r.name },
428
432
  success: Ve,
429
433
  execute: x.gen(function* () {
430
- let o = [], s = 0, c = (e) => {
431
- let t = o.length;
432
- return o.push({
434
+ let s = [], c = 0, l = (e) => {
435
+ let t = s.length;
436
+ return s.push({
433
437
  id: t,
434
438
  status: "running",
435
439
  startedAt: W(),
436
440
  ...e
437
441
  }), t;
438
- }, l = (e, t) => {
439
- o[e] = {
440
- ...o[e],
442
+ }, u = (e, t) => {
443
+ s[e] = {
444
+ ...s[e],
441
445
  completedAt: W(),
442
446
  ...t
443
447
  };
444
- }, u = () => q(e, n, {
445
- steps: o,
446
- currentStep: o.length,
447
- costMicroUsd: s
448
- }), d = N({
448
+ }, d = () => q(e, n, {
449
+ steps: s,
450
+ currentStep: s.length,
451
+ costMicroUsd: c
452
+ }), f = N({
449
453
  name: "run_agent",
450
454
  description: "Delegate a text task to an allowed sub-agent. Returns the agent's text.",
451
455
  input: S.Struct({
@@ -453,14 +457,14 @@ var R = D("ai_flows", {
453
457
  prompt: S.String
454
458
  }),
455
459
  execute: ({ agentRef: e, prompt: n }) => x.gen(function* () {
456
- let r = c({
460
+ let r = l({
457
461
  title: `agent:${e}`,
458
462
  type: "agent"
459
463
  });
460
- if (yield* u(), !t.resolveAgent) return l(r, {
464
+ if (yield* d(), !t.resolveAgent) return u(r, {
461
465
  status: "failed",
462
466
  errorMessage: "no agent resolver configured"
463
- }), yield* u(), "ERROR: no agent resolver configured";
467
+ }), yield* d(), "ERROR: no agent resolver configured";
464
468
  let i = yield* t.resolveAgent(e).pipe(x.map((e) => ({
465
469
  ok: !0,
466
470
  a: e
@@ -468,10 +472,10 @@ var R = D("ai_flows", {
468
472
  ok: !1,
469
473
  e
470
474
  })));
471
- if (!i.ok) return l(r, {
475
+ if (!i.ok) return u(r, {
472
476
  status: "failed",
473
477
  errorMessage: i.e.message
474
- }), yield* u(), `ERROR: ${i.e.message}`;
478
+ }), yield* d(), `ERROR: ${i.e.message}`;
475
479
  let a = yield* L({
476
480
  system: i.a.system,
477
481
  prompt: n,
@@ -485,19 +489,19 @@ var R = D("ai_flows", {
485
489
  })));
486
490
  if (!a.ok) {
487
491
  let e = String(a.e.message ?? a.e);
488
- return l(r, {
492
+ return u(r, {
489
493
  status: "failed",
490
494
  errorMessage: e
491
- }), yield* u(), `ERROR: ${e}`;
495
+ }), yield* d(), `ERROR: ${e}`;
492
496
  }
493
497
  let o = K(a.r.usage, a.r.providerMetadata, i.a.model);
494
- return s += o, l(r, {
498
+ return c += o, u(r, {
495
499
  status: "succeeded",
496
500
  output: { text: a.r.text },
497
501
  costMicroUsd: o
498
- }), yield* u(), a.r.text;
502
+ }), yield* d(), a.r.text;
499
503
  })
500
- }), f = N({
504
+ }), p = N({
501
505
  name: "generate_media",
502
506
  description: "Generate an image, video, or audio artifact. Returns the hosted URL.",
503
507
  input: S.Struct({
@@ -505,19 +509,23 @@ var R = D("ai_flows", {
505
509
  prompt: S.String,
506
510
  model: S.optional(S.String)
507
511
  }),
508
- execute: ({ modality: e, prompt: n, model: r }) => x.gen(function* () {
509
- let i = c({
512
+ execute: ({ modality: e, prompt: r, model: i }) => x.gen(function* () {
513
+ let a = l({
510
514
  title: `media:${e}`,
511
515
  type: "generate"
512
516
  });
513
- if (yield* u(), !t.generateMedia) return l(i, {
517
+ if (yield* d(), !t.generateMedia) return u(a, {
514
518
  status: "failed",
515
519
  errorMessage: `no media generator for ${e}`
516
- }), yield* u(), `ERROR: no media generator configured for ${e}`;
517
- let a = yield* t.generateMedia({
520
+ }), yield* d(), `ERROR: no media generator configured for ${e}`;
521
+ let s = yield* t.generateMedia({
518
522
  modality: e,
519
- prompt: n,
520
- ...r ? { model: r } : {}
523
+ prompt: r,
524
+ ...i ? { model: i } : {},
525
+ run: {
526
+ runId: n,
527
+ tenantId: o
528
+ }
521
529
  }).pipe(x.map((e) => ({
522
530
  ok: !0,
523
531
  r: e
@@ -525,17 +533,17 @@ var R = D("ai_flows", {
525
533
  ok: !1,
526
534
  e
527
535
  })));
528
- return a.ok ? (s += a.r.costMicroUsd ?? 0, l(i, {
536
+ return s.ok ? (c += s.r.costMicroUsd ?? 0, u(a, {
529
537
  status: "succeeded",
530
- url: a.r.url,
531
- ...a.r.requestId ? { requestId: a.r.requestId } : {},
532
- costMicroUsd: a.r.costMicroUsd ?? 0
533
- }), yield* u(), a.r.url) : (l(i, {
538
+ url: s.r.url,
539
+ ...s.r.requestId ? { requestId: s.r.requestId } : {},
540
+ costMicroUsd: s.r.costMicroUsd ?? 0
541
+ }), yield* d(), s.r.url) : (u(a, {
534
542
  status: "failed",
535
- errorMessage: a.e.message
536
- }), yield* u(), `ERROR: ${a.e.message}`);
543
+ errorMessage: s.e.message
544
+ }), yield* d(), `ERROR: ${s.e.message}`);
537
545
  })
538
- }), p = N({
546
+ }), ee = N({
539
547
  name: "pick_structured",
540
548
  description: "Produce a JSON object for a task. Returns the object.",
541
549
  input: S.Struct({
@@ -543,11 +551,11 @@ var R = D("ai_flows", {
543
551
  model: S.optional(S.String)
544
552
  }),
545
553
  execute: ({ prompt: e, model: n }) => x.gen(function* () {
546
- let r = c({
554
+ let r = l({
547
555
  title: "structured",
548
556
  type: "structured"
549
557
  });
550
- yield* u();
558
+ yield* d();
551
559
  let i = yield* I({
552
560
  prompt: e,
553
561
  schema: S.Record({
@@ -564,17 +572,17 @@ var R = D("ai_flows", {
564
572
  })));
565
573
  if (!i.ok) {
566
574
  let e = String(i.e.message ?? i.e);
567
- return l(r, {
575
+ return u(r, {
568
576
  status: "failed",
569
577
  errorMessage: e
570
- }), yield* u(), { error: e };
578
+ }), yield* d(), { error: e };
571
579
  }
572
580
  let a = K(i.r.usage, i.r.providerMetadata, n);
573
- return s += a, l(r, {
581
+ return c += a, u(r, {
574
582
  status: "succeeded",
575
583
  output: { text: JSON.stringify(i.r.object) },
576
584
  costMicroUsd: a
577
- }), yield* u(), i.r.object;
585
+ }), yield* d(), i.r.object;
578
586
  })
579
587
  }), m = t.memoryPrefix ? yield* t.memoryPrefix(a).pipe(x.catchAll(() => x.succeed(""))) : "", h = yield* Ce({
580
588
  system: m ? `${m}\n\n${Be(r)}` : Be(r),
@@ -584,9 +592,9 @@ var R = D("ai_flows", {
584
592
  value: S.Unknown
585
593
  }),
586
594
  tools: {
587
- run_agent: d,
588
- generate_media: f,
589
- pick_structured: p
595
+ run_agent: f,
596
+ generate_media: p,
597
+ pick_structured: ee
590
598
  },
591
599
  maxSteps: r.maxSteps,
592
600
  ...r.orchestrator?.model ? { provider: G(t, r.orchestrator.model) } : {}
@@ -600,36 +608,36 @@ var R = D("ai_flows", {
600
608
  return h.ok ? {
601
609
  object: h.r.object,
602
610
  planCostMicroUsd: K(h.r.usage, h.r.providerMetadata, r.orchestrator?.model),
603
- toolCostMicroUsd: s,
604
- steps: o
611
+ toolCostMicroUsd: c,
612
+ steps: s
605
613
  } : {
606
614
  object: {},
607
615
  planCostMicroUsd: 0,
608
- toolCostMicroUsd: s,
609
- steps: o,
616
+ toolCostMicroUsd: c,
617
+ steps: s,
610
618
  error: String(h.e.message ?? h.e)
611
619
  };
612
620
  })
613
- }), s = o.planCostMicroUsd + o.toolCostMicroUsd;
614
- return o.error ? (yield* q(e, n, {
621
+ }), c = s.planCostMicroUsd + s.toolCostMicroUsd;
622
+ return s.error ? (yield* q(e, n, {
615
623
  status: "failed",
616
- steps: o.steps,
617
- errorMessage: o.error,
624
+ steps: s.steps,
625
+ errorMessage: s.error,
618
626
  completedAt: W(),
619
- costMicroUsd: s
627
+ costMicroUsd: c
620
628
  }), {
621
629
  runId: n,
622
630
  status: "failed"
623
631
  }) : (yield* q(e, n, {
624
632
  status: "succeeded",
625
- steps: o.steps,
626
- output: o.object,
633
+ steps: s.steps,
634
+ output: s.object,
627
635
  completedAt: W(),
628
- costMicroUsd: s
636
+ costMicroUsd: c
629
637
  }), {
630
638
  runId: n,
631
639
  status: "succeeded",
632
- output: o.object
640
+ output: s.object
633
641
  });
634
642
  }), Ue = (e, t = {}) => (n, r) => x.gen(function* () {
635
643
  let { runId: r, flowRef: i, input: a } = n, o = yield* M({
@@ -652,17 +660,18 @@ var R = D("ai_flows", {
652
660
  runId: r,
653
661
  status: "failed"
654
662
  };
655
- let s = o.ir, c = (yield* x.promise(() => e.store.query(E(z).where(C("id", r)).descriptor)).pipe(x.catchAllCause(() => x.succeed([]))))[0]?.ownerId ?? null, l = s.mode === "agentic" ? yield* He(e, t, r, s, a, c) : yield* Ie(e, t, r, s, a, {
656
- ownerId: c,
663
+ let s = o.ir, c = (yield* x.promise(() => e.store.query(E(z).where(C("id", r)).descriptor)).pipe(x.catchAllCause(() => x.succeed([]))))[0], l = c?.ownerId ?? null, u = c?.tenantId ?? null, d = s.mode === "agentic" ? yield* He(e, t, r, s, a, l, u) : yield* Ie(e, t, r, s, a, {
664
+ ownerId: l,
665
+ tenantId: u,
657
666
  source: n.source
658
667
  });
659
- return l.status === "succeeded" && s.chain && (yield* Le(e, r, s, a, l.output ?? {}, n)), t.onEvent && (l.status === "succeeded" || l.status === "failed") && (yield* t.onEvent({
660
- kind: l.status,
668
+ return d.status === "succeeded" && s.chain && (yield* Le(e, r, s, a, d.output ?? {}, n)), t.onEvent && (d.status === "succeeded" || d.status === "failed") && (yield* t.onEvent({
669
+ kind: d.status,
661
670
  runId: r,
662
671
  flowName: s.name,
663
- ownerId: c,
672
+ ownerId: l,
664
673
  source: n.source
665
- }).pipe(x.catchAllCause(() => x.void))), l;
674
+ }).pipe(x.catchAllCause(() => x.void))), d;
666
675
  }), We = {
667
676
  text: (e) => ({
668
677
  type: "generate",
@@ -781,7 +790,8 @@ var R = D("ai_flows", {
781
790
  }));
782
791
  let o = yield* x.promise(() => e.put({
783
792
  data: a.data,
784
- mediaType: a.mediaType
793
+ mediaType: a.mediaType,
794
+ run: n.run
785
795
  }));
786
796
  return {
787
797
  url: o.url,
@@ -795,7 +805,8 @@ var R = D("ai_flows", {
795
805
  ...r ? { provider: i(r) } : {}
796
806
  }), a = yield* x.promise(() => e.put({
797
807
  data: t.audio.data,
798
- mediaType: t.audio.mediaType
808
+ mediaType: t.audio.mediaType,
809
+ run: n.run
799
810
  }));
800
811
  return {
801
812
  url: a.url,
@@ -808,10 +819,11 @@ var R = D("ai_flows", {
808
819
  ...r ? { provider: i(r) } : {},
809
820
  ...n.params ? { params: n.params } : {}
810
821
  }), a;
811
- if (t.video.url) a = e.ingestUrl ? yield* x.promise(() => e.ingestUrl(t.video.url)) : { url: t.video.url };
822
+ if (t.video.url) a = e.ingestUrl ? yield* x.promise(() => e.ingestUrl(t.video.url, n.run)) : { url: t.video.url };
812
823
  else if (t.video.data) a = yield* x.promise(() => e.put({
813
824
  data: t.video.data,
814
- mediaType: t.video.mediaType
825
+ mediaType: t.video.mediaType,
826
+ run: n.run
815
827
  }));
816
828
  else return yield* x.fail(new y({
817
829
  capability: "media:video",
@@ -962,4 +974,4 @@ var R = D("ai_flows", {
962
974
  });
963
975
  };
964
976
  //#endregion
965
- export { e as BriefField, t as ChainPending, _ as FLOW_RUN_WORKFLOW, n as FlowCadence, r as FlowChain, i as FlowMode, a as FlowStatus, o as FlowStep, v as HUMAN_RESPONSE_SIGNAL, s as HumanResponse, c as Json, l as MAX_STEPS_DEFAULT, u as MAX_STEPS_MAX, d as MAX_STEPS_MIN, f as MediaInputSlot, p as MediaInputs, m as Modality, h as ReviewMode, ee as ReviewOption, te as RunReview, ne as RunSource, re as RunStatus, ie as RunStep, ae as RunStepStatus, oe as StepType, se as Visibility, z as aiFlowRunsTable, lt as aiFlowsPlugin, R as aiFlowsTable, B as aiFlowsTables, Ue as buildFlowRunExecute, ge as cadenceMatches, nt as cancelFlow, ce as clampMaxSteps, je as clearRegistry, V as collectRefs, ot as createFlow, X as cronRequestId, qe as defineFlow, ct as deleteFlow, We as flowStep, it as getFlow, ke as getRegisteredFlow, H as interpolate, et as launchFlow, at as listFlows, Ae as listRegisteredFlows, Qe as makeMediaGenerator, he as nextRuns, g as normalizeFlow, Oe as registerFlow, J as resolveIr, rt as respondToFlow, tt as retryFlow, Ye as runCadenceTick, le as sanitizeOutputKey, De as unresolvedRefs, st as updateFlow, Ke as validateFlow };
977
+ export { e as BriefField, t as ChainPending, _ as FLOW_RUN_WORKFLOW, n as FlowCadence, r as FlowChain, i as FlowMode, a as FlowStatus, o as FlowStep, v as HUMAN_RESPONSE_SIGNAL, s as HumanResponse, c as Json, l as MAX_STEPS_DEFAULT, u as MAX_STEPS_MAX, d as MAX_STEPS_MIN, f as MediaInputSlot, p as MediaInputs, ee as Modality, m as ReviewMode, h as ReviewOption, te as RunReview, ne as RunSource, re as RunStatus, ie as RunStep, ae as RunStepStatus, oe as StepType, se as Visibility, z as aiFlowRunsTable, lt as aiFlowsPlugin, R as aiFlowsTable, B as aiFlowsTables, Ue as buildFlowRunExecute, ge as cadenceMatches, nt as cancelFlow, ce as clampMaxSteps, je as clearRegistry, V as collectRefs, ot as createFlow, X as cronRequestId, qe as defineFlow, ct as deleteFlow, We as flowStep, it as getFlow, ke as getRegisteredFlow, H as interpolate, et as launchFlow, at as listFlows, Ae as listRegisteredFlows, Qe as makeMediaGenerator, he as nextRuns, g as normalizeFlow, Oe as registerFlow, J as resolveIr, rt as respondToFlow, tt as retryFlow, Ye as runCadenceTick, le as sanitizeOutputKey, De as unresolvedRefs, st as updateFlow, Ke as validateFlow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-ai-flows",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "description": "AI Flows — durable multi-step AI pipelines (deterministic + agentic) with human-in-the-loop, chaining, and cadence. Author flows in code (defineFlow) or as data (visual editor rows); one engine runs both on @voltro/workflow durability, @voltro/ai generation, storage artifacts, and notifications.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -57,15 +57,15 @@
57
57
  "node": ">=24.0.0"
58
58
  },
59
59
  "dependencies": {
60
- "@voltro/ai": "0.11.0",
61
- "@voltro/database": "0.11.0",
62
- "@voltro/env": "0.11.0",
63
- "@voltro/plugin-audit": "0.11.0",
64
- "@voltro/plugin-multitenancy": "0.11.0",
65
- "@voltro/plugin-soft-delete": "0.11.0",
66
- "@voltro/protocol": "0.11.0",
67
- "@voltro/runtime": "0.11.0",
68
- "@voltro/workflow": "0.11.0"
60
+ "@voltro/ai": "0.11.2",
61
+ "@voltro/database": "0.11.2",
62
+ "@voltro/env": "0.11.2",
63
+ "@voltro/plugin-audit": "0.11.2",
64
+ "@voltro/plugin-multitenancy": "0.11.2",
65
+ "@voltro/plugin-soft-delete": "0.11.2",
66
+ "@voltro/protocol": "0.11.2",
67
+ "@voltro/runtime": "0.11.2",
68
+ "@voltro/workflow": "0.11.2"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "effect": "^3.21.4"