From 5e92319291863d805159abb0235ee8493846eaa8 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:20:18 +0200 Subject: [PATCH 1/9] =?UTF-8?q?docs(workflow-kit):=20spec=20=E2=80=94=20ad?= =?UTF-8?q?apter/world=20injectables=20pour=20fix=20tracing=20Nitro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Conçoit l'injection d'un WorldEngineAdapter pré-construit dans WorkflowKit + un loader 'module' sur createWorldAdapter, pour que les imports de la chaîne world (@ai_kit/workflow-world, @workflow/world-postgres) vivent en littéraux dans le code tracé de l'app hôte → inclus dans .output sans traceInclude. Rétrocompatible, dépendances optionnelles préservées. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-workflow-kit-injectable-adapter-design.md | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-02-workflow-kit-injectable-adapter-design.md diff --git a/docs/superpowers/specs/2026-06-02-workflow-kit-injectable-adapter-design.md b/docs/superpowers/specs/2026-06-02-workflow-kit-injectable-adapter-design.md new file mode 100644 index 0000000..852c04f --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-workflow-kit-injectable-adapter-design.md @@ -0,0 +1,245 @@ +# WorkflowKit — adapter/world injectables (fix tracing Nitro + moins de câblage) + +**Date :** 2026-06-02 +**Statut :** design validé, en attente de relecture avant plan d'implémentation +**Packages touchés :** `@ai_kit/core` (`WorkflowKit`), `@ai_kit/workflow-world` (`createWorldAdapter`) + +## 1. Problème + +Une app hôte (Nuxt/Nitro, preset `node-server`) qui utilise le moteur `world` se déploie +via Docker en ne copiant que `.output`. Or deux dépendances n'arrivent **pas** dans +`.output` parce qu'elles sont atteintes par des `import()` **à argument variable**, que +le traceur de Nitro (`@vercel/nft`) ne sait pas suivre : + +| Hop | Site d'import | Argument | Tracé par nft ? | +|-----|---------------|----------|-----------------| +| 1 | `@ai_kit/core` → `WorkflowKit.ts:22` : `import(WORLD_PACKAGE)` | variable (`const WORLD_PACKAGE: string`) | ❌ | +| 2 | `@ai_kit/workflow-world` → `adapter.ts:25-26` : `import(WORLD_TARGETS[type])` | variable | ❌ | + +En local/monorepo tout résout via `node_modules` (pnpm), donc le problème est invisible. +En Docker (`.output` seul), `@ai_kit/workflow-world` et `@workflow/world-postgres` sont +absents → l'app casse au runtime. + +Le contournement `traceInclude: ['@workflow/world-postgres']` **échoue** : l'option Nitro +attend des **chemins de fichiers**, pas des specifiers de package (le nom est interprété +comme chemin relatif → build cassé). + +### Variables à variable = volontaires + +Ces imports sont à argument variable **exprès** : `@ai_kit/workflow-world` et les worlds +SDK (`@workflow/world-postgres`, `@workflow-worlds/mongodb`) sont des dépendances +**optionnelles**. Un littéral forcerait `tsc`/Node à les résoudre même pour un utilisateur +`legacy` qui ne les installe pas. On ne peut donc pas se contenter de « mettre un +littéral » dans la lib : il faut déplacer le littéral dans le **code tracé du +consommateur**. + +### Théorie « duplication de module » : écartée + +Le wiring actuel de l'app hôte met `workflow`, `@workflow/core`, `@workflow/world-postgres`, +`@ai_kit/workflow-world` en `nitro.externals.external` pour « forcer une instance unique » +et éviter que `setWorld` (adapter) et `getWorld` (entrypoint) divergent. Vérification faite +dans `@workflow/core@4.3.1` : l'état runtime n'est **pas** stocké en variable de module mais +sur `globalThis`, via le registre **global** de symboles : + +- world courant : `globalThis[Symbol.for('@workflow/world//cache')]` (`runtime/world.js`) +- registre des steps : `globalThis[Symbol.for('@workflow/core//registeredSteps')]` avec + `??=` qui fait converger toutes les copies sur la 1ʳᵉ Map créée (`private.js:5-8`) + +Donc une duplication de bundle est **inoffensive** : toutes les copies lisent/écrivent le +même slot. Le hack `externals.external` n'est pas requis pour la correction ; sa +suppression est une vérification côté app hôte (hors périmètre de ce spec, voir §8). + +## 2. Objectif + +Permettre à l'app hôte d'amener `@ai_kit/workflow-world` **et** le world SDK dans `.output` +**sans** `traceInclude` ni hack, en faisant vivre tous les imports de la chaîne `world` +sous forme **statique/littérale dans le code tracé du consommateur**. Bénéfice secondaire : +moins de câblage fragile (plus d'env « magique », plus de `traceInclude`). + +### Critères d'acceptation + +1. **Build Docker** (`nitro preset node-server`) : après `nuxt build`, `.output` contient + `@ai_kit/workflow-world` ET `@workflow/world-postgres` (+ leurs deps statiques) **sans** + `traceInclude` ni entrée dans `externals.external` pour ces deux packages. +2. **Rétrocompatibilité** : le code existant `new WorkflowKit({ engine: 'world', world: {…} })` + continue de fonctionner à l'identique (chemin d'import dynamique conservé). +3. **Dépendances optionnelles préservées** : un utilisateur `legacy` qui n'installe ni + `@ai_kit/workflow-world` ni un world n'a aucune résolution forcée de ces packages. +4. **Tests** : unitaires verts sur les deux chemins (injecté + dynamique) ; un test prouve + que l'adapter injecté est utilisé sans toucher au loader dynamique. + +## 3. Design + +### 3.1 `@ai_kit/core` — `WorkflowKit` accepte un adapter injecté + +`WorkflowKitOptions` gagne un champ optionnel `adapter`. Quand il est fourni, `WorkflowKit` +l'utilise tel quel et **ne fait plus** `import('@ai_kit/workflow-world')` (hop 1 supprimé). + +```ts +// packages/core/src/workflows/kit/types.ts +export interface WorkflowKitOptions { + /** Moteur par défaut. Défaut : "legacy". */ + engine?: WorkflowEngine; + /** Config du world. Requis si engine === "world" ET adapter absent. */ + world?: WorldConfig; + /** + * Adapter world pré-construit (via `createWorldAdapter` de @ai_kit/workflow-world, + * importé statiquement par l'app hôte). Quand fourni, court-circuite l'import + * dynamique de @ai_kit/workflow-world — ce qui rend les deux packages traçables + * par le bundler depuis le code de l'app. Prioritaire sur `world`. + */ + adapter?: WorldEngineAdapter; +} +``` + +Comportement (`WorkflowKit.ts`) : + +- Constructeur : si `options.adapter` présent → `this.#adapter = options.adapter`. + Validation `engine === 'world'` : exiger `world` **ou** `adapter` (sinon throw inchangé). + La validation du `world.type` ne s'applique que si `world` est fourni. +- `#ensureAdapter()` : si `this.#adapter` déjà défini (cas injecté), le retourner + immédiatement — pas de `worldModuleLoader()`. Sinon, chemin dynamique actuel inchangé. +- `start()` / `stop()` / `run()` / `runAndWait()` : inchangés (passent par `#ensureAdapter()`). + +### 3.2 `@ai_kit/workflow-world` — `createWorldAdapter` accepte un loader de world + +La config de `createWorldAdapter` gagne `module?` : un loader fourni par le consommateur +qui remplace le loader dynamique interne (`loaders[type]`) pour le world. C'est la +promotion publique, **par config**, de la couture interne `__setWorldModuleLoaders` (qui +reste réservée aux tests). + +```ts +// packages/workflow-world/src/contract.ts +export interface WorldConfig { + type: WorldType; + url: string; + jobPrefix?: string; + workerConcurrency?: number; + maxPoolSize?: number; + /** + * Loader du module world, fourni par l'app hôte sous forme de littéral + * (`() => import('@workflow/world-postgres')`). Quand fourni, il remplace + * l'import dynamique interne : le littéral vit dans le code tracé de l'app, + * donc nft inclut le package dans `.output`. Doit exposer `createWorld`. + */ + module?: () => Promise<{ createWorld: (opts: Record) => unknown }>; +} +``` + +Dans `adapter.ts`, `loadWorldModule` utilise `config.module` en priorité : + +```ts +async function loadWorldModule(config: WorldConfig) { + const loader = config.module ?? loaders[config.type]; + try { + return await loader(); + } catch (err) { + if ((err as { code?: string }).code === "ERR_MODULE_NOT_FOUND") { + throw new Error( + `workflow-world: le module world '${WORLD_TARGETS[config.type]}' est introuvable. ` + + `Installe-le (pnpm add ${WORLD_TARGETS[config.type]}) ou fournis 'module'.`, + ); + } + throw err; + } +} +``` + +(Les loaders `api`/`runtime` restent des littéraux `import("workflow/api")` / +`import("workflow/runtime")` dans `adapter.ts` → déjà traçables, inchangés.) + +`@ai_kit/workflow-world` exporte déjà `createWorldAdapter` (`index.ts`) — rien à ajouter +côté exports. + +### 3.3 Data flow (chemin injecté, cible) + +``` +app/server/utils/workflow-kit.ts ← tout en imports statiques/littéraux + ├─ import { WorkflowKit } from '@ai_kit/core' (statique) + ├─ import { createWorldAdapter } from '@ai_kit/workflow-world' (statique → hop 1 tracé) + └─ createWorldAdapter({ type, url, module: () => import('@workflow/world-postgres') }) + (littéral → hop 2 tracé) + → new WorkflowKit({ engine: 'world', adapter }) + → kit.start() → adapter.start() → loadWorldModule(config) → config.module() + → setWorld(world) (globalThis) → world.start() + → kit.run()/runAndWait() → adapter.run() → workflow/api start(fn,args,{world}) +``` + +nft, en analysant `workflow-kit.ts` (fichier serveur tracé), voit les deux imports +littéraux et copie `@ai_kit/workflow-world` + `@workflow/world-postgres` (+ deps statiques) +dans `.output`. + +## 4. Migration de l'app hôte (LeRedacteurV2) + +`app/lrd-nuxt/server/utils/workflow-kit.ts`, branche `world` : + +```ts +import { WorkflowKit, type WorkflowEngine } from '@ai_kit/core' +import { createWorldAdapter } from '@ai_kit/workflow-world' + +// … +return new WorkflowKit({ + engine: 'world', + adapter: createWorldAdapter({ + type: 'postgres', + url: WORKFLOW_POSTGRES_URL, + module: () => import('@workflow/world-postgres'), + }), +}) +``` + +Puis : retirer `traceInclude` (jamais commité, déjà reverté) et tenter de retirer +`@workflow/world-postgres` + `@ai_kit/workflow-world` de `nitro.externals.external` +(§8 : à valider au build, indépendant de ce spec). Le plugin +`05.workflow-kit.server.ts` et `runners.ts` restent inchangés. + +## 5. Gestion d'erreurs + +- `module` fourni mais package absent → `loadWorldModule` relève le `ERR_MODULE_NOT_FOUND` + en message explicite (cf. §3.2), comme aujourd'hui pour le chemin dynamique. +- `engine: 'world'` sans `world` ni `adapter` → throw au constructeur (message clair). +- `adapter` fourni avec `engine: 'legacy'` → autorisé mais inerte (l'adapter n'est utilisé + que sur le chemin `world` de `run`/`runAndWait`/`start`/`stop`). Pas de throw (cohérent + avec le pattern « config world attachée même en legacy » de l'app hôte). + +## 6. Tests + +`@ai_kit/core` (`WorkflowKit.test.ts`) : +- `new WorkflowKit({ engine: 'world', adapter: fakeAdapter })` : `start/run/stop` + délèguent à `fakeAdapter` **sans** appeler `worldModuleLoader` (espionner le seam + `__setWorkflowWorldLoader` pour prouver qu'il n'est jamais invoqué). +- Validation : `engine: 'world'` sans `world` ni `adapter` → throw. +- Rétrocompat : `new WorkflowKit({ engine: 'world', world })` → chemin dynamique intact. + +`@ai_kit/workflow-world` (`adapter.test.ts`) : +- `createWorldAdapter({ type, url, module })` : `start()` appelle `config.module()` et + **pas** `loaders[type]` (mock des deux, vérifier lequel est appelé). +- `module` qui rejette `ERR_MODULE_NOT_FOUND` → message explicite. +- Sans `module` : comportement actuel (loaders dynamiques) inchangé. + +**Acceptation déploiement (manuel, hors CI)** : build Docker de LeRedacteurV2 ; vérifier +`ls .output/server/node_modules/@ai_kit/workflow-world` et `.../@workflow/world-postgres` ; +démarrer le conteneur, lancer un run `world`, confirmer l'exécution des steps (pas de +`StepNotRegistered`, pas de fallback world-local). + +## 7. Fichiers touchés + +- `packages/core/src/workflows/kit/types.ts` — `WorkflowKitOptions.adapter?` +- `packages/core/src/workflows/kit/WorkflowKit.ts` — init `#adapter` depuis options, + `#ensureAdapter` court-circuit, validation constructeur +- `packages/core/src/workflows/kit/WorkflowKit.test.ts` — tests chemin injecté +- `packages/workflow-world/src/contract.ts` — `WorldConfig.module?` +- `packages/workflow-world/src/adapter.ts` — `loadWorldModule(config)` priorise `config.module` +- `packages/workflow-world/src/adapter.test.ts` — tests `module` +- `packages/workflow-world/README.md` + doc world-engine — recette déploiement Nitro/Docker + (chemin injecté, ligne build `workflow/nitro`, plugin `start/stop`, plus de `traceInclude`) + +## 8. Hors périmètre + +- **Module Nuxt clé-en-main** (`@ai_kit/workflow-world/nuxt`) : explicitement écarté (surface + de maintenance pour un seul consommateur). +- **Suppression du hack `externals.external`** côté app hôte : recommandée (la théorie + duplication est écartée) mais à valider au build Docker ; ce n'est pas une modif de lib. +- **Auto-start paresseux** du worker : non retenu (le worker doit tourner dans le runtime de + l'entrypoint ; un plugin serveur explicite reste le modèle le plus clair). +- **Injection des loaders `api`/`runtime`** : inutile (déjà littéraux, déjà tracés). From 13219e82b4f74306997bfeca64de3a2ba843f678 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:29:31 +0200 Subject: [PATCH 2/9] =?UTF-8?q?docs(workflow-kit):=20plan=20d'impl=C3=A9me?= =?UTF-8?q?ntation=20=E2=80=94=20adapter/world=20injectables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...6-06-02-workflow-kit-injectable-adapter.md | 484 ++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-02-workflow-kit-injectable-adapter.md diff --git a/docs/superpowers/plans/2026-06-02-workflow-kit-injectable-adapter.md b/docs/superpowers/plans/2026-06-02-workflow-kit-injectable-adapter.md new file mode 100644 index 0000000..97f5711 --- /dev/null +++ b/docs/superpowers/plans/2026-06-02-workflow-kit-injectable-adapter.md @@ -0,0 +1,484 @@ +# WorkflowKit — adapter/world injectables — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Permettre à l'app hôte d'injecter un `WorldEngineAdapter` pré-construit (et le loader du world SDK) dans `WorkflowKit`, pour que les imports de la chaîne `world` soient des littéraux tracés par le bundler → `@ai_kit/workflow-world` et `@workflow/world-postgres` entrent dans `.output` sans `traceInclude`. + +**Architecture :** Deux changements rétrocompatibles. (1) `@ai_kit/workflow-world` : `createWorldAdapter(config)` accepte `config.module` (loader du world). (2) `@ai_kit/core` : `WorkflowKitOptions.adapter` court-circuite l'import dynamique de `@ai_kit/workflow-world`. Puis bump de version + merge `dev→main` (publish CI) + câblage LeRedacteurV2. + +**Tech Stack :** TypeScript (ESM), Vitest, pnpm workspace, Nitro/Nuxt (app hôte), Vercel Workflow SDK (`workflow`, `@workflow/world-postgres`). + +**Spec :** `docs/superpowers/specs/2026-06-02-workflow-kit-injectable-adapter-design.md` + +**Branche :** `feat/workflow-kit-injectable-adapter` (basée sur `origin/dev`). Le spec y est déjà commité. + +--- + +## File Structure + +- `packages/workflow-world/src/contract.ts` — ajoute `WorldConfig.module?` (loader optionnel du world). +- `packages/workflow-world/src/adapter.ts` — `loadWorldModule` priorise `config.module` sur le loader dynamique interne. +- `packages/workflow-world/src/adapter.test.ts` — test du chemin `module`. +- `packages/core/src/workflows/kit/types.ts` — ajoute `WorkflowKitOptions.adapter?`. +- `packages/core/src/workflows/kit/WorkflowKit.ts` — init `#adapter` depuis options + validation. +- `packages/core/src/workflows/kit/WorkflowKit.test.ts` — test du chemin adapter injecté. +- `packages/workflow-world/README.md` — recette déploiement (pattern injecté). +- `packages/{core,workflow-world}/package.json` — bumps de version. + +--- + +## Phase 1 — `@ai_kit/workflow-world` : loader `module` injectable + +### Task 1 : `WorldConfig.module?` + `loadWorldModule(config)` + +**Files:** +- Modify: `packages/workflow-world/src/contract.ts` +- Modify: `packages/workflow-world/src/adapter.ts` +- Test: `packages/workflow-world/src/adapter.test.ts` + +- [ ] **Step 1 : Écrire le test qui échoue** + +Ajouter ce test dans le `describe("createWorldAdapter (postgres)", …)` de `packages/workflow-world/src/adapter.test.ts` : + +```ts +it("module injecté : utilise config.module et NON le loader dynamique du type", async () => { + // loader dynamique interne : doit ne JAMAIS être appelé + const dynPostgres = vi.fn(async () => ({ createWorld: vi.fn() })); + const setWorld = vi.fn(); + __setWorldModuleLoaders({ + postgres: dynPostgres, + runtime: async () => ({ setWorld }), + api: async () => ({ start: vi.fn() }), + }); + + // loader fourni par l'app hôte (littéral chez le consommateur) + const world = { start: vi.fn().mockResolvedValue(undefined) }; + const moduleCreateWorld = vi.fn(() => world); + const moduleLoader = vi.fn(async () => ({ createWorld: moduleCreateWorld })); + + const adapter = createWorldAdapter({ + type: "postgres", + url: "postgres://u:p@h:5432/db", + module: moduleLoader, + }); + await adapter.start(); + + expect(moduleLoader).toHaveBeenCalledTimes(1); + expect(moduleCreateWorld).toHaveBeenCalledWith({ connectionString: "postgres://u:p@h:5432/db" }); + expect(setWorld).toHaveBeenCalledWith(world); + expect(dynPostgres).not.toHaveBeenCalled(); +}); +``` + +- [ ] **Step 2 : Lancer le test → échec attendu** + +Run: `cd packages/workflow-world && pnpm vitest run src/adapter.test.ts -t "module injecté"` +Expected: FAIL — TS : `module` n'existe pas sur le type du paramètre (ou test rouge). + +- [ ] **Step 3 : Ajouter `module?` au contrat** + +Dans `packages/workflow-world/src/contract.ts`, ajouter le champ à l'interface `WorldConfig` (après `maxPoolSize?`) : + +```ts + /** Postgres : taille du pool de connexions. */ + maxPoolSize?: number; + /** + * Loader du module world fourni par l'app hôte, sous forme de littéral + * (`() => import('@workflow/world-postgres')`). Quand présent, il remplace + * l'import dynamique interne : le littéral vit dans le code tracé de l'app, + * donc le bundler (nft) inclut le package dans `.output`. Doit exposer `createWorld`. + */ + module?: () => Promise<{ createWorld: (opts: Record) => unknown }>; +``` + +- [ ] **Step 4 : `loadWorldModule` priorise `config.module`** + +Dans `packages/workflow-world/src/adapter.ts`, remplacer la fonction `loadWorldModule` : + +```ts +async function loadWorldModule(config: WorldConfig) { + const loader = config.module ?? loaders[config.type]; + try { + return await loader(); + } catch (err) { + if ((err as { code?: string }).code === "ERR_MODULE_NOT_FOUND") { + throw new Error( + `workflow-world: the world module '${WORLD_TARGETS[config.type]}' could not be loaded. ` + + `Install it (pnpm add ${WORLD_TARGETS[config.type]}) or pass 'module' in the world config.`, + ); + } + throw err; + } +} +``` + +Puis, dans `createWorldAdapter`, mettre à jour l'appel et caster le world (le loader injecté renvoie `unknown`) : + +```ts + async start() { + const mod = await loadWorldModule(config); + world = mod.createWorld(buildWorldOptions(config)) as SdkWorld; + const { setWorld } = await loaders.runtime(); + setWorld(world); + await world.start?.(); + }, +``` + +- [ ] **Step 5 : Lancer le test ciblé → succès** + +Run: `cd packages/workflow-world && pnpm vitest run src/adapter.test.ts -t "module injecté"` +Expected: PASS + +- [ ] **Step 6 : Suite complète + build du package** + +Run: `cd packages/workflow-world && pnpm vitest run && pnpm build` +Expected: tous les tests PASS (les tests existants — chemin dynamique sans `module` — restent verts), build OK (génère `dist/`). + +- [ ] **Step 7 : Commit** + +```bash +git add packages/workflow-world/src/contract.ts packages/workflow-world/src/adapter.ts packages/workflow-world/src/adapter.test.ts +git commit -m "feat(workflow-world): WorldConfig.module — loader de world injectable (tracing-friendly) + +Permet à l'app hôte de fournir () => import('@workflow/world-postgres'), +littéral tracé par le bundler, à la place de l'import dynamique interne. +Rétrocompatible : sans 'module', comportement inchangé. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Phase 2 — `@ai_kit/core` : `WorkflowKit` accepte un adapter injecté + +### Task 2 : `WorkflowKitOptions.adapter?` + court-circuit + +**Files:** +- Modify: `packages/core/src/workflows/kit/types.ts` +- Modify: `packages/core/src/workflows/kit/WorkflowKit.ts` +- Test: `packages/core/src/workflows/kit/WorkflowKit.test.ts` + +- [ ] **Step 1 : Écrire les tests qui échouent** + +Ajouter ce `describe` à la fin de `packages/core/src/workflows/kit/WorkflowKit.test.ts` : + +```ts +describe("WorkflowKit — adapter injecté", () => { + it("world : start/run/stop délèguent à l'adapter SANS charger @ai_kit/workflow-world", async () => { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue({ runId: "r_inj" }), + }; + const loader = vi.fn(); // le seam ne doit JAMAIS être invoqué + __setWorkflowWorldLoader(loader); + + const kit = new WorkflowKit({ engine: "world", adapter }); + await kit.start(); + const fn = async () => 1; + const handle = await kit.run(fn, ["a"]); + await kit.stop(); + + expect(adapter.start).toHaveBeenCalledTimes(1); + expect(adapter.run).toHaveBeenCalledWith(fn, ["a"]); + expect(handle).toEqual({ runId: "r_inj" }); + expect(adapter.stop).toHaveBeenCalledTimes(1); + expect(loader).not.toHaveBeenCalled(); + }); + + it("world : adapter injecté sans config 'world' → ne throw pas", () => { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue({ runId: "r" }), + }; + expect(() => new WorkflowKit({ engine: "world", adapter })).not.toThrow(); + }); +}); +``` + +- [ ] **Step 2 : Lancer → échec attendu** + +Run: `cd packages/core && pnpm vitest run src/workflows/kit/WorkflowKit.test.ts -t "adapter injecté"` +Expected: FAIL — TS : `adapter` n'existe pas sur `WorkflowKitOptions` ; et `new WorkflowKit({ engine:'world', adapter })` throw encore (validation actuelle). + +- [ ] **Step 3 : Ajouter `adapter?` aux options** + +Dans `packages/core/src/workflows/kit/types.ts`, étendre `WorkflowKitOptions` : + +```ts +export interface WorkflowKitOptions { + /** Moteur par défaut. Défaut : "legacy". */ + engine?: WorkflowEngine; + /** Config du world. Requis si engine === "world" ET adapter absent. */ + world?: WorldConfig; + /** + * Adapter world pré-construit (via `createWorldAdapter` de @ai_kit/workflow-world, + * importé statiquement par l'app hôte). Quand fourni, court-circuite l'import + * dynamique de @ai_kit/workflow-world — ce qui rend les packages traçables par + * le bundler depuis le code de l'app. Prioritaire sur `world`. + */ + adapter?: WorldEngineAdapter; +} +``` + +- [ ] **Step 4 : Init `#adapter` + validation dans `WorkflowKit`** + +Dans `packages/core/src/workflows/kit/WorkflowKit.ts`, remplacer le constructeur : + +```ts + constructor(options: WorkflowKitOptions = {}) { + this.engine = options.engine ?? "legacy"; + this.world = options.world; + if (options.adapter) this.#adapter = options.adapter; + + if (this.engine === "world" && !this.world && !this.#adapter) { + throw new Error( + "WorkflowKit: engine 'world' requires a 'world' config or an 'adapter'", + ); + } + if (this.world && !VALID_WORLD_TYPES.includes(this.world.type)) { + throw new Error(`WorkflowKit: unsupported world type '${this.world.type}'`); + } + } +``` + +(`#ensureAdapter()` retourne déjà `this.#adapter` en premier — aucun autre changement requis : l'adapter injecté est utilisé sans appeler `worldModuleLoader`.) + +- [ ] **Step 5 : Lancer le test ciblé → succès** + +Run: `cd packages/core && pnpm vitest run src/workflows/kit/WorkflowKit.test.ts` +Expected: PASS (les tests existants — chemin dynamique via seam, validation sans world ni adapter — restent verts). + +- [ ] **Step 6 : Build du package** + +Run: `cd packages/core && pnpm build` +Expected: build OK (tsc). NB (mémoire projet) : la suite globale `core` a des échecs pré-existants sans rapport ; ne lancer que le fichier `WorkflowKit.test.ts` ci-dessus pour cette tâche. + +- [ ] **Step 7 : Commit** + +```bash +git add packages/core/src/workflows/kit/types.ts packages/core/src/workflows/kit/WorkflowKit.ts packages/core/src/workflows/kit/WorkflowKit.test.ts +git commit -m "feat(workflow-kit): WorkflowKitOptions.adapter — injection d'un WorldEngineAdapter + +Quand fourni, court-circuite l'import dynamique de @ai_kit/workflow-world : +l'app hôte importe createWorldAdapter statiquement → traçable par le bundler. +Rétrocompatible : sans 'adapter', le chemin d'import dynamique est inchangé. + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Phase 3 — Documentation + +### Task 3 : Recette déploiement dans le README workflow-world + +**Files:** +- Modify: `packages/workflow-world/README.md` + +- [ ] **Step 1 : Ajouter une section « Déploiement bundlé (Nitro/Docker) »** + +Insérer après la section `## Usage` de `packages/workflow-world/README.md` : + +````markdown +## Déploiement bundlé (Nitro/Docker) — imports traçables + +En build bundlé (Nitro `node-server`, déploiement qui ne copie que `.output`), les +imports dynamiques **à argument variable** ne sont pas tracés par nft, donc +`@ai_kit/workflow-world` et le world SDK manquent dans `.output`. Pour les rendre +traçables, **injecte l'adapter** depuis un fichier serveur (imports littéraux) : + +```ts +// server/utils/workflow-kit.ts (app hôte) +import { WorkflowKit } from '@ai_kit/core' +import { createWorldAdapter } from '@ai_kit/workflow-world' // statique → tracé + +export const workflowKit = new WorkflowKit({ + engine: 'world', + adapter: createWorldAdapter({ + type: 'postgres', + url: process.env.WORKFLOW_POSTGRES_URL!, + module: () => import('@workflow/world-postgres'), // littéral → tracé + }), +}) +``` + +Avec ce pattern, **plus besoin** de `nitro.externals.traceInclude` ni de lister ces +packages dans `nitro.externals.external`. Le worker se démarre toujours via un plugin +serveur (`kit.start()` au boot, `kit.stop()` à la fermeture). +```` + +- [ ] **Step 2 : Commit** + +```bash +git add packages/workflow-world/README.md +git commit -m "docs(workflow-world): recette déploiement Nitro/Docker via adapter injecté + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Phase 4 — Bump de version + +### Task 4 : Bumper `@ai_kit/core` et `@ai_kit/workflow-world` + +**Files:** +- Modify: `packages/core/package.json` +- Modify: `packages/workflow-world/package.json` + +- [ ] **Step 1 : Vérifier que les versions cibles sont libres sur NPM** + +Run: +```bash +npm view @ai_kit/core@1.6.0 version 2>/dev/null && echo "PRISE" || echo "LIBRE core 1.6.0" +npm view @ai_kit/workflow-world@0.2.0 version 2>/dev/null && echo "PRISE" || echo "LIBRE ww 0.2.0" +``` +Expected: `LIBRE core 1.6.0` et `LIBRE ww 0.2.0`. (Si « PRISE », incrémenter au prochain patch/minor libre et reporter dans les steps suivants.) + +- [ ] **Step 2 : Bumper core 1.5.0 → 1.6.0** + +Dans `packages/core/package.json`, passer `"version": "1.5.0"` à `"version": "1.6.0"`. + +- [ ] **Step 3 : Bumper workflow-world 0.1.1 → 0.2.0** + +Dans `packages/workflow-world/package.json`, passer `"version": "0.1.1"` à `"version": "0.2.0"`. + +- [ ] **Step 4 : Commit** + +```bash +git add packages/core/package.json packages/workflow-world/package.json +git commit -m "chore(release): @ai_kit/core 1.6.0 + @ai_kit/workflow-world 0.2.0 + +feat: adapter/world injectables (fix tracing Nitro/Docker) + +Co-Authored-By: Claude Opus 4.8 (1M context) " +``` + +--- + +## Phase 5 — Merge `dev → main` et publication (⚠️ GATE de confirmation) + +> ⚠️ **Action sortante / irréversible (publication NPM).** NE PAS exécuter sans +> confirmation explicite de l'utilisateur. La CI publie dès que `packages/*/package.json` +> change sur `main` ; un `npm publish` ne se défait pas. + +### Task 5 : Intégrer la branche, merger sur dev puis main, vérifier la publication + +- [ ] **Step 1 : Pousser la branche de feature** + +```bash +git push -u origin feat/workflow-kit-injectable-adapter +``` + +- [ ] **Step 2 : Vérifier la CI PR-dev (si applicable)** + +Ouvrir une PR `feat/workflow-kit-injectable-adapter → dev` (ou merge direct si c'est le flow habituel). Attendre `pr-dev-tests.yml` vert. +Run (option gh): `gh pr create --base dev --head feat/workflow-kit-injectable-adapter --fill && gh pr checks --watch` + +- [ ] **Step 3 : Merger dans `dev`** + +```bash +git checkout dev && git pull origin dev +git merge --no-ff feat/workflow-kit-injectable-adapter +git push origin dev +``` + +- [ ] **Step 4 : CONFIRMATION UTILISATEUR avant `main`** + +Demander explicitement : « Je merge `dev → main` et ça publie `@ai_kit/core@1.6.0` + `@ai_kit/workflow-world@0.2.0` sur NPM. Je pousse ? » — attendre le feu vert. + +- [ ] **Step 5 : Merger `dev → main` et pousser** + +```bash +git checkout main && git pull origin main +git merge --no-ff dev +git push origin main +``` + +- [ ] **Step 6 : Vérifier la publication NPM** + +Run (après la fin des workflows `realease-core.yml` et `release-workflow-world.yml`) : +```bash +gh run list --branch main --limit 5 +npm view @ai_kit/core@1.6.0 version +npm view @ai_kit/workflow-world@0.2.0 version +``` +Expected: les deux versions retournées par NPM. + +--- + +## Phase 6 — Câbler LeRedacteurV2 sur la nouvelle version (après publication) + +> Repo séparé : `/home/killian/Documents/dev/LeRedacteurV2` (app `app/lrd-nuxt`). +> Dépend de la Phase 5 (packages publiés). + +### Task 6 : Migrer l'app hôte vers l'adapter injecté + +**Files:** +- Modify: `LeRedacteurV2/app/lrd-nuxt/package.json` (versions de deps) +- Modify: `LeRedacteurV2/app/lrd-nuxt/server/utils/workflow-kit.ts` +- Modify: `LeRedacteurV2/app/lrd-nuxt/nuxt.config.ts` (retrait externals/traceInclude) + +- [ ] **Step 1 : Bumper les deps ai-kit** + +Dans `app/lrd-nuxt/package.json`, passer `@ai_kit/core` à `^1.6.0` et `@ai_kit/workflow-world` à `^0.2.0`, puis : +```bash +cd /home/killian/Documents/dev/LeRedacteurV2 && pnpm install +``` + +- [ ] **Step 2 : Utiliser l'adapter injecté** + +Dans `app/lrd-nuxt/server/utils/workflow-kit.ts`, modifier la branche `world` de `buildKit()` : + +```ts +import { WorkflowKit, type WorkflowEngine } from '@ai_kit/core' +import { createWorldAdapter } from '@ai_kit/workflow-world' + +// … dans buildKit(), branche WORKFLOW_ENGINE === 'world' : +return new WorkflowKit({ + engine: 'world', + adapter: createWorldAdapter({ + type: 'postgres', + url: WORKFLOW_POSTGRES_URL, + module: () => import('@workflow/world-postgres'), + }), +}) +``` + +- [ ] **Step 3 : Retirer les contournements de tracing** + +Dans `app/lrd-nuxt/nuxt.config.ts`, retirer `@workflow/world-postgres` et `@ai_kit/workflow-world` de `nitro.externals.external` (garder `workflow`, `@workflow/core` pour l'instant), et supprimer tout `traceInclude` résiduel. + +- [ ] **Step 4 : Build + vérifier que les packages sont dans `.output`** + +Run: +```bash +cd /home/killian/Documents/dev/LeRedacteurV2/app/lrd-nuxt && pnpm build +ls .output/server/node_modules/@ai_kit/workflow-world >/dev/null && echo "OK workflow-world" +ls .output/server/node_modules/@workflow/world-postgres >/dev/null && echo "OK world-postgres" +``` +Expected: `OK workflow-world` et `OK world-postgres`. +(Si l'un manque : vérifier que `workflow-kit.ts` est bien atteint statiquement par le graphe serveur, et que le littéral `import('@workflow/world-postgres')` y est présent.) + +- [ ] **Step 5 : Vérifier au runtime (Docker)** + +Construire l'image Docker (`Dockerfile.lrd-nuxt`), démarrer le conteneur avec `WORKFLOW_ENGINE=world` + `WORKFLOW_POSTGRES_URL`, lancer un run `world` (ex. `runWriteDoc`) et confirmer : pas de module manquant, pas de `StepNotRegistered`, steps exécutés en world Postgres (pas de fallback local). + +- [ ] **Step 6 : Commit (dans LeRedacteurV2, branche dédiée)** + +```bash +cd /home/killian/Documents/dev/LeRedacteurV2 +git add app/lrd-nuxt/package.json app/lrd-nuxt/server/utils/workflow-kit.ts app/lrd-nuxt/nuxt.config.ts pnpm-lock.yaml +git commit -m "feat(workflow): adapter world injecté (@ai_kit 1.6.0/0.2.0) — fix tracing .output" +``` + +--- + +## Self-Review (auteur du plan) + +- **Couverture spec :** §3.1 (core `adapter`) → Task 2 ; §3.2 (workflow-world `module`) → Task 1 ; §4 (migration LeRedacteur) → Task 6 ; §6 (tests) → Tasks 1 & 2 ; critère d'acceptation build Docker → Task 6 step 4-5 ; bump+publish (demande user) → Tasks 4-5. ✅ +- **Placeholders :** aucun — code complet à chaque step. ✅ +- **Cohérence des types :** `WorldConfig.module` (contract.ts) ↔ usage `config.module` (adapter.ts) ↔ `createWorldAdapter({…module})` (README, LeRedacteur) ; `WorkflowKitOptions.adapter: WorldEngineAdapter` ↔ `#adapter` ↔ `createWorldAdapter()` retourne `WorldEngineAdapter` (compat structurelle inter-packages). ✅ +- **Rétrocompat :** chemins dynamiques préservés (tests existants inchangés dans Tasks 1 & 2). ✅ From d6be9b00b2bb4d161974341351dc45db72654a22 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:32:30 +0200 Subject: [PATCH 3/9] =?UTF-8?q?feat(workflow-world):=20WorldConfig.module?= =?UTF-8?q?=20=E2=80=94=20loader=20de=20world=20injectable=20(tracing-frie?= =?UTF-8?q?ndly)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet à l'app hôte de fournir () => import('@workflow/world-postgres'), littéral tracé par le bundler, à la place de l'import dynamique interne. Rétrocompatible : sans 'module', comportement inchangé. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/workflow-world/src/adapter.test.ts | 28 +++++++++++++++++++++ packages/workflow-world/src/adapter.ts | 13 +++++----- packages/workflow-world/src/contract.ts | 7 ++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/packages/workflow-world/src/adapter.test.ts b/packages/workflow-world/src/adapter.test.ts index a15e100..f6102c8 100644 --- a/packages/workflow-world/src/adapter.test.ts +++ b/packages/workflow-world/src/adapter.test.ts @@ -77,6 +77,34 @@ describe("createWorldAdapter (postgres)", () => { const adapter = createWorldAdapter({ type: "postgres", url: "postgres://x" }); await expect(adapter.run(async () => 1, [])).rejects.toThrow(/start\(\) before run\(\)/); }); + + it("module injecté : utilise config.module et NON le loader dynamique du type", async () => { + // loader dynamique interne : doit ne JAMAIS être appelé + const dynPostgres = vi.fn(async () => ({ createWorld: vi.fn() })); + const setWorld = vi.fn(); + __setWorldModuleLoaders({ + postgres: dynPostgres, + runtime: async () => ({ setWorld }), + api: async () => ({ start: vi.fn() }), + }); + + // loader fourni par l'app hôte (littéral chez le consommateur) + const world = { start: vi.fn().mockResolvedValue(undefined) }; + const moduleCreateWorld = vi.fn(() => world); + const moduleLoader = vi.fn(async () => ({ createWorld: moduleCreateWorld })); + + const adapter = createWorldAdapter({ + type: "postgres", + url: "postgres://u:p@h:5432/db", + module: moduleLoader, + }); + await adapter.start(); + + expect(moduleLoader).toHaveBeenCalledTimes(1); + expect(moduleCreateWorld).toHaveBeenCalledWith({ connectionString: "postgres://u:p@h:5432/db" }); + expect(setWorld).toHaveBeenCalledWith(world); + expect(dynPostgres).not.toHaveBeenCalled(); + }); }); describe("createWorldAdapter (mongodb)", () => { diff --git a/packages/workflow-world/src/adapter.ts b/packages/workflow-world/src/adapter.ts index 2e9f8a6..9d4b32b 100644 --- a/packages/workflow-world/src/adapter.ts +++ b/packages/workflow-world/src/adapter.ts @@ -36,14 +36,15 @@ export function __setWorldModuleLoaders(custom?: Partial): v loaders = { ...defaultLoaders(), ...custom }; } -async function loadWorldModule(type: WorldConfig["type"]) { +async function loadWorldModule(config: WorldConfig) { + const loader = config.module ?? loaders[config.type]; try { - return await loaders[type](); + return await loader(); } catch (err) { if ((err as { code?: string }).code === "ERR_MODULE_NOT_FOUND") { throw new Error( - `workflow-world: the optional dependency '${WORLD_TARGETS[type]}' is not installed. ` + - `Install it: pnpm add ${WORLD_TARGETS[type]}`, + `workflow-world: the world module '${WORLD_TARGETS[config.type]}' could not be loaded. ` + + `Install it (pnpm add ${WORLD_TARGETS[config.type]}) or pass 'module' in the world config.`, ); } throw err; @@ -55,8 +56,8 @@ export function createWorldAdapter(config: WorldConfig): WorldEngineAdapter { return { async start() { - const mod = await loadWorldModule(config.type); - world = mod.createWorld(buildWorldOptions(config)); + const mod = await loadWorldModule(config); + world = mod.createWorld(buildWorldOptions(config)) as SdkWorld; const { setWorld } = await loaders.runtime(); setWorld(world); await world.start?.(); diff --git a/packages/workflow-world/src/contract.ts b/packages/workflow-world/src/contract.ts index 23bdb74..e174408 100644 --- a/packages/workflow-world/src/contract.ts +++ b/packages/workflow-world/src/contract.ts @@ -20,6 +20,13 @@ export interface WorldConfig { workerConcurrency?: number; /** Postgres : taille du pool de connexions. */ maxPoolSize?: number; + /** + * Loader du module world fourni par l'app hôte, sous forme de littéral + * (`() => import('@workflow/world-postgres')`). Quand présent, il remplace + * l'import dynamique interne : le littéral vit dans le code tracé de l'app, + * donc le bundler (nft) inclut le package dans `.output`. Doit exposer `createWorld`. + */ + module?: () => Promise<{ createWorld: (opts: Record) => unknown }>; } /** Statut d'un run world (SDK Vercel). */ From 7b1eeb3af7e6bc678b8d5478460e2f2d581eb9a5 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:36:07 +0200 Subject: [PATCH 4/9] =?UTF-8?q?refactor(workflow-world):=20hint=20d'erreur?= =?UTF-8?q?=20conditionnel=20quand=20module=20est=20inject=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/workflow-world/src/adapter.test.ts | 1 + packages/workflow-world/src/adapter.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/workflow-world/src/adapter.test.ts b/packages/workflow-world/src/adapter.test.ts index f6102c8..76ce7f4 100644 --- a/packages/workflow-world/src/adapter.test.ts +++ b/packages/workflow-world/src/adapter.test.ts @@ -103,6 +103,7 @@ describe("createWorldAdapter (postgres)", () => { expect(moduleLoader).toHaveBeenCalledTimes(1); expect(moduleCreateWorld).toHaveBeenCalledWith({ connectionString: "postgres://u:p@h:5432/db" }); expect(setWorld).toHaveBeenCalledWith(world); + expect(world.start).toHaveBeenCalledTimes(1); expect(dynPostgres).not.toHaveBeenCalled(); }); }); diff --git a/packages/workflow-world/src/adapter.ts b/packages/workflow-world/src/adapter.ts index 9d4b32b..4a88426 100644 --- a/packages/workflow-world/src/adapter.ts +++ b/packages/workflow-world/src/adapter.ts @@ -42,9 +42,11 @@ async function loadWorldModule(config: WorldConfig) { return await loader(); } catch (err) { if ((err as { code?: string }).code === "ERR_MODULE_NOT_FOUND") { + const hint = config.module + ? `Install it: pnpm add ${WORLD_TARGETS[config.type]}` + : `Install it (pnpm add ${WORLD_TARGETS[config.type]}) or pass 'module' in the world config.`; throw new Error( - `workflow-world: the world module '${WORLD_TARGETS[config.type]}' could not be loaded. ` + - `Install it (pnpm add ${WORLD_TARGETS[config.type]}) or pass 'module' in the world config.`, + `workflow-world: the world module '${WORLD_TARGETS[config.type]}' could not be loaded. ${hint}`, ); } throw err; From 105d9362ec12fe9c8a90dd842e8efcaeb10a2393 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:37:49 +0200 Subject: [PATCH 5/9] =?UTF-8?q?feat(workflow-kit):=20WorkflowKitOptions.ad?= =?UTF-8?q?apter=20=E2=80=94=20injection=20d'un=20WorldEngineAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand fourni, court-circuite l'import dynamique de @ai_kit/workflow-world : l'app hôte importe createWorldAdapter statiquement → traçable par le bundler. Rétrocompatible : sans 'adapter', le chemin d'import dynamique est inchangé. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/workflows/kit/WorkflowKit.test.ts | 33 +++++++++++++++++++ .../core/src/workflows/kit/WorkflowKit.ts | 7 ++-- packages/core/src/workflows/kit/types.ts | 9 ++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/core/src/workflows/kit/WorkflowKit.test.ts b/packages/core/src/workflows/kit/WorkflowKit.test.ts index 6a4f5f1..d367e49 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.test.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.test.ts @@ -119,3 +119,36 @@ describe("WorkflowKit — runAndWait", () => { await expect(kit.runAndWait(fakeWorkflow as any, { inputData: {} })).rejects.toThrow("boom"); }); }); + +describe("WorkflowKit — adapter injecté", () => { + it("world : start/run/stop délèguent à l'adapter SANS charger @ai_kit/workflow-world", async () => { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue({ runId: "r_inj" }), + }; + const loader = vi.fn(); // le seam ne doit JAMAIS être invoqué + __setWorkflowWorldLoader(loader); + + const kit = new WorkflowKit({ engine: "world", adapter }); + await kit.start(); + const fn = async () => 1; + const handle = await kit.run(fn, ["a"]); + await kit.stop(); + + expect(adapter.start).toHaveBeenCalledTimes(1); + expect(adapter.run).toHaveBeenCalledWith(fn, ["a"]); + expect(handle).toEqual({ runId: "r_inj" }); + expect(adapter.stop).toHaveBeenCalledTimes(1); + expect(loader).not.toHaveBeenCalled(); + }); + + it("world : adapter injecté sans config 'world' → ne throw pas", () => { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue({ runId: "r" }), + }; + expect(() => new WorkflowKit({ engine: "world", adapter })).not.toThrow(); + }); +}); diff --git a/packages/core/src/workflows/kit/WorkflowKit.ts b/packages/core/src/workflows/kit/WorkflowKit.ts index a7d8277..dd81646 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.ts @@ -34,9 +34,12 @@ export class WorkflowKit { constructor(options: WorkflowKitOptions = {}) { this.engine = options.engine ?? "legacy"; this.world = options.world; + if (options.adapter) this.#adapter = options.adapter; - if (this.engine === "world" && !this.world) { - throw new Error("WorkflowKit: engine 'world' requires a 'world' config"); + if (this.engine === "world" && !this.world && !this.#adapter) { + throw new Error( + "WorkflowKit: engine 'world' requires a 'world' config or an 'adapter'", + ); } if (this.world && !VALID_WORLD_TYPES.includes(this.world.type)) { throw new Error(`WorkflowKit: unsupported world type '${this.world.type}'`); diff --git a/packages/core/src/workflows/kit/types.ts b/packages/core/src/workflows/kit/types.ts index 041605d..8944f79 100644 --- a/packages/core/src/workflows/kit/types.ts +++ b/packages/core/src/workflows/kit/types.ts @@ -18,8 +18,15 @@ export interface WorldConfig { export interface WorkflowKitOptions { /** Moteur par défaut. Défaut : "legacy". */ engine?: WorkflowEngine; - /** Config du world. Requis si engine === "world". */ + /** Config du world. Requis si engine === "world" ET adapter absent. */ world?: WorldConfig; + /** + * Adapter world pré-construit (via `createWorldAdapter` de @ai_kit/workflow-world, + * importé statiquement par l'app hôte). Quand fourni, court-circuite l'import + * dynamique de @ai_kit/workflow-world — ce qui rend les packages traçables par + * le bundler depuis le code de l'app. Prioritaire sur `world`. + */ + adapter?: WorldEngineAdapter; } /** Statut d'un run world (SDK Vercel). */ From 1ad56758d0a8f71f4dffdea53b1a31631799e200 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:42:03 +0200 Subject: [PATCH 6/9] =?UTF-8?q?fix(workflow-kit):=20adapter=20inject=C3=A9?= =?UTF-8?q?=20ignore=20vraiment=20la=20config=20world=20(validation=20type?= =?UTF-8?q?=20gated)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/src/workflows/kit/WorkflowKit.test.ts | 14 +++++++++++++- packages/core/src/workflows/kit/WorkflowKit.ts | 4 +++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/core/src/workflows/kit/WorkflowKit.test.ts b/packages/core/src/workflows/kit/WorkflowKit.test.ts index d367e49..1f6e998 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.test.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.test.ts @@ -127,7 +127,7 @@ describe("WorkflowKit — adapter injecté", () => { stop: vi.fn().mockResolvedValue(undefined), run: vi.fn().mockResolvedValue({ runId: "r_inj" }), }; - const loader = vi.fn(); // le seam ne doit JAMAIS être invoqué + const loader = vi.fn().mockRejectedValue(new Error("loader must not be called")); // le seam ne doit JAMAIS être invoqué __setWorkflowWorldLoader(loader); const kit = new WorkflowKit({ engine: "world", adapter }); @@ -151,4 +151,16 @@ describe("WorkflowKit — adapter injecté", () => { }; expect(() => new WorkflowKit({ engine: "world", adapter })).not.toThrow(); }); + + it("world : adapter injecté + config world de type invalide → ne throw pas (world ignoré)", () => { + const adapter = { + start: vi.fn().mockResolvedValue(undefined), + stop: vi.fn().mockResolvedValue(undefined), + run: vi.fn().mockResolvedValue({ runId: "r" }), + }; + expect( + // @ts-expect-error type de world volontairement invalide + () => new WorkflowKit({ engine: "world", world: { type: "redis", url: "x" }, adapter }), + ).not.toThrow(); + }); }); diff --git a/packages/core/src/workflows/kit/WorkflowKit.ts b/packages/core/src/workflows/kit/WorkflowKit.ts index dd81646..12b658d 100644 --- a/packages/core/src/workflows/kit/WorkflowKit.ts +++ b/packages/core/src/workflows/kit/WorkflowKit.ts @@ -41,7 +41,9 @@ export class WorkflowKit { "WorkflowKit: engine 'world' requires a 'world' config or an 'adapter'", ); } - if (this.world && !VALID_WORLD_TYPES.includes(this.world.type)) { + // L'adapter injecté a la priorité : si présent, la config `world` est ignorée, + // donc on ne valide son `type` que lorsqu'aucun adapter n'est fourni. + if (this.world && !this.#adapter && !VALID_WORLD_TYPES.includes(this.world.type)) { throw new Error(`WorkflowKit: unsupported world type '${this.world.type}'`); } } From 6a73923d193406bf655ef7c7c73b18447da76860 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:43:42 +0200 Subject: [PATCH 7/9] =?UTF-8?q?docs(workflow-world):=20recette=20d=C3=A9pl?= =?UTF-8?q?oiement=20Nitro/Docker=20via=20adapter=20inject=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/workflow-world/README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/workflow-world/README.md b/packages/workflow-world/README.md index 8f126d6..8704e48 100644 --- a/packages/workflow-world/README.md +++ b/packages/workflow-world/README.md @@ -52,6 +52,32 @@ const handle = await kit.run(myWorldWorkflow, [arg]); // → start() du SDK await kit.stop(); // arrêt propre ``` +## Déploiement bundlé (Nitro/Docker) — imports traçables + +En build bundlé (Nitro `node-server`, déploiement qui ne copie que `.output`), les +imports dynamiques **à argument variable** ne sont pas tracés par nft, donc +`@ai_kit/workflow-world` et le world SDK manquent dans `.output`. Pour les rendre +traçables, **injecte l'adapter** depuis un fichier serveur (imports littéraux) : + +```ts +// server/utils/workflow-kit.ts (app hôte) +import { WorkflowKit } from '@ai_kit/core' +import { createWorldAdapter } from '@ai_kit/workflow-world' // statique → tracé + +export const workflowKit = new WorkflowKit({ + engine: 'world', + adapter: createWorldAdapter({ + type: 'postgres', + url: process.env.WORKFLOW_POSTGRES_URL!, + module: () => import('@workflow/world-postgres'), // littéral → tracé + }), +}) +``` + +Avec ce pattern, **plus besoin** de `nitro.externals.traceInclude` ni de lister ces +packages dans `nitro.externals.external`. Le worker se démarre toujours via un plugin +serveur (`kit.start()` au boot, `kit.stop()` à la fermeture). + ## Écriture des workflows/steps (important) Il n'existe **pas** de helper runtime `defineWorldStep` : le compilateur `workflow/nitro` From ea70e2aa6d63d417b779513eea81d0584a66f764 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:44:31 +0200 Subject: [PATCH 8/9] chore(release): @ai_kit/core 1.6.0 + @ai_kit/workflow-world 0.2.0 feat: adapter/world injectables (fix tracing Nitro/Docker) Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/core/package.json | 2 +- packages/workflow-world/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 62e4fab..a46110b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ai_kit/core", - "version": "1.5.0", + "version": "1.6.0", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/workflow-world/package.json b/packages/workflow-world/package.json index 3eae109..6883f34 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.1", + "version": "0.2.0", "description": "Vercel Workflow SDK world engine adapter for AI Kit (self-hosted Postgres/MongoDB).", "type": "module", "main": "./dist/index.js", From dfaab4ec12932001bbb577749b49be173a3b2c52 Mon Sep 17 00:00:00 2001 From: Killian-Aidalinfo Date: Tue, 2 Jun 2026 22:49:19 +0200 Subject: [PATCH 9/9] =?UTF-8?q?test(workflow-world):=20couvre=20le=20chemi?= =?UTF-8?q?n=20module=20inject=C3=A9=20qui=20rejette=20ERR=5FMODULE=5FNOT?= =?UTF-8?q?=5FFOUND?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/workflow-world/src/adapter.test.ts | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/workflow-world/src/adapter.test.ts b/packages/workflow-world/src/adapter.test.ts index 76ce7f4..c8abdb8 100644 --- a/packages/workflow-world/src/adapter.test.ts +++ b/packages/workflow-world/src/adapter.test.ts @@ -106,6 +106,34 @@ describe("createWorldAdapter (postgres)", () => { expect(world.start).toHaveBeenCalledTimes(1); expect(dynPostgres).not.toHaveBeenCalled(); }); + + it("module injecté qui rejette ERR_MODULE_NOT_FOUND → message explicite, sans hint 'module'", async () => { + const setWorld = vi.fn(); + __setWorldModuleLoaders({ + runtime: async () => ({ setWorld }), + api: async () => ({ start: vi.fn() }), + }); + const adapter = createWorldAdapter({ + type: "postgres", + url: "postgres://x", + module: async () => { + const e = new Error("nf") as Error & { code?: string }; + e.code = "ERR_MODULE_NOT_FOUND"; + throw e; + }, + }); + await expect(adapter.start()).rejects.toThrow( + "workflow-world: the world module '@workflow/world-postgres' could not be loaded.", + ); + let msg = ""; + try { + await adapter.start(); + } catch (e) { + msg = (e as Error).message; + } + expect(msg).toContain("Install it: pnpm add @workflow/world-postgres"); + expect(msg).not.toContain("or pass 'module'"); + }); }); describe("createWorldAdapter (mongodb)", () => {