feat(ai-orchestration): generator-based workflows + orchestrators#542
feat(ai-orchestration): generator-based workflows + orchestrators#542AlemTuzlak wants to merge 82 commits into
Conversation
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.
…single entry point
…yield* delegation works
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.
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (1)
packages/typescript/ai-orchestration/src/run-store/in-memory.ts (1)
32-61:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRun teardown still drops live state without unblocking paused workflows.
TTL expiry and
deleteRunremoveliveentries, 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 winUUID 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 winThis test doesn’t reliably prove retry short-circuiting.
callCountis 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
📒 Files selected for processing (34)
docs/config.jsondocs/getting-started/workflows.mdpackages/typescript/ai-client/src/index.tspackages/typescript/ai-client/src/workflow-client.tspackages/typescript/ai-orchestration/README.mdpackages/typescript/ai-orchestration/src/define/define-workflow.tspackages/typescript/ai-orchestration/src/engine/emit-events.tspackages/typescript/ai-orchestration/src/engine/fingerprint.tspackages/typescript/ai-orchestration/src/engine/run-workflow.tspackages/typescript/ai-orchestration/src/index.tspackages/typescript/ai-orchestration/src/primitives/now.tspackages/typescript/ai-orchestration/src/primitives/patched.tspackages/typescript/ai-orchestration/src/primitives/sleep.tspackages/typescript/ai-orchestration/src/primitives/step.tspackages/typescript/ai-orchestration/src/primitives/uuid.tspackages/typescript/ai-orchestration/src/primitives/wait-for-signal.tspackages/typescript/ai-orchestration/src/registry/select-version.tspackages/typescript/ai-orchestration/src/run-store/in-memory.tspackages/typescript/ai-orchestration/src/server/parse-request.tspackages/typescript/ai-orchestration/src/types.tspackages/typescript/ai-orchestration/tests/engine.attach.test.tspackages/typescript/ai-orchestration/tests/engine.cas.test.tspackages/typescript/ai-orchestration/tests/engine.durability.test.tspackages/typescript/ai-orchestration/tests/engine.idempotency.test.tspackages/typescript/ai-orchestration/tests/engine.patched.test.tspackages/typescript/ai-orchestration/tests/engine.primitives.test.tspackages/typescript/ai-orchestration/tests/engine.publisher.test.tspackages/typescript/ai-orchestration/tests/engine.retry.test.tspackages/typescript/ai-orchestration/tests/engine.signals.test.tspackages/typescript/ai-orchestration/tests/engine.smoke.test.tspackages/typescript/ai-orchestration/tests/engine.timeout.test.tspackages/typescript/ai-orchestration/tests/in-memory-store.test.tspackages/typescript/ai-orchestration/tests/registry.test.tspackages/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
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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
- 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.
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/`.
Summary
@tanstack/ai-orchestrationpackage: 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).defineOrchestratoris sugar overdefineWorkflow(same runtime, different vocabulary).defineRouter(config, fn)lets users extract the orchestrator router as a named function with full type inference.RunStarted,StepStarted/Finished,StateSnapshot,StateDeltavia JSON Patch RFC 6902,RunFinishedcarrying typed output,RunError). Approvals reuse the existingapproval-requestedcustom event.WorkflowClientadded to@tanstack/ai-client(mirrorsChatClient's connection-adapter pattern),useWorkflow/useOrchestrationhooks added to@tanstack/ai-react.parseWorkflowRequest(request)extractor; consumers userunWorkflow({ workflow, runStore, ...params })+toServerSentEventsResponse(stream)— symmetric with howchat()is called.RunStoreinterface with defaultinMemoryRunStore(1h TTL).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).DraftPreviewrendering the article-in-progress as the editor mutates state.Architecture decisions captured during design
function*), not a node-array DSL — preserves "feels like JavaScript".succeed({...})/fail(reason)helpers replaceas constdiscriminator casts at return sites.runStoreis 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)
parallel/loop/step/askprimitivesTest plan
Summary by CodeRabbit
New Features
UI
Documentation
Tests
Style