-
-
Notifications
You must be signed in to change notification settings - Fork 206
feat(ai-orchestration): generator-based workflows + orchestrators #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
AlemTuzlak
wants to merge
47
commits into
main
Choose a base branch
from
worktree-cryptic-singing-wadler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
4aa6eb2
feat(ai-orchestration): scaffold package
AlemTuzlak 06e4f54
feat(ai-orchestration): add core public types
AlemTuzlak 826920c
feat(ai-orchestration): add in-memory RunStore
AlemTuzlak 8b39b2f
fix(ai-orchestration): align with repo lint conventions
AlemTuzlak 03f95d8
feat(ai-orchestration): add approve, bindAgents, and retry primitives
AlemTuzlak fbb1114
feat(ai-orchestration): add state snapshot/diff and AG-UI event emit …
AlemTuzlak 24702c6
feat(ai-orchestration): agent invocation with three return shapes
AlemTuzlak f2fcaaa
feat(ai-orchestration): workflow engine drive loop
AlemTuzlak 916055a
fix(ai-orchestration): persist resumed state, share pendingEvents que…
AlemTuzlak 2592f84
feat(ai-orchestration): public API helpers, SSE response, and index e…
AlemTuzlak 8b3423a
feat(ai-client): WorkflowClient
AlemTuzlak 018763d
feat(ai-react): useWorkflow + useOrchestration
AlemTuzlak c1334db
feat(ts-react-chat): article workflow demo
AlemTuzlak f8ce39d
feat(ts-react-chat): feature orchestrator demo
AlemTuzlak 87d7129
feat(ts-react-chat): workflow & orchestration API routes
AlemTuzlak cf11d43
feat(ts-react-chat): workflow + orchestration demo pages
AlemTuzlak 7b07432
test(ai-orchestration): engine smoke tests
AlemTuzlak de7f442
refactor(ai-orchestration): collapse runWorkflow/resumeWorkflow into …
AlemTuzlak 2d273ac
refactor(ai-orchestration): drop toWorkflowSSEResponse wrapper
AlemTuzlak 802548d
fix(ai-orchestration): make StepGenerator TNext=any so heterogeneous …
AlemTuzlak 0c30b69
feat(ai-orchestration): pass agents map to orchestrator router for ty…
AlemTuzlak 32a1987
refactor(ts-react-chat): simplify orchestrator router to be cast-free
AlemTuzlak a7bac73
chore(ts-react-chat): use toServerSentEventsResponse directly
AlemTuzlak 5c2c310
feat(ai-orchestration): ok/fail result helpers
AlemTuzlak 400dc3d
refactor(ai-orchestration): rename ok to succeed
AlemTuzlak 58be487
feat(ai-orchestration): add defineRouter helper for extracted routers
AlemTuzlak aad5d98
refactor(ts-react-chat): drop redundant initialize and use defineRouter
AlemTuzlak 95f095f
feat(ai-client): add endpoint shortcut for useWorkflow
AlemTuzlak 247712b
refactor(ts-react-chat): use endpoint shortcut in demo pages
AlemTuzlak dd4bb66
feat(ai-orchestration): add handleWorkflowRequest server helper
AlemTuzlak 5594a91
refactor(ts-react-chat): use handleWorkflowRequest in API routes
AlemTuzlak 1473ace
refactor(ai-client): drop endpoint shortcut from WorkflowClient
AlemTuzlak 474b73f
feat(ai-client): add fetchWorkflowEvents adapter helper
AlemTuzlak 2e743ac
feat(ai-react): re-export fetchWorkflowEvents and FetchWorkflowEvents…
AlemTuzlak 878dcfe
refactor(ai-orchestration): replace handleWorkflowRequest with parseW…
AlemTuzlak 6253eee
refactor(ts-react-chat): use fetchWorkflowEvents + parseWorkflowRequest
AlemTuzlak 9bbbc83
fix(ai-orchestration): default-import fast-json-patch for ESM/CJS int…
AlemTuzlak b7e7843
refactor(ai-orchestration): hand-roll JSON Patch differ, drop fast-js…
AlemTuzlak cefd387
fix(ai-react): stabilize useWorkflow client identity (mirror useChat …
AlemTuzlak 6256f20
fix(ai-orchestration): share dispatch loop so resume handles full des…
AlemTuzlak 37e31f3
feat(ts-react-chat): editorial-brutalist redesign of workflow + orche…
AlemTuzlak 85224f3
feat(ai-orchestration,ai-client): stream workflow output through RUN_…
AlemTuzlak c0202ef
feat(ai-orchestration,ai-react): support free-text feedback on approv…
AlemTuzlak 6db5d96
feat(ts-react-chat): article revision loop with editor feedback and 3…
AlemTuzlak f87140b
feat(ts-react-chat): live DraftPreview in workflow right column
AlemTuzlak 9880c45
feat: finalize approval step on resume + modal preview of published a…
AlemTuzlak 16354b7
ci: apply automated fixes
autofix-ci[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { useEffect } from 'react' | ||
|
|
||
| interface Article { | ||
| title: string | ||
| paragraphs: Array<string> | ||
| } | ||
|
|
||
| export function ArticleModal(props: { article: Article; onClose: () => void }) { | ||
| // Close on Escape, lock body scroll while open. | ||
| useEffect(() => { | ||
| const onKey = (e: KeyboardEvent) => { | ||
| if (e.key === 'Escape') props.onClose() | ||
| } | ||
| document.addEventListener('keydown', onKey) | ||
| const prev = document.body.style.overflow | ||
| document.body.style.overflow = 'hidden' | ||
| return () => { | ||
| document.removeEventListener('keydown', onKey) | ||
| document.body.style.overflow = prev | ||
| } | ||
| }, [props]) | ||
|
|
||
| const date = new Date().toLocaleDateString('en-US', { | ||
| year: 'numeric', | ||
| month: 'long', | ||
| day: 'numeric', | ||
| }) | ||
|
|
||
| return ( | ||
| <div | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-label="Published article" | ||
| className="fixed inset-0 z-50 anim-log-in" | ||
| > | ||
| {/* backdrop */} | ||
| <div | ||
| onClick={props.onClose} | ||
| className="absolute inset-0 bg-ink/85 backdrop-blur-sm" | ||
| /> | ||
|
|
||
| {/* page wrapper — scrollable */} | ||
| <div className="relative h-full overflow-auto px-4 sm:px-8 py-10 flex justify-center"> | ||
| <article className="relative max-w-3xl w-full bg-cream text-ink shadow-[16px_16px_0_0_var(--color-citron)] my-4"> | ||
| {/* paper grain */} | ||
| <div | ||
| className="absolute inset-0 pointer-events-none opacity-25 mix-blend-multiply" | ||
| style={{ | ||
| backgroundImage: | ||
| "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.2' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.6'/></svg>\")", | ||
| }} | ||
| /> | ||
|
|
||
| {/* hazard tape header strip */} | ||
| <div className="tape-citron h-2.5" /> | ||
|
|
||
| {/* close button */} | ||
| <button | ||
| onClick={props.onClose} | ||
| aria-label="Close" | ||
| className="absolute top-5 right-5 z-10 w-9 h-9 flex items-center justify-center bg-ink text-cream hover:bg-rust transition-colors label-mono" | ||
| > | ||
| ✕ | ||
| </button> | ||
|
|
||
| <div className="relative px-8 sm:px-14 py-12"> | ||
| {/* masthead */} | ||
| <div className="flex items-baseline justify-between border-b border-ink pb-3 mb-10"> | ||
| <span className="label-mono text-rust">Published</span> | ||
| <span className="label-mono text-taupe-deep tabular">{date}</span> | ||
| </div> | ||
|
|
||
| <h1 | ||
| className="text-[clamp(2.25rem,5.5vw,4.25rem)] leading-[0.96] tracking-tight mb-10" | ||
| style={{ | ||
| fontFamily: 'var(--font-display)', | ||
| fontVariationSettings: "'opsz' 144, 'SOFT' 30, 'WONK' 1", | ||
| }} | ||
| > | ||
| {props.article.title} | ||
| </h1> | ||
|
|
||
| {/* article body — column layout for longer pieces */} | ||
| <div className="columns-1 md:columns-2 gap-10"> | ||
| {props.article.paragraphs.map((p, i) => ( | ||
| <p | ||
| key={i} | ||
| className={`mb-5 text-ink leading-[1.65] text-[17px] break-inside-avoid ${ | ||
| i === 0 | ||
| ? 'first-letter:float-left first-letter:text-7xl first-letter:font-bold first-letter:leading-[0.85] first-letter:mr-3 first-letter:text-rust' | ||
| : '' | ||
| }`} | ||
| style={{ | ||
| fontFamily: 'var(--font-display)', | ||
| fontVariationSettings: "'opsz' 17, 'SOFT' 100, 'WONK' 0", | ||
| }} | ||
| > | ||
| {p} | ||
| </p> | ||
| ))} | ||
| </div> | ||
|
|
||
| {/* colophon */} | ||
| <footer className="mt-14 pt-5 border-t border-ink/40 flex items-baseline justify-between label-mono text-taupe-deep"> | ||
| <span>TanStack AI · Article Pipeline</span> | ||
| <span>—fin—</span> | ||
| </footer> | ||
| </div> | ||
|
|
||
| <div className="tape-citron h-2.5" /> | ||
| </article> | ||
| </div> | ||
|
|
||
| {/* corner hint */} | ||
| <div className="absolute bottom-4 left-1/2 -translate-x-1/2 label-mono text-bone/60"> | ||
| press esc or click outside to close | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { useEffect, useRef, useState } from 'react' | ||
|
|
||
| interface Draft { | ||
| title?: string | ||
| paragraphs?: Array<string> | ||
| } | ||
|
|
||
| export function DraftPreview(props: { draft: unknown; phase?: string }) { | ||
| const draft = ( | ||
| props.draft && typeof props.draft === 'object' ? props.draft : null | ||
| ) as Draft | null | ||
|
|
||
| // Pulse highlight when the draft content changes — gives a sense of life. | ||
| const [bumpKey, setBumpKey] = useState(0) | ||
| const lastSerialized = useRef('') | ||
| useEffect(() => { | ||
| const next = JSON.stringify(draft ?? {}) | ||
| if (next !== lastSerialized.current) { | ||
| lastSerialized.current = next | ||
| setBumpKey((k) => k + 1) | ||
| } | ||
| }, [draft]) | ||
|
|
||
| const hasContent = | ||
| draft && (draft.title || (draft.paragraphs && draft.paragraphs.length > 0)) | ||
|
|
||
| return ( | ||
| <aside className="relative"> | ||
| <div className="flex items-baseline justify-between border-b border-bone pb-3 mb-4"> | ||
| <span className="label-mono text-bone">Draft Preview</span> | ||
| <span className="label-mono text-taupe tabular"> | ||
| {hasContent | ||
| ? `${(draft.paragraphs?.length ?? 0).toString().padStart(2, '0')} ¶` | ||
| : '—'} | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="relative bg-cream text-ink shadow-[8px_8px_0_0_var(--color-ink-soft)] border border-ink overflow-hidden"> | ||
| {/* phase stamp */} | ||
| {props.phase && ( | ||
| <div className="absolute top-3 right-3 px-2 py-0.5 bg-ink text-cream label-mono"> | ||
| {props.phase} | ||
| </div> | ||
| )} | ||
|
|
||
| {/* paper grain */} | ||
| <div | ||
| className="absolute inset-0 pointer-events-none opacity-30 mix-blend-multiply" | ||
| style={{ | ||
| backgroundImage: | ||
| "url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='1.4' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.5'/></svg>\")", | ||
| }} | ||
| /> | ||
|
|
||
| <div | ||
| key={bumpKey} | ||
| className="relative px-6 py-7 max-h-[34rem] overflow-auto anim-log-in" | ||
| > | ||
| {!hasContent ? ( | ||
| <Empty /> | ||
| ) : ( | ||
| <> | ||
| <div className="label-mono text-taupe-deep mb-3"> | ||
| Draft № {String(bumpKey).padStart(2, '0')} | ||
| </div> | ||
| {draft.title && ( | ||
| <h2 | ||
| className="text-[clamp(1.5rem,2.4vw,2rem)] leading-[0.98] tracking-tight mb-5" | ||
| style={{ | ||
| fontFamily: 'var(--font-display)', | ||
| fontVariationSettings: "'opsz' 144, 'SOFT' 30, 'WONK' 1", | ||
| }} | ||
| > | ||
| {draft.title} | ||
| </h2> | ||
| )} | ||
| {draft.paragraphs?.map((p, i) => ( | ||
| <p | ||
| key={i} | ||
| className={`mb-3.5 text-[14px] leading-[1.55] text-ink ${ | ||
| i === 0 | ||
| ? 'first-letter:float-left first-letter:text-5xl first-letter:font-bold first-letter:leading-[0.85] first-letter:mr-2 first-letter:text-rust' | ||
| : '' | ||
| }`} | ||
| style={{ | ||
| fontFamily: 'var(--font-display)', | ||
| fontVariationSettings: "'opsz' 14, 'SOFT' 100, 'WONK' 0", | ||
| }} | ||
| > | ||
| {p} | ||
| </p> | ||
| ))} | ||
| </> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </aside> | ||
| ) | ||
| } | ||
|
|
||
| function Empty() { | ||
| return ( | ||
| <div className="py-10 text-center"> | ||
| <div | ||
| className="text-3xl text-taupe-deep italic mb-2" | ||
| style={{ | ||
| fontFamily: 'var(--font-display)', | ||
| fontVariationSettings: "'opsz' 96, 'SOFT' 80, 'WONK' 1", | ||
| }} | ||
| > | ||
| no draft yet. | ||
| </div> | ||
| <div className="label-mono text-taupe">awaiting writer</div> | ||
| </div> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden
draftshape validation before usingparagraphs.map.draftis only object-checked/cast, so malformed payloads (e.g.paragraphs: "text"or{}) can reach Line 77 and crash when.mapis invoked.Suggested fix
interface Draft { title?: string paragraphs?: Array<string> } export function DraftPreview(props: { draft: unknown; phase?: string }) { - const draft = ( - props.draft && typeof props.draft === 'object' ? props.draft : null - ) as Draft | null + const raw = props.draft + const draft: Draft | null = + raw && typeof raw === 'object' + ? { + title: typeof (raw as { title?: unknown }).title === 'string' + ? (raw as { title: string }).title + : undefined, + paragraphs: Array.isArray((raw as { paragraphs?: unknown }).paragraphs) + ? (raw as { paragraphs: unknown[] }).paragraphs.filter( + (p): p is string => typeof p === 'string', + ) + : undefined, + } + : nullAlso applies to: 24-26, 77-92
🤖 Prompt for AI Agents