Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ai_kit/core",
"version": "1.4.0",
"version": "1.5.0",
"description": "",
"type": "module",
"main": "./dist/index.js",
Expand Down
55 changes: 55 additions & 0 deletions packages/core/src/workflows/kit/WorkflowKit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) {
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");
});
});
45 changes: 42 additions & 3 deletions packages/core/src/workflows/kit/WorkflowKit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ export class WorkflowKit {
dispatch?: WorkflowRunDispatchOptions,
): Promise<WorkflowRunResult<Output, any, any>>;
// Overload: world engine
run(
workflow: (...args: any[]) => unknown,
run<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: WorkflowRunDispatchOptions,
): Promise<WorldRunHandle>;
): Promise<WorldRunHandle<TResult>>;
// Implementation
async run(workflow: any, input: any, dispatch?: WorkflowRunDispatchOptions): Promise<unknown> {
const engine = dispatch?.engine ?? this.engine;
Expand All @@ -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<Output>(
workflow: Workflow<any, Output, any, any>,
options: WorkflowRunOptions<any, any, any>,
dispatch?: WorkflowRunDispatchOptions,
): Promise<Output>;
// Overload: world engine — awaits the durable run, returns its return value
runAndWait<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: WorkflowRunDispatchOptions,
): Promise<TResult>;
/**
* 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<unknown> {
const engine = dispatch?.engine ?? this.engine;
if (engine === "legacy") {
const result = await (workflow as Workflow<any, any, any, any>).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<WorldEngineAdapter> {
if (this.#adapter) return this.#adapter;
if (!this.world) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/workflows/kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type {
WorldConfig,
WorkflowKitOptions,
WorldRunHandle,
WorldRunStatus,
WorldEngineAdapter,
WorkflowWorldModule,
WorkflowRunDispatchOptions,
Expand Down
30 changes: 27 additions & 3 deletions packages/core/src/workflows/kit/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TResult = unknown> {
/** Identifiant du run durable. */
runId: string;
/** Sortie du run : attend la complétion ; rejette en cas d'échec/annulation. */
readonly returnValue: Promise<TResult>;
/** Statut courant du run. */
readonly status: Promise<WorldRunStatus>;
/** Le run existe-t-il dans le world. */
readonly exists: Promise<boolean>;
/** Annule le run. */
cancel(): Promise<void>;
/** Pass-through : autres membres du `Run` SDK (wakeUp, getReadable, timestamps…) disponibles au runtime. */
[key: string]: unknown;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-docs-server/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
48 changes: 41 additions & 7 deletions packages/mintlify-docs/en/api-reference/workflow-kit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,57 @@ run<Output>(
): Promise<WorkflowRunResult<Output>>;

// World overload — workflow is a "use workflow" function, input is the args array
run(
workflow: (...args: any[]) => unknown,
run<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: { engine?: WorkflowEngine },
): Promise<WorldRunHandle>;
): Promise<WorldRunHandle<TResult>>;
```

- **`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<Output>(
workflow: Workflow<any, Output>,
options: WorkflowRunOptions,
dispatch?: { engine?: WorkflowEngine },
): Promise<Output>;

// World overload → awaits the durable run and resolves with its return value
runAndWait<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: { engine?: WorkflowEngine },
): Promise<TResult>;
```

**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<TResult>`

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<TResult>` | Resolves with the output once complete; **rejects** if the run failed/was cancelled. |
| `status` | `Promise<WorldRunStatus>` | `"pending" \| "running" \| "completed" \| "failed" \| "cancelled"`. |
| `exists` | `Promise<boolean>` | Whether the run exists in the world. |
| `cancel()` | `Promise<void>` | Cancels the run. |

Reconstitute a handle later from a stored id with `getRun(runId)` from `workflow/api`.

## Example

Expand Down
22 changes: 22 additions & 0 deletions packages/mintlify-docs/en/workflows/world-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<Note>
`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.
</Note>

## Installing the world engine

The world engine lives in an optional package so the core stays lightweight:
Expand Down
48 changes: 41 additions & 7 deletions packages/mintlify-docs/fr/api-reference/workflow-kit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,57 @@ run<Output>(
): Promise<WorkflowRunResult<Output>>;

// Surcharge world — workflow est une fonction "use workflow", input est le tableau d'args
run(
workflow: (...args: any[]) => unknown,
run<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: { engine?: WorkflowEngine },
): Promise<WorldRunHandle>;
): Promise<WorldRunHandle<TResult>>;
```

- **`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<Output>(
workflow: Workflow<any, Output>,
options: WorkflowRunOptions,
dispatch?: { engine?: WorkflowEngine },
): Promise<Output>;

// Surcharge world → attend le run durable et se résout avec sa valeur de retour
runAndWait<TResult = unknown>(
workflow: (...args: any[]) => TResult | Promise<TResult>,
args: unknown[],
dispatch?: { engine?: WorkflowEngine },
): Promise<TResult>;
```

**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<TResult>`

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<TResult>` | Se résout avec la sortie une fois terminé ; **rejette** si le run a échoué ou été annulé. |
| `status` | `Promise<WorldRunStatus>` | `"pending" \| "running" \| "completed" \| "failed" \| "cancelled"`. |
| `exists` | `Promise<boolean>` | Indique si le run existe dans le world. |
| `cancel()` | `Promise<void>` | Annule le run. |

Reconstituez un handle plus tard à partir d'un identifiant stocké avec `getRun(runId)` depuis `workflow/api`.

## Exemple

Expand Down
22 changes: 22 additions & 0 deletions packages/mintlify-docs/fr/workflows/world-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

<Note>
`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.
</Note>

## Installer le moteur world

Le moteur world vit dans un package optionnel afin que le core reste léger :
Expand Down
2 changes: 1 addition & 1 deletion packages/workflow-world/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/workflow-world/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,7 +15,7 @@ interface WorldModuleLoaders {
fn: (...args: any[]) => unknown,
args: unknown[],
options?: { world?: SdkWorld },
) => Promise<WorldRunHandle>;
) => Promise<any>;
}>;
runtime: () => Promise<{ setWorld: (world: SdkWorld | undefined) => void }>;
}
Expand Down
Loading
Loading