diff --git a/packages/core/package.json b/packages/core/package.json index 6cc79c9..62e4fab 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ai_kit/core", - "version": "1.4.0", + "version": "1.5.0", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/src/workflows/kit/WorkflowKit.test.ts b/packages/core/src/workflows/kit/WorkflowKit.test.ts index dac2c8f..6a4f5f1 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.test.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.test.ts @@ -64,3 +64,58 @@ describe("WorkflowKit — dispatch run", () => { await expect(kit.start()).rejects.toThrow("@ai_kit/workflow-world"); }); }); + +describe("WorkflowKit — runAndWait", () => { + function worldKitWith(handle: Record) { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue(handle), + }; + __setWorkflowWorldLoader(async () => ({ createWorldAdapter: () => adapter })); + return new WorkflowKit({ engine: "world", world: { type: "postgres", url: "postgres://x" } }); + } + + it("world : résout avec returnValue du run", async () => { + const kit = worldKitWith({ + runId: "r_1", + returnValue: Promise.resolve({ ok: true }), + status: Promise.resolve("completed"), + exists: Promise.resolve(true), + cancel: vi.fn(), + }); + const out = await kit.runAndWait(async () => ({ ok: true }), ["a"]); + expect(out).toEqual({ ok: true }); + }); + + it("world : propage le rejet de returnValue (échec du run)", async () => { + const kit = worldKitWith({ + runId: "r_2", + // getter : crée la promesse rejetée seulement quand runAndWait la lit (pas d'unhandled rejection) + get returnValue() { + return Promise.reject(new Error("workflow failed")); + }, + status: Promise.resolve("failed"), + exists: Promise.resolve(true), + cancel: vi.fn(), + }); + await expect(kit.runAndWait(async () => 1, ["a"])).rejects.toThrow("workflow failed"); + }); + + it("legacy : résout avec result.result quand status=success", async () => { + const fakeWorkflow = { + run: vi.fn().mockResolvedValue({ status: "success", result: { total: 42 } }), + }; + const kit = new WorkflowKit(); + const out = await kit.runAndWait(fakeWorkflow as any, { inputData: {} }); + expect(out).toEqual({ total: 42 }); + }); + + it("legacy : throw quand status != success", async () => { + const fakeWorkflow = { + run: vi.fn().mockResolvedValue({ status: "failed", error: new Error("boom") }), + }; + const kit = new WorkflowKit(); + await expect(kit.runAndWait(fakeWorkflow as any, { inputData: {} })).rejects.toThrow("boom"); + }); +}); diff --git a/packages/core/src/workflows/kit/WorkflowKit.ts b/packages/core/src/workflows/kit/WorkflowKit.ts index ed7dcac..a7d8277 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.ts @@ -61,11 +61,11 @@ export class WorkflowKit { dispatch?: WorkflowRunDispatchOptions, ): Promise>; // Overload: world engine - run( - workflow: (...args: any[]) => unknown, + run( + workflow: (...args: any[]) => TResult | Promise, args: unknown[], dispatch?: WorkflowRunDispatchOptions, - ): Promise; + ): Promise>; // Implementation async run(workflow: any, input: any, dispatch?: WorkflowRunDispatchOptions): Promise { const engine = dispatch?.engine ?? this.engine; @@ -76,6 +76,45 @@ export class WorkflowKit { return adapter.run(workflow, input as unknown[]); } + // Overload: legacy engine — returns the workflow output, throws on non-success + runAndWait( + workflow: Workflow, + options: WorkflowRunOptions, + dispatch?: WorkflowRunDispatchOptions, + ): Promise; + // Overload: world engine — awaits the durable run, returns its return value + runAndWait( + workflow: (...args: any[]) => TResult | Promise, + args: unknown[], + dispatch?: WorkflowRunDispatchOptions, + ): Promise; + /** + * Runs a workflow and resolves with its output (synchronous-style), regardless + * of engine. Throws if the run does not succeed: + * - legacy: throws when the result status is not "success" (with the run error); + * - world: rejects via the SDK (`WorkflowRunFailedError` / `WorkflowRunCancelledError`). + */ + async runAndWait( + workflow: any, + input: any, + dispatch?: WorkflowRunDispatchOptions, + ): Promise { + const engine = dispatch?.engine ?? this.engine; + if (engine === "legacy") { + const result = await (workflow as Workflow).run(input); + if (result.status !== "success") { + if (result.error instanceof Error) throw result.error; + throw new Error( + `WorkflowKit: workflow run finished with status '${result.status}'`, + ); + } + return result.result; + } + const adapter = await this.#ensureAdapter(); + const handle = await adapter.run(workflow, input as unknown[]); + return handle.returnValue; + } + async #ensureAdapter(): Promise { if (this.#adapter) return this.#adapter; if (!this.world) { diff --git a/packages/core/src/workflows/kit/index.ts b/packages/core/src/workflows/kit/index.ts index aba6a4d..2ea7a3b 100644 --- a/packages/core/src/workflows/kit/index.ts +++ b/packages/core/src/workflows/kit/index.ts @@ -4,6 +4,7 @@ export type { WorldConfig, WorkflowKitOptions, WorldRunHandle, + WorldRunStatus, WorldEngineAdapter, WorkflowWorldModule, WorkflowRunDispatchOptions, diff --git a/packages/core/src/workflows/kit/types.ts b/packages/core/src/workflows/kit/types.ts index 086d2ab..041605d 100644 --- a/packages/core/src/workflows/kit/types.ts +++ b/packages/core/src/workflows/kit/types.ts @@ -22,9 +22,33 @@ export interface WorkflowKitOptions { world?: WorldConfig; } -/** Handle opaque renvoyé par le moteur world (pass-through du SDK Vercel). */ -export interface WorldRunHandle { - runId?: string; +/** Statut d'un run world (SDK Vercel). */ +export type WorldRunStatus = + | "pending" + | "running" + | "completed" + | "failed" + | "cancelled"; + +/** + * Handle d'un run "world" (pass-through du `Run` du SDK Vercel). + * + * `returnValue` poll jusqu'à la complétion du run : il **résout** avec la sortie + * du workflow, ou **rejette** (`WorkflowRunFailedError` / `WorkflowRunCancelledError`) + * si le run échoue ou est annulé. + */ +export interface WorldRunHandle { + /** Identifiant du run durable. */ + runId: string; + /** Sortie du run : attend la complétion ; rejette en cas d'échec/annulation. */ + readonly returnValue: Promise; + /** Statut courant du run. */ + readonly status: Promise; + /** Le run existe-t-il dans le world. */ + readonly exists: Promise; + /** Annule le run. */ + cancel(): Promise; + /** Pass-through : autres membres du `Run` SDK (wakeUp, getReadable, timestamps…) disponibles au runtime. */ [key: string]: unknown; } diff --git a/packages/mcp-docs-server/package.json b/packages/mcp-docs-server/package.json index 7bcf244..11baa72 100644 --- a/packages/mcp-docs-server/package.json +++ b/packages/mcp-docs-server/package.json @@ -1,6 +1,6 @@ { "name": "@ai_kit/mcp-docs", - "version": "1.0.7", + "version": "1.0.8", "description": "MCP server exposing AI Kit documentation", "type": "module", "main": "./dist/index.js", diff --git a/packages/mintlify-docs/en/api-reference/workflow-kit.mdx b/packages/mintlify-docs/en/api-reference/workflow-kit.mdx index d43c71f..c20a6ee 100644 --- a/packages/mintlify-docs/en/api-reference/workflow-kit.mdx +++ b/packages/mintlify-docs/en/api-reference/workflow-kit.mdx @@ -86,23 +86,57 @@ run( ): Promise>; // World overload — workflow is a "use workflow" function, input is the args array -run( - workflow: (...args: any[]) => unknown, +run( + workflow: (...args: any[]) => TResult | Promise, args: unknown[], dispatch?: { engine?: WorkflowEngine }, -): Promise; +): Promise>; ``` - **`legacy`** → delegates to `Workflow.run(options)` and returns a [`WorkflowRunResult`](/en/api-reference/workflow). -- **`world`** → delegates to the SDK's `start(fn, args)` and returns a `WorldRunHandle`. +- **`world`** → delegates to the SDK's `start(fn, args)` and returns a `WorldRunHandle` (durable; the output is **not** returned directly — see `runAndWait` / `returnValue`). -### `WorldRunHandle` +### `runAndWait(workflow, input, dispatch?)` -The opaque handle returned by a world run (pass-through of the SDK `Run`): +Runs a workflow and resolves with **its output**, synchronous-style, regardless of engine — the closest match to the legacy `await workflow.run()` then `.result`. -| Property | Type | Description | +```ts +// Legacy overload → resolves with the workflow Output +runAndWait( + workflow: Workflow, + options: WorkflowRunOptions, + dispatch?: { engine?: WorkflowEngine }, +): Promise; + +// World overload → awaits the durable run and resolves with its return value +runAndWait( + workflow: (...args: any[]) => TResult | Promise, + args: unknown[], + dispatch?: { engine?: WorkflowEngine }, +): Promise; +``` + +**Throws on failure** (so wrap it where legacy code read `result.status`): +- `legacy` → throws when the result status is not `"success"` (with the run error); +- `world` → rejects via the SDK (`WorkflowRunFailedError` / `WorkflowRunCancelledError`). + +```ts +const report = await kit.runAndWait(reportWorkflow, [input]); // output directly +``` + +### `WorldRunHandle` + +The handle returned by a world `run` (pass-through of the SDK `Run`). Use it when you want to start now and consume later: + +| Member | Type | Description | | --- | --- | --- | | `runId` | `string` | Identifier of the durable run. | +| `returnValue` | `Promise` | Resolves with the output once complete; **rejects** if the run failed/was cancelled. | +| `status` | `Promise` | `"pending" \| "running" \| "completed" \| "failed" \| "cancelled"`. | +| `exists` | `Promise` | Whether the run exists in the world. | +| `cancel()` | `Promise` | Cancels the run. | + +Reconstitute a handle later from a stored id with `getRun(runId)` from `workflow/api`. ## Example diff --git a/packages/mintlify-docs/en/workflows/world-engine.mdx b/packages/mintlify-docs/en/workflows/world-engine.mdx index 72c2589..174e63f 100644 --- a/packages/mintlify-docs/en/workflows/world-engine.mdx +++ b/packages/mintlify-docs/en/workflows/world-engine.mdx @@ -76,6 +76,28 @@ The constructor validates the config: `engine: "world"` without a `world` throws See the [`WorkflowKit` API reference](/en/api-reference/workflow-kit) for the full surface. +## Retrieving a run's result + +A world run is **durable and decoupled**: `kit.run(fn, args)` returns a `WorldRunHandle` (a pass-through of the SDK `Run`), **not the output directly**. To consume it synchronously — like the legacy `await workflow.run()` then `.result` — use `kit.runAndWait`: + +```ts +const report = await kit.runAndWait(reportWorkflow, [input]); // resolves with the output +``` + +Or keep the handle and await its `returnValue` (polls until the run completes): + +```ts +const run = await kit.run(reportWorkflow, [input]); +const report = await run.returnValue; // throws if the run failed / was cancelled +console.log(run.runId, await run.status); // also: run.exists, run.cancel() +``` + +`returnValue` **rejects on failure** (`WorkflowRunFailedError`) or cancellation (`WorkflowRunCancelledError`), carrying the original error — so wrap it in `try/catch` where legacy code inspected `result.status`. For deferred consumption, store `runId` and reconstitute the handle later with `getRun(runId)` from `workflow/api`. + + +`runAndWait` works for **both** engines (legacy returns the workflow output; world awaits `returnValue`) and throws on any non-success outcome — a clean drop-in for synchronously-consumed workflows being migrated. + + ## Installing the world engine The world engine lives in an optional package so the core stays lightweight: diff --git a/packages/mintlify-docs/fr/api-reference/workflow-kit.mdx b/packages/mintlify-docs/fr/api-reference/workflow-kit.mdx index 5684f63..247dd43 100644 --- a/packages/mintlify-docs/fr/api-reference/workflow-kit.mdx +++ b/packages/mintlify-docs/fr/api-reference/workflow-kit.mdx @@ -86,23 +86,57 @@ run( ): Promise>; // Surcharge world — workflow est une fonction "use workflow", input est le tableau d'args -run( - workflow: (...args: any[]) => unknown, +run( + workflow: (...args: any[]) => TResult | Promise, args: unknown[], dispatch?: { engine?: WorkflowEngine }, -): Promise; +): Promise>; ``` - **`legacy`** → délègue à `Workflow.run(options)` et retourne un [`WorkflowRunResult`](/fr/api-reference/workflow). -- **`world`** → délègue au `start(fn, args)` du SDK et retourne un `WorldRunHandle`. +- **`world`** → délègue au `start(fn, args)` du SDK et retourne un `WorldRunHandle` (durable ; la sortie n'est **pas** retournée directement — voir `runAndWait` / `returnValue`). -### `WorldRunHandle` +### `runAndWait(workflow, input, dispatch?)` -Le handle opaque retourné par un run world (pass-through du `Run` SDK) : +Exécute un workflow et se résout avec **sa sortie**, de manière synchrone, quel que soit le moteur — l'équivalent le plus proche du legacy `await workflow.run()` puis `.result`. -| Propriété | Type | Description | +```ts +// Surcharge legacy → se résout avec la sortie Output du workflow +runAndWait( + workflow: Workflow, + options: WorkflowRunOptions, + dispatch?: { engine?: WorkflowEngine }, +): Promise; + +// Surcharge world → attend le run durable et se résout avec sa valeur de retour +runAndWait( + workflow: (...args: any[]) => TResult | Promise, + args: unknown[], + dispatch?: { engine?: WorkflowEngine }, +): Promise; +``` + +**Lève une erreur en cas d'échec** (encapsulez là où le code legacy lisait `result.status`) : +- `legacy` → lève une erreur quand le statut du résultat n'est pas `"success"` (avec l'erreur du run) ; +- `world` → rejette via le SDK (`WorkflowRunFailedError` / `WorkflowRunCancelledError`). + +```ts +const report = await kit.runAndWait(reportWorkflow, [input]); // sortie directement +``` + +### `WorldRunHandle` + +Le handle retourné par un run world (pass-through du `Run` SDK). Utilisez-le quand vous voulez démarrer maintenant et consommer plus tard : + +| Membre | Type | Description | | --- | --- | --- | | `runId` | `string` | Identifiant du run durable. | +| `returnValue` | `Promise` | Se résout avec la sortie une fois terminé ; **rejette** si le run a échoué ou été annulé. | +| `status` | `Promise` | `"pending" \| "running" \| "completed" \| "failed" \| "cancelled"`. | +| `exists` | `Promise` | Indique si le run existe dans le world. | +| `cancel()` | `Promise` | Annule le run. | + +Reconstituez un handle plus tard à partir d'un identifiant stocké avec `getRun(runId)` depuis `workflow/api`. ## Exemple diff --git a/packages/mintlify-docs/fr/workflows/world-engine.mdx b/packages/mintlify-docs/fr/workflows/world-engine.mdx index a24462e..a81b194 100644 --- a/packages/mintlify-docs/fr/workflows/world-engine.mdx +++ b/packages/mintlify-docs/fr/workflows/world-engine.mdx @@ -76,6 +76,28 @@ Le constructeur valide la configuration : `engine: "world"` sans `world` lève u Consultez la [référence API `WorkflowKit`](/fr/api-reference/workflow-kit) pour la surface complète. +## Récupérer le résultat d'un run + +Un run world est **durable et découplé** : `kit.run(fn, args)` retourne un `WorldRunHandle` (pass-through du `Run` SDK), **pas la sortie directement**. Pour le consommer de manière synchrone — comme le legacy `await workflow.run()` puis `.result` — utilisez `kit.runAndWait` : + +```ts +const report = await kit.runAndWait(reportWorkflow, [input]); // se résout avec la sortie +``` + +Ou conservez le handle et attendez son `returnValue` (interroge jusqu'à la fin du run) : + +```ts +const run = await kit.run(reportWorkflow, [input]); +const report = await run.returnValue; // lève une erreur si le run a échoué / été annulé +console.log(run.runId, await run.status); // aussi : run.exists, run.cancel() +``` + +`returnValue` **rejette en cas d'échec** (`WorkflowRunFailedError`) ou d'annulation (`WorkflowRunCancelledError`), en transportant l'erreur d'origine — encapsulez-le dans un `try/catch` là où le code legacy inspectait `result.status`. Pour une consommation différée, stockez `runId` et reconstituez le handle plus tard avec `getRun(runId)` depuis `workflow/api`. + + +`runAndWait` fonctionne pour **les deux** moteurs (legacy retourne la sortie du workflow ; world attend `returnValue`) et lève une erreur pour tout résultat non réussi — un remplacement drop-in propre pour les workflows consommés de manière synchrone en cours de migration. + + ## Installer le moteur world Le moteur world vit dans un package optionnel afin que le core reste léger : diff --git a/packages/workflow-world/package.json b/packages/workflow-world/package.json index 9b0f839..3eae109 100644 --- a/packages/workflow-world/package.json +++ b/packages/workflow-world/package.json @@ -1,6 +1,6 @@ { "name": "@ai_kit/workflow-world", - "version": "0.1.0", + "version": "0.1.1", "description": "Vercel Workflow SDK world engine adapter for AI Kit (self-hosted Postgres/MongoDB).", "type": "module", "main": "./dist/index.js", diff --git a/packages/workflow-world/src/adapter.ts b/packages/workflow-world/src/adapter.ts index 7d07a85..2e9f8a6 100644 --- a/packages/workflow-world/src/adapter.ts +++ b/packages/workflow-world/src/adapter.ts @@ -1,4 +1,4 @@ -import type { WorldConfig, WorldEngineAdapter, WorldRunHandle } from "./contract.js"; +import type { WorldConfig, WorldEngineAdapter } from "./contract.js"; import { buildWorldOptions, WORLD_TARGETS } from "./worlds.js"; interface SdkWorld { @@ -15,7 +15,7 @@ interface WorldModuleLoaders { fn: (...args: any[]) => unknown, args: unknown[], options?: { world?: SdkWorld }, - ) => Promise; + ) => Promise; }>; runtime: () => Promise<{ setWorld: (world: SdkWorld | undefined) => void }>; } diff --git a/packages/workflow-world/src/contract.ts b/packages/workflow-world/src/contract.ts index 5d11da9..23bdb74 100644 --- a/packages/workflow-world/src/contract.ts +++ b/packages/workflow-world/src/contract.ts @@ -22,9 +22,24 @@ export interface WorldConfig { maxPoolSize?: number; } -/** Handle opaque renvoyé par le moteur world (pass-through du SDK Vercel). */ -export interface WorldRunHandle { - runId?: string; +/** Statut d'un run world (SDK Vercel). */ +export type WorldRunStatus = + | "pending" + | "running" + | "completed" + | "failed" + | "cancelled"; + +/** + * Handle d'un run "world" (pass-through du `Run` du SDK Vercel). + * `returnValue` résout avec la sortie, ou rejette si le run échoue/est annulé. + */ +export interface WorldRunHandle { + runId: string; + readonly returnValue: Promise; + readonly status: Promise; + readonly exists: Promise; + cancel(): Promise; [key: string]: unknown; }