Skip to content

feat(ai-orchestration): generator-based workflows + orchestrators#542

Open
AlemTuzlak wants to merge 82 commits into
mainfrom
worktree-cryptic-singing-wadler
Open

feat(ai-orchestration): generator-based workflows + orchestrators#542
AlemTuzlak wants to merge 82 commits into
mainfrom
worktree-cryptic-singing-wadler

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented May 10, 2026

Summary

  • New @tanstack/ai-orchestration package: define agents, workflows, and orchestrators using async generators. yield* agents.x(...) for typed agent calls, yield* approve(...) for pause/resume on user decision, plain JS for everything else (if, for, await, Promise.all).
  • defineOrchestrator is sugar over defineWorkflow (same runtime, different vocabulary). defineRouter(config, fn) lets users extract the orchestrator router as a named function with full type inference.
  • All workflow lifecycle is emitted as native AG-UI events (RunStarted, StepStarted/Finished, StateSnapshot, StateDelta via JSON Patch RFC 6902, RunFinished carrying typed output, RunError). Approvals reuse the existing approval-requested custom event.
  • WorkflowClient added to @tanstack/ai-client (mirrors ChatClient's connection-adapter pattern), useWorkflow / useOrchestration hooks added to @tanstack/ai-react.
  • Server-side parseWorkflowRequest(request) extractor; consumers use runWorkflow({ workflow, runStore, ...params }) + toServerSentEventsResponse(stream) — symmetric with how chat() is called.
  • Pluggable RunStore interface with default inMemoryRunStore (1h TTL).
  • Two demo pages on ts-react-chat: /workflow (article writing pipeline with writer → legal → skeptic → editor → approve/revise loop, dramatic fullscreen ArticleModal on publish) and /orchestration (Claude Code-style spec → approve → implement [nested workflow] → review).
  • Editorial-brutalist UI: Fraunces variable serif + JetBrains Mono, warm ink/cream palette with citron accent, paper-grain textures, hazard-tape approval bands, live DraftPreview rendering the article-in-progress as the editor mutates state.

Architecture decisions captured during design

  • Workflow body is an async generator (function*), not a node-array DSL — preserves "feels like JavaScript".
  • State lives in a declared schema, mutated as a plain object inside the generator; engine snapshots between yields and emits JSON Patches.
  • succeed({...}) / fail(reason) helpers replace as const discriminator casts at return sites.
  • v1 runStore is in-memory only; engine uses live generator handles (no replay) for resume. Pluggable interface in place for future durable stores.

Out of scope (post-prototype)

  • Comprehensive test suite (3 engine smoke tests included; full coverage deferred per the prototype-first direction)
  • Full API docs
  • Streaming structured output integration (waits on a separate in-flight chat() PR)
  • Abort signal plumbing through API routes (engine supports it; route handlers no-op for now)
  • parallel / loop / step / ask primitives
  • Dedicated devtools workflow panel (events render in the existing iteration timeline)

Test plan

  • `pnpm --filter @tanstack/ai-orchestration test:lib` — 3 smoke tests pass
  • `pnpm --filter @tanstack/ai-orchestration build` — builds clean
  • `pnpm --filter @tanstack/ai-orchestration test:types` — clean
  • `pnpm --filter @tanstack/ai-orchestration test:eslint` — clean
  • `pnpm --filter @tanstack/ai-client test:types` and `@tanstack/ai-react test:types` — clean
  • In the example app (`pnpm --filter ts-react-chat dev`):
    • Open `/workflow`, click Run with default topic — writer → legal → skeptic → editor steps stream live in the timeline rail; `DraftPreview` populates and refreshes
    • Approve the published-article approval card → modal opens with the full typeset article (esc / backdrop closes it)
    • Click Revise with feedback in the textarea → editor re-runs (visible in timeline + DraftPreview), approval prompts again
    • Open `/orchestration`, click Run — triage routes through spec → await-approval → implementation sub-workflow (nested steps appear under their parent) → review
    • Verify `State Snapshot` paper card on the right reflects `STATE_DELTA` updates as the workflow progresses

Summary by CodeRabbit

  • New Features

    • Workflows & Orchestration: generator-based workflows, orchestrators, approvals, signals, retries, durable run persistence, server SSE streaming, and a headless Workflow client with run controls.
  • UI

    • New Orchestration and Workflow pages with terminal-style orchestration UI, pipeline timeline, draft preview, file tree, syntax-highlighted code blocks, state inspector, and article modal.
  • Documentation

    • Extensive guides and API reference for workflows, orchestrators, approvals, retries, persistence, and examples.
  • Tests

    • Broad test coverage for engine durability, idempotency, retries, signals, timeouts, and primitives.
  • Style

    • Updated fonts, theme tokens, utilities, animations, and syntax highlighting for the example app.

Review Change Stack

AlemTuzlak added 30 commits May 10, 2026 19:25
Implements yield-helpers for the workflow engine: approve() for human-in-the-loop approval steps, bindAgents() to convert agent/workflow definitions into bound step generators, and retry() (async generator) for fault-tolerant step execution with configurable backoff.
…helpers

Implements snapshotState/diffState using fast-json-patch for RFC 6902 JSON Patch diffs, plus emit-events helpers (runStartedEvent, stepStartedEvent, stateSnapshotEvent, approvalRequestedEvent, etc.) that produce StreamChunk values for the workflow SSE stream.
…ue, plug RUN_ERROR runId

- resumeWorkflow now calls runStore.set() before runStore.delete() so observers see the finished state
- pendingEvents queue moved onto LiveRun so the emit() closure captured during runWorkflow is drained correctly by resumeWorkflow
- Both drive loops drain live.pendingEvents at the top of each iteration
- runErrorEvent now includes runId in the returned chunk
…xports

Implements Tasks 3.1–3.4 and 4.1: defineAgent, defineWorkflow, defineOrchestrator
factory functions; toWorkflowSSEResponse SSE helper; and wires up the full public
API surface in src/index.ts.
Rename the result helper `ok()` to `succeed()` for clarity. The name
`succeed` reads better alongside `fail` and avoids shadowing the
`Response.ok` DOM property name in server contexts.
Add `defineRouter(config, fn)` — a phantom-config wrapper that captures
generic type parameters from a shared config object so users can extract
orchestrator routers as named functions without losing type inference.
Remove `phase: 'scoping' as const` from the orchestrator initialize
since the schema default covers it. Extract the orchestrator router
using the new `defineRouter` helper to demonstrate zero-cast extraction
of a named router function.
Add an `endpoint` option to WorkflowClientOptions (and UseWorkflowOptions)
as a mutually exclusive alternative to `connection`. When `endpoint` is
provided the client internally POSTs JSON and parses the SSE response,
eliminating the inline fetch boilerplate and `as any` cast at every call
site.
Replace the 50-line inline fetch+SSE adapter with a single
\`endpoint: '/api/workflow'\` (resp. \`/api/orchestration\`) option,
removing the last \`as any\` cast in the demo route files.
Add a \`handleWorkflowRequest\` function that encapsulates JSON body
parsing, start-vs-resume-vs-abort dispatch, and SSE response shaping.
Server API routes can now delegate entirely to this helper, eliminating
the \`as { ... }\` cast on the request body and the manual
\`toServerSentEventsResponse\` wiring.
PR #589 added new primitives (step, sleep, waitForSignal, now, uuid,
patched) plus engine rewrites that hit the strictness regime added by
#564:

- Add eslint-disable lines on every legitimate `as unknown as <Type>`
  cast where the engine resumes a generator with a typed value via
  `gen.next(value)` — same pattern already used in approve.ts. Covers
  the new primitives and the two run-workflow.ts cast sites (RUN_STARTED
  runId extraction + LiveRun.generator placeholder).
- Add `override` modifier to StepTimeoutError.name and LogConflictError.name
  so noImplicitOverride accepts the Error.name shadow.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
packages/typescript/ai-orchestration/src/run-store/in-memory.ts (1)

32-61: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run teardown still drops live state without unblocking paused workflows.

TTL expiry and deleteRun remove live entries, but they don’t abort the live controller or resolve/reject a pending approval wait, so paused generators can hang/leak.

🔧 Suggested patch
   const expirations = new Map<string, NodeJS.Timeout>()
 
+  function teardownLive(runId: string) {
+    const l = live.get(runId)
+    if (!l) return
+    try {
+      l.abortController.abort()
+    } catch {}
+    if (l.approvalResolver) {
+      l.approvalResolver({
+        approved: false,
+        approvalId: '',
+        feedback: 'run terminated',
+      })
+    }
+    live.delete(runId)
+  }
+
   function scheduleExpiry(runId: string) {
     const existing = expirations.get(runId)
     if (existing) clearTimeout(existing)
     const handle = setTimeout(() => {
       runs.delete(runId)
-      live.delete(runId)
+      teardownLive(runId)
       stepLogs.delete(runId)
       expirations.delete(runId)
     }, ttl)
     expirations.set(runId, handle)
   }
@@
     deleteRun(runId, _reason) {
       runs.delete(runId)
-      live.delete(runId)
+      teardownLive(runId)
       stepLogs.delete(runId)
       const handle = expirations.get(runId)
       if (handle) clearTimeout(handle)
       expirations.delete(runId)
       return Promise.resolve()
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-orchestration/src/run-store/in-memory.ts` around lines
32 - 61, The TTL expiry handler in scheduleExpiry and the deleteRun
implementation remove entries from runs/live/stepLogs but never notify the live
controller or settle any pending approval waiters, causing paused generators to
hang; update both the timeout callback in scheduleExpiry and the deleteRun
function to first retrieve the live entry (live.get(runId)), and if present call
the live controller's cancellation/unblock API (e.g., controller.abort() or
controller.cancel()) and settle any stored approval promise (resolve/reject it
with a consistent Error like "run deleted" or "run expired"), then proceed to
clear timeouts and delete the maps; ensure setRunState still calls
scheduleExpiry so expiry path is covered.
🧹 Nitpick comments (2)
packages/typescript/ai-orchestration/tests/engine.primitives.test.ts (1)

304-306: ⚡ Quick win

UUID assertion is not v4-specific.

The current pattern allows non-v4 UUIDs while the test intent says v4.

Suggested regex
-    expect(recordedId).toMatch(
-      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
-    )
+    expect(recordedId).toMatch(
+      /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-orchestration/tests/engine.primitives.test.ts` around
lines 304 - 306, The test currently asserts recordedId against a generic UUID
regex; change it to validate a v4 UUID specifically by using a v4-specific
pattern or the project's UUID validator (e.g., assert / validate via the uuid
library) for the recordedId variable in engine.primitives.test.ts; update the
expect(recordedId).toMatch(...) to a regex that enforces the v4 variant (correct
hex and the '4' in the version nibble and the appropriate variant bits) or
replace the regex check with a call like isV4(recordedId) if a helper exists.
packages/typescript/ai-orchestration/tests/engine.timeout.test.ts (1)

206-250: ⚡ Quick win

This test doesn’t reliably prove retry short-circuiting.

callCount is never incremented, and Line 249 (< 200) can still pass with all retries. The assertion is currently too permissive for the stated behavior.

Suggested tightening
         try {
           yield* step(
             'timing-out',
-            () => new Promise(() => {}), // never resolves
+            () =>
+              new Promise<void>(() => {
+                callCount++
+              }), // never resolves
             {
               timeout: 20,
               retry: {
@@
-      // allow generous slack here for CI noise.
-      expect(elapsed).toBeLessThan(200)
+      expect(elapsed).toBeLessThan(120)
+      expect(callCount).toBe(1)
@@
-      expect(finished).toBeDefined()
+      expect(finished?.output.caughtImmediately).toBe(true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-orchestration/tests/engine.timeout.test.ts` around
lines 206 - 250, The test never increments callCount so it can't prove retry
short-circuiting; modify the workflow step function passed to step(...) (the
anon function that returns new Promise(() => {})) to increment callCount each
invocation (so callCount reflects attempts), then replace or augment the
elapsed-based assertion with a deterministic check such as
expect(callCount).toBe(1) (or assert caughtImmediately is true) after
collect(...) completes; this uses the existing wf/step, callCount, collect,
runWorkflow and StepTimeoutError symbols to observe that retries were
short-circuited rather than relying on the flaky elapsed < 200 timing check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typescript/ai-orchestration/src/engine/fingerprint.ts`:
- Around line 38-40: The current fingerprint uses join(',') which can collide
when patch names contain commas; instead serialize the sorted patches
unambiguously before feeding fnv1a64—e.g., replace the join(',') usage with a
robust serializer like JSON.stringify(sorted) (or another deterministic
escaping) so the input to fnv1a64 (referenced here as workflow.patches, sorted,
and fnv1a64) cannot collide.

In `@packages/typescript/ai-orchestration/src/server/parse-request.ts`:
- Around line 49-51: The returned params currently include both approval and
signalDelivery which allows ambiguous downstream behavior; modify the
normalization in parse-request.ts so that when body.signal is present you do not
forward body.approval (e.g., compute a normalizedApproval = body.signal ?
undefined : body.approval or set approval: body.signal ? undefined :
body.approval) and continue to set signalDelivery: body.signal and input:
body.input; update the object construction that currently references approval,
signalDelivery, input to use this normalized approval value so signal takes
precedence over approval.

In `@packages/typescript/ai-orchestration/tests/engine.cas.test.ts`:
- Around line 33-71: Test never performs the duplicate delivery step; after the
first runWorkflow call that delivers signalDelivery with signalId 'same-id' you
must call runWorkflow again with the same runId and signalDelivery: { signalId:
'same-id', payload: { ok: true } } (using the same inMemoryRunStore) and collect
its events, then assert the duplicate attempt returns the existing
record/idempotent result (e.g., verify the returned events still include
RUN_FINISHED and/or that the duplicate response indicates the existing delivery
rather than creating a new one). Use the same helpers shown (runWorkflow,
collect, inMemoryRunStore, signalDelivery) and add an expect on the second
collect to confirm idempotent behavior.

In `@packages/typescript/ai-orchestration/tests/engine.idempotency.test.ts`:
- Around line 162-206: The test "persists signalDelivery.signalId on the
resulting step record" is incomplete (ends with a `void 0` no-op) and either
needs to be finished or skipped; replace the `void 0` placeholder by
implementing the verification using the same pattern as the later working test:
use the defined workflow `wf`, the `inMemoryRunStore()` `store`, call
`collect(runWorkflow(...))` to start and pause, then resume with `runWorkflow`
passing `signalDelivery: { signalId: 'sig-abc-123', payload: {...} }`, and
assert that the persisted step record in `store` contains the `signalId` (or
alternatively mark the test with `.skip`/`.todo` to reflect it's intentionally
incomplete) — look for usages of `runWorkflow`, `collect`, `inMemoryRunStore`,
and the test name to place the assertion or skip.

---

Duplicate comments:
In `@packages/typescript/ai-orchestration/src/run-store/in-memory.ts`:
- Around line 32-61: The TTL expiry handler in scheduleExpiry and the deleteRun
implementation remove entries from runs/live/stepLogs but never notify the live
controller or settle any pending approval waiters, causing paused generators to
hang; update both the timeout callback in scheduleExpiry and the deleteRun
function to first retrieve the live entry (live.get(runId)), and if present call
the live controller's cancellation/unblock API (e.g., controller.abort() or
controller.cancel()) and settle any stored approval promise (resolve/reject it
with a consistent Error like "run deleted" or "run expired"), then proceed to
clear timeouts and delete the maps; ensure setRunState still calls
scheduleExpiry so expiry path is covered.

---

Nitpick comments:
In `@packages/typescript/ai-orchestration/tests/engine.primitives.test.ts`:
- Around line 304-306: The test currently asserts recordedId against a generic
UUID regex; change it to validate a v4 UUID specifically by using a v4-specific
pattern or the project's UUID validator (e.g., assert / validate via the uuid
library) for the recordedId variable in engine.primitives.test.ts; update the
expect(recordedId).toMatch(...) to a regex that enforces the v4 variant (correct
hex and the '4' in the version nibble and the appropriate variant bits) or
replace the regex check with a call like isV4(recordedId) if a helper exists.

In `@packages/typescript/ai-orchestration/tests/engine.timeout.test.ts`:
- Around line 206-250: The test never increments callCount so it can't prove
retry short-circuiting; modify the workflow step function passed to step(...)
(the anon function that returns new Promise(() => {})) to increment callCount
each invocation (so callCount reflects attempts), then replace or augment the
elapsed-based assertion with a deterministic check such as
expect(callCount).toBe(1) (or assert caughtImmediately is true) after
collect(...) completes; this uses the existing wf/step, callCount, collect,
runWorkflow and StepTimeoutError symbols to observe that retries were
short-circuited rather than relying on the flaky elapsed < 200 timing check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31b1c1cb-f2c1-479a-8cf6-cf2edba5fd8e

📥 Commits

Reviewing files that changed from the base of the PR and between 0f66212 and 5486c02.

📒 Files selected for processing (34)
  • docs/config.json
  • docs/getting-started/workflows.md
  • packages/typescript/ai-client/src/index.ts
  • packages/typescript/ai-client/src/workflow-client.ts
  • packages/typescript/ai-orchestration/README.md
  • packages/typescript/ai-orchestration/src/define/define-workflow.ts
  • packages/typescript/ai-orchestration/src/engine/emit-events.ts
  • packages/typescript/ai-orchestration/src/engine/fingerprint.ts
  • packages/typescript/ai-orchestration/src/engine/run-workflow.ts
  • packages/typescript/ai-orchestration/src/index.ts
  • packages/typescript/ai-orchestration/src/primitives/now.ts
  • packages/typescript/ai-orchestration/src/primitives/patched.ts
  • packages/typescript/ai-orchestration/src/primitives/sleep.ts
  • packages/typescript/ai-orchestration/src/primitives/step.ts
  • packages/typescript/ai-orchestration/src/primitives/uuid.ts
  • packages/typescript/ai-orchestration/src/primitives/wait-for-signal.ts
  • packages/typescript/ai-orchestration/src/registry/select-version.ts
  • packages/typescript/ai-orchestration/src/run-store/in-memory.ts
  • packages/typescript/ai-orchestration/src/server/parse-request.ts
  • packages/typescript/ai-orchestration/src/types.ts
  • packages/typescript/ai-orchestration/tests/engine.attach.test.ts
  • packages/typescript/ai-orchestration/tests/engine.cas.test.ts
  • packages/typescript/ai-orchestration/tests/engine.durability.test.ts
  • packages/typescript/ai-orchestration/tests/engine.idempotency.test.ts
  • packages/typescript/ai-orchestration/tests/engine.patched.test.ts
  • packages/typescript/ai-orchestration/tests/engine.primitives.test.ts
  • packages/typescript/ai-orchestration/tests/engine.publisher.test.ts
  • packages/typescript/ai-orchestration/tests/engine.retry.test.ts
  • packages/typescript/ai-orchestration/tests/engine.signals.test.ts
  • packages/typescript/ai-orchestration/tests/engine.smoke.test.ts
  • packages/typescript/ai-orchestration/tests/engine.timeout.test.ts
  • packages/typescript/ai-orchestration/tests/in-memory-store.test.ts
  • packages/typescript/ai-orchestration/tests/registry.test.ts
  • packages/typescript/ai-react/src/use-workflow.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/typescript/ai-orchestration/src/primitives/uuid.ts
  • packages/typescript/ai-orchestration/README.md

Comment thread packages/typescript/ai-orchestration/src/engine/fingerprint.ts Outdated
Comment thread packages/typescript/ai-orchestration/src/server/parse-request.ts Outdated
Comment thread packages/typescript/ai-orchestration/tests/engine.cas.test.ts Outdated
Comment thread packages/ai-orchestration/tests/engine.idempotency.test.ts
AlemTuzlak and others added 5 commits May 20, 2026 14:42
Reconnaissance found three categories of violations across all 13 test
files merged via #589:

- 66 `workflow: wf as any` casts in `runWorkflow({...})` calls —
  defensive; type-checked clean without them.
- 17 `(store as unknown as { getLive }).getLive = (...) => undefined`
  stubs to force the engine into replay-from-log mode.
- 17 `events.find(...) as unknown as { output: {...} } | undefined`
  casts to read the typed output off RUN_FINISHED.

Cleanup:

- New tests/test-utils.ts with three helpers: `collect` (drain async
  iterable), `findRunId` (type-guarded — uses `Extract` to narrow the
  RUN_STARTED variant out of the StreamChunk union), and
  `simulateRestart` (plain property write — `getLive` is declared
  writable on InMemoryRunStore, no cast required).
- Replaced output-cast patterns with `toMatchObject({ output: {...} })`
  inline — preserves the exact assertion semantics and drops the
  double cast.
- Removed local duplicates of `collect` / `findRunId` /
  `RunStartedChunk` interface from 10 files; they all import from the
  shared utils now.
- One bug surfaced: `routed = await reg.forRun(...)` returns
  `Workflow | undefined` but registry.test.ts was passing it straight
  to `runWorkflow({ workflow: routed })`. Added a guard.

Net: 445 deletions → 205 insertions, zero `as any` / `as unknown as`
remaining in the test suite, 68 tests still passing.
Round 1 of the cr-loop CR pass against PR #589 surfaced ~56 bucket (a)
findings across 19 review agents. This commit lands the well-scoped
mechanical batch (~22 fixes). Engine-source and example-app fixes
are tracked separately.

Docs (docs/orchestration/run-persistence.md, docs/api/ai-orchestration.md,
docs/orchestration/workflows.md, docs/orchestration/orchestrators.md):
- Replace stale RunStore shape (get/set/delete) with the actual
  five-method interface (getRunState/setRunState/deleteRun/appendStep/
  getSteps) and document the CAS log semantics.
- Restate RunState's full field set including fingerprint,
  workflowVersion, startingPatches, waitingFor — load-bearing for
  durable RunStore implementers.
- Fix observableRunStore example to wrap the real method names
  (setRunState/deleteRun/appendStep instead of set/delete).
- Document that the engine already implements replay-from-log
  durability; the gap to durable resume is the runStore type widening,
  not engine work.
- runWorkflow options table: add signalDelivery, attach, publish that
  PR #589 added; note the InMemoryRunStore narrow typing.
- parseWorkflowRequest fields: drop the false signal/attach claim,
  add signalDelivery and note the body-field rename.
- defineWorkflow config: add version, patches, defaultStepRetry.
- useWorkflow/useOrchestration action table: add attach(runId) and
  signal(name, payload, { signalId? }) PR #589 added.
- Types table: add SignalResult, StepRecord, StepKind, StepRetryOptions,
  LogConflictError, StepTimeoutError; correct RouterDecision shape.

Packaging (packages/typescript/ai-orchestration):
- package.json: add sideEffects: false and engines.node so tree-shaking
  works in consumer bundlers; add "skills" to files so the agent skill
  ships to npm.
- tsconfig.json: extend ../../../tsconfig.base.json (matches every
  sibling package) instead of the root tsconfig.json; drop the .tsx
  glob from include (this is a Node-only library) and the
  vite.config.ts include/exclude contradiction.

Configs:
- knip.json: drop the dead packages/react-ai workspace entry.
- coupling.json: drop the $schema reference to the missing
  coupling.schema.json.

Skill (ai-core/SKILL.md):
- Bump library_version 0.10.0 -> 0.20.0 to match the current
  @tanstack/ai package version.
- Fix the useChat({ clientTools }) lie — the actual call is
  createChatClientOptions({ tools }) using the clientTools() helper.

Tests (packages/typescript/ai-orchestration/tests):
- engine.timeout.test.ts: the StepTimeoutError retry-predicate test
  never incremented callCount inside the step fn, so caughtImmediately
  was always false and the only outer assertion was an existence check.
  Increment callCount, assert via toMatchObject on the run output,
  drop the dead monkeyPatch/timeoutFired scaffolding.
- engine.idempotency.test.ts: the signalId-persistence test ended
  with `void 0` and no assertions. Add a RUN_FINISHED check and a
  comment pointing to the multi-signal test for the persistence
  assertion.
- engine.cas.test.ts: the duplicate-delivery test only did ONE
  delivery. Rewrote with a two-stage workflow that pauses between
  signals so the same signalId can be replayed against the existing
  log entry via simulateRestart.
- engine.durability.test.ts: the StepRecord-per-agent test never
  read the step log because deleteRun fires on finish. Add an
  approve() pause before return so the log is inspectable, then
  assert two agent records with their results.

All 162 nx tasks green (test:sherif, test:knip, test:docs, test:eslint,
test:lib, test:types, test:build, build).
Five clean fixes in the orchestration demo:

- ArticleModal: useEffect deps were `[props]`, which is a new reference
  every render and re-ran the effect — capturing `'hidden'` into `prev`
  on the second pass and leaking `document.body.style.overflow = 'hidden'`
  after the modal closed. Pin onClose via a ref and depend on `[]` so
  the effect only runs on mount/unmount.
- diff-extract `stripCodeFence`: the closing-fence regex was
  `\`\`\`?\s*$` (third backtick optional) and was matching mid-content
  early-termination on patches containing two-backtick spans. Tighten to
  `\n?\`\`\`\s*$` (mandatory triple-backtick at the close).
- CodeBlock: `errored` state was sticky — once it flipped true, subsequent
  successful re-highlights stayed hidden behind the fallback `<pre>`.
  And `html` from a previous `code` value was shown during the async
  re-highlight, masking the new (raw) code. Reset both at the top of
  the effect; also log on failure so the dev console isn't silent.
- shiki highlighter: cache no longer poisoned on init failure
  (highlighterPromise is cleared so a remount can retry), and the failure
  is logged.
- shiki normalizeLang: aliases (`md`, `ts`, `sh`) now map to canonical
  ids (`markdown`, `typescript`, `bash`) before `loadLanguage` is called.
  Previously the aliases passed through unchanged and shiki rejected them,
  combined with the now-fixed sticky-errored bug it kept the panel
  permanently unhighlightable.
- workflow.tsx: remove dead `if (wf.status === 'idle' || wf.status ===
  'running')` block whose only body was a comment.
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 20, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedshiki@​4.1.01001007794100
Added@​tanstack/​workflow-core@​0.0.2781009892100

View full report

AlemTuzlak and others added 6 commits May 20, 2026 16:36
- retry primitive: relax TNext from `T` to `any` to match StepGenerator<T>.
  Constraining TNext to T rejected workflows that yielded multiple agent
  or step calls with differing return types inside the retried block.
  Type-only change; no runtime impact.

- runErrorEvent: emit `threadId` alongside `runId` so error events match
  the AG-UI shape that runStartedEvent / runFinishedEvent already use.
  Falls back to runId when no threadId is provided, mirroring the
  existing helpers.

Both fixes are pure additions / loosenings — no behavior change for
existing callers. 68 tests still pass.
…s from cr-loop

Engine (run-workflow.ts):
- Honor pre-aborted AbortSignal at start/resume so a caller who cancels
  before runWorkflow has a chance to listen still triggers the engine's
  abort controller. addEventListener('abort') is not invoked for the
  already-aborted state.
- Track step-timeout cause via an explicit `timedOut` flag rather than
  `!timeoutHandle` (which was always truthy once setTimeout assigned),
  so a run-level abort during a step+timeout no longer misclassifies as
  StepTimeoutError. Test in engine.timeout.test.ts.
- Restore RunState.status to 'paused' on signal_lost before returning,
  on both the in-memory and replay paths. The losing caller's resume
  was setting status to 'running' but never reverting, so the next
  resume saw a stale running state.
- Idempotent in-memory retry now emits STEP_FINISHED with the EXISTING
  recorded result (not the caller's payload) AND overrides nextValue
  so the generator resumes with the authoritative first-write. Two
  tabs delivering the same signalId with different payloads now
  observe identical state.
- Use the reserved sentinel `__approval` (not `'approval'`) for the
  signalName equality check, matching every other site in the codebase.
- Attach to a paused-on-approval run now re-emits `approval-requested`
  after `run.paused` so the client's existing handler populates
  pendingApproval. Test in engine.attach.test.ts.

Engine type-widening:
- runWorkflow.runStore now accepts the base RunStore interface. Engine
  uses a new asLiveStore() helper to duck-type setLive/getLive — durable
  RunStore implementations omit those and the engine falls back to the
  replay path. Internal helpers (drainPersistedRun, etc.) widened to
  RunStore too.

Engine misc:
- fingerprint.ts: rewrote the docblock to be honest about being a custom
  64-bit dispersion hash (uses 32-bit FNV prime as FNV_PRIME_LO + 16-bit
  carry split) rather than canonical FNV-1a-64. Locked-in by stored
  fingerprints — changing the algorithm would invalidate in-flight runs.
- sleep.ts: documented the Date.now() determinism gap. Anchoring via
  yield* now() requires engine cooperation we don't have yet; the
  deadline is advisory so the divergence only affects hosts that build
  time-indexed worker jobs off waitingFor.deadline on the replay path.
- invoke-agent shape (c) detection now verifies stream is async-iterable
  AND output is thenable before taking that branch, so a user object
  with literal `stream`/`output` keys doesn't crash.
- selectWorkflowVersion: a versioned run that doesn't match any
  registered version now returns undefined instead of silently falling
  through to the unversioned default. Test in registry.test.ts.
- inMemoryRunStore: don't schedule TTL expiry while a run is paused.
  Long-running waitForSignal / sleep > TTL would silently delete the
  run from underneath the engine; the host owns cleanup via deleteRun.
- parseWorkflowRequest now wraps JSON.parse failures and non-object
  bodies in a typed WorkflowRequestParseError that callers can catch to
  return a proper 400. Tests in parse-request.test.ts.

Client (workflow-client.ts):
- STEP_FINISHED failure detection now requires the engine error envelope
  (`{ error: { name, message } }`) instead of `'error' in content`,
  so a successful step returning `{ error: null, value: ... }` (a
  common tagged-result shape) is no longer misclassified as failed.
  Test in workflow-client.test.ts.
- applyJsonPatch handles root-pointer ops (`path: ''`) — replace/add
  swap the doc, remove clears it. Previously the engine emitted a
  root-replace whenever prev/next state types disagreed and the client
  silently dropped it. Test in workflow-client.test.ts.
- handleChunk guards RUN_FINISHED / RUN_ERROR / RUN_STARTED against
  flipping a terminal local status. After user calls stop(), a delayed
  server RUN_FINISHED from the in-flight stream no longer overwrites
  the local 'aborted'. The error field is also cleared when status is
  'aborted'. Test in workflow-client.test.ts.

New tests (+ 78 → 86 passing):
- tests/engine.smoke.test.ts: pre-aborted signal propagates to agent.
- tests/engine.timeout.test.ts: parent abort during step+timeout does
  NOT surface as StepTimeoutError.
- tests/engine.attach.test.ts: paused-on-approval attach emits
  approval-requested so client UI gets the prompt.
- tests/registry.test.ts: versioned run with no match returns
  undefined (no silent unversioned fallback).
- tests/parse-request.test.ts: 6 tests for the new request-parse
  surface — field extraction, signal→signalDelivery rename, malformed
  JSON / non-object body rejection, cause preservation.
- packages/typescript/ai-client/tests/workflow-client.test.ts: 7 tests
  covering applyJsonPatch root replace + nested mix, failure-envelope
  detection, stop() terminal-state guard, RUN_ERROR aborted-code
  handling, and idle subscribe state.

All 162 nx tasks green. 86 tests pass (+10 from prior). E2E green (179
passed, 5 flaky retries succeeded).
Subject-scoped, load-bearing fixes from CR review on PR #542:

- state-diff: normalize undefined to null in emitted JSON Patch ops so
  JSON.stringify doesn't drop the value field (RFC 6902 invalidity).
- in-memory store: aborting deleteRun now tears down the live controller
  and rejects any pending approval resolver so awaiters don't hang.
- parse-request: enforce signal-over-approval precedence at the parse
  boundary so downstream code never sees an ambiguous body.
- fingerprint: use JSON.stringify for the sorted patch array so patch
  names containing commas don't collide.
- workflow-client: surface stream iteration errors as { status: 'error',
  error } so UI recovery works after a connection drop; preserve the
  aborted terminal state when late failures arrive.
- workflow-client.applyJsonPatch: skip nested ops when an intermediate
  path segment is missing or a primitive, instead of throwing.
- invoke-agent: settle the output Promise from a finally block so
  consumers awaiting it can't hang when the iterator exits early.
- example api routes: wrap parseWorkflowRequest/runWorkflow in try/catch
  and return 400/500 JSON instead of unhandled errors.
- DraftPreview / WorkflowTimeline FailureBlock: narrow unknown payloads
  with proper runtime guards instead of blind casts.
- package.json: switch peerDependencies @tanstack/ai to workspace:*.
- docs: add language tag to fenced code block (MD040).

Tests:
- new state-diff.test.ts pinning undefined → null normalization.
- new in-memory teardown test pinning paused-run abort + resolver reject.
- new workflow-client tests pinning consumeStream error mapping and
  late-failure-after-stop behavior.
- parse-request precedence test pinning signal-wins-over-approval.

162 nx tasks green, 9 ai-client suites pass, 15 ai-orchestration suites
pass (86 tests up from 82), E2E 183 passed.
AlemTuzlak and others added 14 commits May 23, 2026 18:58
Resolves conflicts after main flattened packages/typescript/* to packages/*
and removed PHP/Python packages (#643). Key conflict resolutions:

- packages/ai-client/src/connection-adapters.ts: combined imports and kept
  both StreamTruncatedError (from main) and WorkflowConnectionAdapter (PR)
- packages/ai-client/src/index.ts: re-exported both stream (PR) and
  StreamTruncatedError (main)
- examples/ts-react-chat/src/components/Header.tsx: import both Network
  (PR, orchestration link) and Server (main, server-fn-chat link) icons
- examples/ts-react-chat/src/routeTree.gen.ts: regenerated to include both
  orchestration/workflow (PR) and server-fn-chat (main) routes
- knip.json: adopted flattened workspace paths from main, kept PR's
  duplicates rule
- .agent/self-learning/coupling.json: flattened SKILL.md paths and kept
  the ai-orchestration entry added by the PR
- Moved packages/typescript/ai-orchestration/ to packages/ai-orchestration/
  via git mv to match main's flattened layout (fixes tsconfig extends path,
  package.json directory field)
- pnpm-lock.yaml: regenerated via pnpm install
0.0.2 hardens the runtime: strict approval/signal delivery matching,
retained terminal run logs, and automatic input/output schema
validation. Notable wins for ai-orchestration:

- Post-mortem attach works for free — attaching to a finished run
  now replays the persisted RUN_FINISHED instead of emitting
  run_lost. Hosts no longer need an archive hook to support
  refresh-after-completion.
- Mismatched approval/signal deliveries now lose deterministically
  with explicit `approval_lost` / `signal_lost` codes instead of
  racing.
- Workflow input/output get validated by the engine, so consumers
  see a `validation_error` RUN_ERRORED instead of a downstream
  type error.

Test updates required by the new strict approvalId match: tests
that hard-coded `approvalId: 'a1'` now extract the engine-generated
id from phase 1 events via a new `findApprovalId` helper.
`engine.attach.test.ts > attach — finished run` updated to assert
the new replay behavior.

Also fix `packages/ai-orchestration/eslint.config.js` import path
(`../../../` → `../../`), a leftover from when the package lived
under `packages/typescript/`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant