@voltro/plugin-ai-flows 0.10.0 → 0.11.1
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 +134 -0
- package/dist/index.d.ts +81 -57
- package/dist/index.js +99 -87
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,140 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.11.1] — 2026-07-23
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- **@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.
|
|
47
|
+
|
|
48
|
+
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`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export default defineExecutor(getRoadmapsByYear, (input, ctx) => …)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
- **@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.
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
- **@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.
|
|
59
|
+
- **@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.
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
- **@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.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
- **@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).
|
|
66
|
+
|
|
67
|
+
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.
|
|
68
|
+
- **@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.
|
|
69
|
+
|
|
70
|
+
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`.
|
|
71
|
+
- **@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.
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
- **@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.
|
|
75
|
+
|
|
76
|
+
import { insertRow } from '@voltro/database' await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
- **@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.
|
|
80
|
+
- **@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.
|
|
81
|
+
- **@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.
|
|
82
|
+
- **@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.
|
|
83
|
+
- **@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.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **@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.
|
|
88
|
+
- **@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.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## [0.11.0] — 2026-07-22
|
|
93
|
+
|
|
94
|
+
### ⚠ BREAKING
|
|
95
|
+
|
|
96
|
+
- **@voltro/client, @voltro/web** — **@voltro/client** — `useSequence` steps gain **`covers`** and **`when`**, and the undo contract that was implicit is now written down.
|
|
97
|
+
|
|
98
|
+
**`covers` — overlapping undos.** Every succeeded step's undo runs, and the runner has no idea whether two of them reverse the same thing. Reported from a Jira rollback: `deleteJiraDraftTicket` deletes the issue *and* discards the draft, so the earlier `discardDraft` undo ran on something already gone. It worked only because discarding is idempotent — and **nothing said that was load-bearing**. For a refund or a cancellation email the double-run is a defect, not a nuisance.
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
.step('draft', createDraft, { undo: discardDraft })
|
|
102
|
+
.step('jira', createTicket, { undo: deleteJiraDraftTicket, covers: ['draft'] })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`covers` is static rather than a runtime signal on purpose: "this reverse also reverses that one" is a property of the operation, legible where it is defined and checkable against the step names in scope.
|
|
106
|
+
|
|
107
|
+
**When the covering undo FAILS**, the covered steps are neither run nor claimed. Whether the cascade got that far is genuinely unknown — running the covered undo risks the double reverse, skipping it risks an orphan — so both guesses are refused and the steps come back in `compensationUncertain` with the step that was supposed to cover them. Same principle as never compensating the step that failed: surface the ambiguity, don't resolve it by assumption.
|
|
108
|
+
|
|
109
|
+
**`when` — one optional step.** 13 multi-await blocks in one app; only 5 could migrate. Several of the rest were linear *except* for one conditional step ("schedule the summary only if the set changed", `if (assigneeKey) assign else unassign`) and fell back to `try/catch` entirely, though 80% of the flow was a clean pipeline. An optional step is a different shape from a loop, and the scoping was treating them the same.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
.step('summary', (c) => scheduleSummary.run({ id: c.save.id }), { when: (c) => c.save.changed })
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A skipped step contributes `undefined` to the context — the overload says so, so a later step has to acknowledge it — and gets no undo, since it had no effect to reverse. Loops and real branches still keep their `try/catch`; this deliberately does not widen to them.
|
|
116
|
+
|
|
117
|
+
**The break:** `StepOptions` gained a type parameter — `StepOptions<Result>` became `StepOptions<Ctx, Result>`, because `covers` and `when` both need to see the accumulated context (`covers` is checked against the step names in scope; `when` receives it). Call sites that pass an object literal to `.step()` are unaffected — the type is inferred — but anyone who *named* the type explicitly gets a compile error. Filed BREAKING rather than Added for the same reason a widened union is: the test is "can this turn code that compiled into code that does not", and it can. `@voltro/web` is listed because it re-exports the client surface — the third time in this round that coupling has decided where a change lands.
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
|
|
121
|
+
- **@voltro/cli** — **Repo gate** — a new `Claimed-wiring check` (`scripts/check-claimed-wirings.mjs`, wired into CI and therefore into `pnpm gate`): a doc comment that says something wires a symbol up must be telling the truth.
|
|
122
|
+
|
|
123
|
+
`setSystemStoreHandle`'s comment read *"Process-wide handle, registered by the runtime boot (dev.ts / start.ts)"*. Nothing registered it, in either path, through an entire release — so `runAsSystem` threw for every consumer, and the comment was the only evidence anyone had that it should work. The shape is not rare: a comment gets written when the wiring is planned, the wiring gets deferred, and the comment never finds out. It then reads as documentation of behaviour rather than of intention, and the more confidently it is phrased the less likely anyone is to check it.
|
|
124
|
+
|
|
125
|
+
Two things it does that the obvious version does not, both learned by watching it report the bug as clean:
|
|
126
|
+
|
|
127
|
+
- **it counts real call expressions, not text.** The first version matched regexes and found `setSystemStoreHandle({ … })` inside `runAsSystem`'s own error-message string — a text match cannot tell a call from a sentence about a call. (Precisely the defect fixed in the hand-roll detector one commit earlier, repeated one file later.) - **it checks the NAMED caller, not any caller.** The second version asked "does anything call this"; `@voltro/testing` calls it from a test harness, so the bug read clean again. A claim that the runtime boot registers something is not satisfied by a test helper registering it.
|
|
128
|
+
|
|
129
|
+
Verified the only way this kind of check can be: by removing the wiring and confirming it fails, with the diagnosis that would have saved the original investigation — `called by: packages/testing/src/testContext.ts ← none of these is the boot`.
|
|
130
|
+
|
|
131
|
+
### Fixed
|
|
132
|
+
|
|
133
|
+
- **@voltro/database, @voltro/runtime, @voltro/sql-postgres, @voltro/sql-mysql, @voltro/sql-sqlite, @voltro/sql-mssql** — **@voltro/database + every store** — a row inserted into a table that declares an `id()` scheme now gets one **at the store**, not only when the caller happened to go through `wrapStoreWithMixinBehaviour`.
|
|
134
|
+
|
|
135
|
+
Id generation was sitting one layer too high. It is the single stamped field that needs no subject — the scheme is a property of the declared table — yet it lived in the subject-aware wrapper. So every insert through an unwrapped store reached the database with `id: null`:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
null value in column "id" of relation "_voltro_seeds" violates not-null constraint
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
That was a real shipped bug in the seed ledger (the runner holds the raw store), and a survey found the same shape waiting elsewhere: `plugin-rbac/userRoleStore.ts`, `plugin-governance/consent.ts`, and `plugin-sso-saml/saml-cache.ts` all insert without an id into tables that declare one. Whether any of them broke came down to how their caller happened to obtain its store — and *"depends on how the caller obtained its store"* is not a contract, it is a coin flip with a NOT NULL constraint on the other side.
|
|
142
|
+
|
|
143
|
+
`stampGeneratedId` now runs in every store's insert path (all four dialects plus the in-memory store, at the private `executeInsert` / `executeInsertMany` / `executeInsertIgnore` choke points that every public and namespace-view path funnels through). It preserves the semantics the wrapper had: an explicit id is never overwritten, a `numeric` scheme deletes the key so the dialect's SERIAL fires, and an unregistered table is passed through untouched rather than guessed at.
|
|
144
|
+
|
|
145
|
+
The wrapper still stamps — it holds the schema registry and does the subject-derived fields in the same pass — and now finds the id already set. That is a floor, not a second implementation of a rule: the invariant is "a row that reaches the database has an id when its table declares a generating scheme", and only the store can promise that for *every* caller, including a `DataStore` someone implemented themselves.
|
|
146
|
+
- **@voltro/cli** — **Repo tests** — the liveness backstop for real-listener / real-child-process suites goes from 60s to 120s, in `vitest.config.ts` and the CI flag that overrides it.
|
|
147
|
+
|
|
148
|
+
Worth stating plainly what this number is, because raising a timeout is the classic way to bury a problem: it asserts nothing about performance. A real `serveApi` boot is ~0.2s in isolation. The value is pure headroom against residual starvation that the four existing mitigations — the unit/integration project split, group ordering, `fileParallelism: false`, per-package mssql databases — cannot reach, because the remaining contention is turbo running two *packages* concurrently alongside the docker stack.
|
|
149
|
+
|
|
150
|
+
60s tripped twice in one session, on two different files (`connectionServe.test.ts`, then `serveApi.test.ts`), each passing in ~2s alone. Different file each time, always in the same family, never reproducible in isolation: that is the signature of starvation rather than of a slow test, and it cost two full gate runs.
|
|
151
|
+
|
|
152
|
+
What would not be honest is treating a green run afterwards as evidence the contention is gone. It is not. If a third file trips this, the answer is to stop running two heavy packages concurrently — not to raise it again.
|
|
153
|
+
- **@voltro/cli, @voltro/database** — **@voltro/cli, @voltro/database** — two fixes to the 0.10.0 seed ledger, both reported from its first real use, both mine.
|
|
154
|
+
|
|
155
|
+
**The ledger never wrote.** `store.insert('_voltro_seeds', …)` ran against the RAW store, and auto-id lives in `wrapStoreWithMixinBehaviour` — so the row reached postgres with `id: null` and died on the NOT NULL constraint. Every boot reported `ran=1 skipped=0` regardless of fingerprint: the feature shipped doing nothing. The ledger now stamps its own id, derived from `_voltroSeedsTable`'s declared scheme rather than a hardcoded prefix, so it no longer depends on how a caller happens to have wrapped its store.
|
|
156
|
+
|
|
157
|
+
**Worse than the bug was the logging.** I put the failure on `debug` and swallowed it, reasoning that "it degrades to re-running, which is only a performance regression". That reasoning is exactly what made it undiagnosable: a silently unwritten ledger looks identical to a working one whose seeds all changed — no symptom, nothing to grep, and the reporter had to read the error out of a debug stream to find it. Both the write and the read path now warn, naming the table and the consequence.
|
|
158
|
+
|
|
159
|
+
**And the reason it shipped:** the test fake *invented an id* when the row lacked one, making it more permissive than any database. A fake that supplies what the subject under test forgot is not a test — it is the subject testing itself. It now rejects an id-less insert with the real constraint's message; reintroducing the bug fails five cases.
|
|
160
|
+
|
|
161
|
+
**A seed step could not reach a usable store.** `SeedStore` exposed only `query`/`insert`/`update`/`delete`, and `query` took just `{ table, predicate }`. A restore of 1361 rows across 167 tables, multi-pass for FK order, needs an idempotent insert and a read it can page — and `upsertByUnique` is neither: a read per row, and it overwrites what it finds, which is wrong whenever the live row is newer than the snapshot. `SeedStore` now carries **`insertIgnore`** (one statement per row, native on every dialect) and a full descriptor read (`order` / `take` / `skip` / `projection`).
|
|
162
|
+
|
|
163
|
+
Seed reads are unscoped and include soft-deleted rows *by construction* — a seed runs at boot with no request and therefore no subject, so nothing applies a tenant filter or the `deletedAt IS NULL` predicate. That is now documented on the type rather than left to be discovered, since the absence of `.unscoped()` / `.withDeleted()` reads as a missing feature until you know why they cannot exist here.
|
|
164
|
+
|
|
165
|
+
*(`apiSurface: compatible`: the golden churn in `@voltro/database` is two `(undocumented)` markers disappearing because `SeedStore` and its new member gained TSDoc — a comment cannot break a caller. The added `insertIgnore` member is additive for consumers, which is everyone: a `SeedStore` is what `ctx.store` IS, handed to you by the runner. Nobody constructs one, so nobody can be missing a member.)*
|
|
166
|
+
- **@voltro/cli** — **@voltro/cli** — `runAsSystem` now works. The process-wide system store is registered at boot by **both** `voltro dev` and `voltro serve`; until now neither did, so every call threw `no data store available — register one at boot via setSystemStoreHandle`.
|
|
167
|
+
|
|
168
|
+
`setSystemStoreHandle`'s own doc comment reads "registered by the runtime boot (dev.ts / start.ts)". It describes wiring that was never written: the only callers in the repo were its unit test and a note in `@voltro/testing`. So the failure was not a lifecycle-ordering subtlety — the handle was never set at any point, in any command, and `runAsSystem` was unusable for every consumer.
|
|
169
|
+
|
|
170
|
+
Surfaced by someone reporting it as "not registered *yet* at seed time", which implied it worked later. Checking that framing rather than the symptom is what turned a scheduling question into a missing-wiring one. It is registered before the seed runner in dev, since seeds are the earliest thing that can plausibly want it.
|
|
171
|
+
|
|
172
|
+
Same class as the seed lifecycle table and the `@voltro/web` re-export: a documented behaviour with nothing behind it, where the doc is the only evidence anyone has.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
42
176
|
## [0.10.0] — 2026-07-22
|
|
43
177
|
|
|
44
178
|
### ⚠ 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
|
|
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),
|
|
104
|
-
if (
|
|
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:
|
|
107
|
-
message: `step "${n.id}" references unresolved {{${
|
|
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
|
|
190
|
+
let a = n.model ?? t.defaultModels?.[r], o = yield* t.generateMedia({
|
|
191
191
|
modality: r,
|
|
192
|
-
...
|
|
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
|
|
203
|
-
value:
|
|
204
|
-
url:
|
|
205
|
-
...
|
|
206
|
-
costMicroUsd:
|
|
207
|
-
} : { error:
|
|
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
|
|
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
|
|
431
|
-
let t =
|
|
432
|
-
return
|
|
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
|
-
},
|
|
439
|
-
|
|
440
|
-
...
|
|
442
|
+
}, u = (e, t) => {
|
|
443
|
+
s[e] = {
|
|
444
|
+
...s[e],
|
|
441
445
|
completedAt: W(),
|
|
442
446
|
...t
|
|
443
447
|
};
|
|
444
|
-
},
|
|
445
|
-
steps:
|
|
446
|
-
currentStep:
|
|
447
|
-
costMicroUsd:
|
|
448
|
-
}),
|
|
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 =
|
|
460
|
+
let r = l({
|
|
457
461
|
title: `agent:${e}`,
|
|
458
462
|
type: "agent"
|
|
459
463
|
});
|
|
460
|
-
if (yield*
|
|
464
|
+
if (yield* d(), !t.resolveAgent) return u(r, {
|
|
461
465
|
status: "failed",
|
|
462
466
|
errorMessage: "no agent resolver configured"
|
|
463
|
-
}), yield*
|
|
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
|
|
475
|
+
if (!i.ok) return u(r, {
|
|
472
476
|
status: "failed",
|
|
473
477
|
errorMessage: i.e.message
|
|
474
|
-
}), yield*
|
|
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
|
|
492
|
+
return u(r, {
|
|
489
493
|
status: "failed",
|
|
490
494
|
errorMessage: e
|
|
491
|
-
}), yield*
|
|
495
|
+
}), yield* d(), `ERROR: ${e}`;
|
|
492
496
|
}
|
|
493
497
|
let o = K(a.r.usage, a.r.providerMetadata, i.a.model);
|
|
494
|
-
return
|
|
498
|
+
return c += o, u(r, {
|
|
495
499
|
status: "succeeded",
|
|
496
500
|
output: { text: a.r.text },
|
|
497
501
|
costMicroUsd: o
|
|
498
|
-
}), yield*
|
|
502
|
+
}), yield* d(), a.r.text;
|
|
499
503
|
})
|
|
500
|
-
}),
|
|
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:
|
|
509
|
-
let
|
|
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*
|
|
517
|
+
if (yield* d(), !t.generateMedia) return u(a, {
|
|
514
518
|
status: "failed",
|
|
515
519
|
errorMessage: `no media generator for ${e}`
|
|
516
|
-
}), yield*
|
|
517
|
-
let
|
|
520
|
+
}), yield* d(), `ERROR: no media generator configured for ${e}`;
|
|
521
|
+
let s = yield* t.generateMedia({
|
|
518
522
|
modality: e,
|
|
519
|
-
prompt:
|
|
520
|
-
...
|
|
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
|
|
536
|
+
return s.ok ? (c += s.r.costMicroUsd ?? 0, u(a, {
|
|
529
537
|
status: "succeeded",
|
|
530
|
-
url:
|
|
531
|
-
...
|
|
532
|
-
costMicroUsd:
|
|
533
|
-
}), yield*
|
|
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:
|
|
536
|
-
}), yield*
|
|
543
|
+
errorMessage: s.e.message
|
|
544
|
+
}), yield* d(), `ERROR: ${s.e.message}`);
|
|
537
545
|
})
|
|
538
|
-
}),
|
|
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 =
|
|
554
|
+
let r = l({
|
|
547
555
|
title: "structured",
|
|
548
556
|
type: "structured"
|
|
549
557
|
});
|
|
550
|
-
yield*
|
|
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
|
|
575
|
+
return u(r, {
|
|
568
576
|
status: "failed",
|
|
569
577
|
errorMessage: e
|
|
570
|
-
}), yield*
|
|
578
|
+
}), yield* d(), { error: e };
|
|
571
579
|
}
|
|
572
580
|
let a = K(i.r.usage, i.r.providerMetadata, n);
|
|
573
|
-
return
|
|
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*
|
|
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:
|
|
588
|
-
generate_media:
|
|
589
|
-
pick_structured:
|
|
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:
|
|
604
|
-
steps:
|
|
611
|
+
toolCostMicroUsd: c,
|
|
612
|
+
steps: s
|
|
605
613
|
} : {
|
|
606
614
|
object: {},
|
|
607
615
|
planCostMicroUsd: 0,
|
|
608
|
-
toolCostMicroUsd:
|
|
609
|
-
steps:
|
|
616
|
+
toolCostMicroUsd: c,
|
|
617
|
+
steps: s,
|
|
610
618
|
error: String(h.e.message ?? h.e)
|
|
611
619
|
};
|
|
612
620
|
})
|
|
613
|
-
}),
|
|
614
|
-
return
|
|
621
|
+
}), c = s.planCostMicroUsd + s.toolCostMicroUsd;
|
|
622
|
+
return s.error ? (yield* q(e, n, {
|
|
615
623
|
status: "failed",
|
|
616
|
-
steps:
|
|
617
|
-
errorMessage:
|
|
624
|
+
steps: s.steps,
|
|
625
|
+
errorMessage: s.error,
|
|
618
626
|
completedAt: W(),
|
|
619
|
-
costMicroUsd:
|
|
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:
|
|
626
|
-
output:
|
|
633
|
+
steps: s.steps,
|
|
634
|
+
output: s.object,
|
|
627
635
|
completedAt: W(),
|
|
628
|
-
costMicroUsd:
|
|
636
|
+
costMicroUsd: c
|
|
629
637
|
}), {
|
|
630
638
|
runId: n,
|
|
631
639
|
status: "succeeded",
|
|
632
|
-
output:
|
|
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,
|
|
656
|
-
ownerId:
|
|
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
|
|
660
|
-
kind:
|
|
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:
|
|
672
|
+
ownerId: l,
|
|
664
673
|
source: n.source
|
|
665
|
-
}).pipe(x.catchAllCause(() => x.void))),
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.11.1",
|
|
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.
|
|
61
|
-
"@voltro/database": "0.
|
|
62
|
-
"@voltro/env": "0.
|
|
63
|
-
"@voltro/plugin-audit": "0.
|
|
64
|
-
"@voltro/plugin-multitenancy": "0.
|
|
65
|
-
"@voltro/plugin-soft-delete": "0.
|
|
66
|
-
"@voltro/protocol": "0.
|
|
67
|
-
"@voltro/runtime": "0.
|
|
68
|
-
"@voltro/workflow": "0.
|
|
60
|
+
"@voltro/ai": "0.11.1",
|
|
61
|
+
"@voltro/database": "0.11.1",
|
|
62
|
+
"@voltro/env": "0.11.1",
|
|
63
|
+
"@voltro/plugin-audit": "0.11.1",
|
|
64
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
65
|
+
"@voltro/plugin-soft-delete": "0.11.1",
|
|
66
|
+
"@voltro/protocol": "0.11.1",
|
|
67
|
+
"@voltro/runtime": "0.11.1",
|
|
68
|
+
"@voltro/workflow": "0.11.1"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"effect": "^3.21.4"
|