From 3f439afcbe72b64be95886137ea38ed5f446184f Mon Sep 17 00:00:00 2001 From: evanboyle Date: Wed, 25 Dec 2024 23:11:53 -0800 Subject: [PATCH] add natural pattern for delineating bewteen streaming and sync llm output --- hn_analysis_report.md | 39 +++++++----- hn_analysis_tweet.txt | 2 +- playground/chatCompletion.tsx | 23 +++++++ playground/index.tsx | 65 ++++++++++++++++---- src/component.ts | 30 ++++++++- src/index.ts | 11 ++-- src/jsx-runtime.ts | 58 +++++++++++++----- src/llm.ts | 112 +++++++++++++++++++++++++++++++++- src/resolve.ts | 58 ++++++++++++------ src/stream.ts | 35 +++++++++++ src/types.ts | 26 ++++++-- 11 files changed, 389 insertions(+), 70 deletions(-) create mode 100644 playground/chatCompletion.tsx create mode 100644 src/stream.ts diff --git a/hn_analysis_report.md b/hn_analysis_report.md index 87337c3f..a2909501 100644 --- a/hn_analysis_report.md +++ b/hn_analysis_report.md @@ -1,29 +1,38 @@ -When you sift through the latest chatter on Hacker News, it’s like walking into a buzzing café where software engineers and tech enthusiasts sip their coffee and debate the next big thing. Here’s a rundown of the trends that have them leaning forward in their seats or shaking their heads. +When you sift through the vibrant exchanges on Hacker News, you start to see patterns in the chaos. It's like peering into a crystal ball, albeit one that's a bit cracked and smudged with fingerprints. The topics buzzing around tell us a lot about where technology is heading and what’s gnawing at the minds of those in the trenches. -## Themes with a Thumbs Up +### Exciting Waves in Tech -1. **Gaming on the Web’s Frontier**: Remember when games on the web were like those old handheld consoles? Times have changed. Projects like [Eonfall](https://news.ycombinator.com/item?id=42480624) are proving that you can have rich, cooperative action experiences right in your browser. Using Unity and Nuxt 3, they’re pushing the envelope, despite some bugs. It’s like when the first iPhone came out—flawed but revolutionary. +1. **AI and Machine Learning:** + AI is the new electricity, lighting up the imagination of everyone from garage tinkerers to corporate juggernauts. Take, for instance, the buzz around [Semantic Search for ArXiv Papers](https://news.ycombinator.com/item?id=42507116). It's a project that uses AI to sift through the academic haystack to find those elusive needles. The real excitement lies in AI's potential to revolutionize fields like healthcare by making diagnostics smarter, or code smarter by augmenting software development. -2. **AI in Everyday Objects**: Integrating AI into microcontrollers, as seen with [openai-realtime-embedded-SDK](https://news.ycombinator.com/item?id=42451409), is like putting a brain into your toaster. Suddenly, the mundane becomes interesting. The potential here is vast, from smart homes to smarter appliances. It’s AI stepping off the cloud and into the real world. +2. **Cutting-Edge Development Tools:** + Developers are always on the hunt for ways to do more with less hassle. Enter tools like [Cudair](https://news.ycombinator.com/item?id=42484994), which offers live-reloading for CUDA applications. Such innovations are like giving a chef a sharper knife—they can still chop faster without losing a finger. In high-performance computing, where every millisecond counts, these tools are nothing short of revolutionary. -3. **Streamlined Web Development**: Developers are tired of the kitchen-sink frameworks that feel like driving a tank to the grocery store. Enter [Mizu.js](https://news.ycombinator.com/item?id=42464310), a lightweight templating solution that’s like carrying a Swiss Army knife: simple but surprisingly versatile. +3. **Decentralization and Privacy:** + Privacy is the new frontier, and decentralized systems are the pioneers staking their claim. Projects such as [TideCloak](https://news.ycombinator.com/item?id=42460131) are building the infrastructure for a world where control over personal data is not just a pipe dream. As our lives move increasingly online, the demand for privacy and user sovereignty is growing louder. -## Themes with a Raised Eyebrow +### Storm Clouds on the Horizon -1. **AI: All Hype and No Bite?**: AI is the rock star of the tech world, but like many rock stars, it’s got its skeptics. The discussions around [agentic LLM systems](https://news.ycombinator.com/item?id=42431361) are a reality check. People wonder if these systems are as autonomous as they claim, or if they’re more like a teenager with a learner’s permit. +1. **Tech Monopolies and Privacy Concerns:** + The tech giants are feeling more and more like Big Brother to the Hacker News crowd. Consider the uproar over [uBlock in Chrome](https://news.ycombinator.com/item?id=42506506). Users are frustrated by changes that seem to prioritize profits over privacy, making them feel like pawns rather than participants. -2. **Privacy and Security in the Spotlight**: New tools like [SignWith](https://news.ycombinator.com/item?id=42470254) are under scrutiny for how they handle sensitive data. It’s like inviting a new roommate and not knowing if they’re going to respect your privacy. The community demands transparency and reliability, especially when legal compliance is at stake. +2. **AI-Induced Job Anxiety:** + The specter of AI-induced job loss is haunting the discussions, especially concerning the [future of programming jobs](https://news.ycombinator.com/item?id=42500926). There's a palpable tension between the promise of AI augmenting human work and the fear of it replacing entry-level positions. Yet, the optimists argue that AI will create more jobs than it destroys, akin to how ATMs didn't eliminate bank tellers. -3. **Kubernetes and the Illusion of Simplicity**: Tools like [K8s Cleaner](https://news.ycombinator.com/item?id=42454723) face pushback for potentially glossing over deeper issues in Kubernetes resource management. It’s akin to putting a fresh coat of paint on a building with structural problems. The call is for fixes that address the root, not just the surface. +3. **Cloud Dependency Issues:** + The reliance on cloud services is a double-edged sword. While they offer convenience, they also introduce risks, as seen with issues like those surrounding [Google Authenticator](https://news.ycombinator.com/item?id=42510300). The fragility of these systems can be unnerving, especially when they hold the keys to your digital kingdom. -## The Unexpected +### Surprising Currents -1. **A Love Affair with Software History**: Who knew geeks had such a soft spot for history? The [OS/2 Warp localization project](https://news.ycombinator.com/item?id=42423742) is a testament to the community’s passion for preserving the digital past, akin to restoring an old car to its former glory. +1. **AI's Cross-Disciplinary Ventures:** + AI is proving to be a versatile tool, popping up in unexpected places like [crossword generation](https://news.ycombinator.com/item?id=42496953). It's like discovering your hammer can also make a decent screwdriver. This cross-pollination of ideas is pushing AI into creative domains, sparking new forms of problem-solving. -2. **Nostalgic Gaming**: Games like [SmartHome](https://news.ycombinator.com/item?id=42424508) are tapping into nostalgia by replicating the frustrations of modern tech. It’s like playing a game that mirrors your daily tech annoyances—a cathartic experience for many. +2. **DIY and Open-Source Renaissance:** + There's a resurgence of interest in DIY and open-source projects, as demonstrated by [Musoq](https://news.ycombinator.com/item?id=42453650). It enables querying across diverse data sources with SQL, empowering developers to break free from the shackles of proprietary software. It's a reminder that innovation often thrives outside corporate walls. -3. **Cross-Pollination of Careers**: Tech isn’t an island. The discussion on [interesting jobs](https://news.ycombinator.com/item?id=42421835) highlights a trend of blending tech with fields like patent law and public service. It’s like a mashup of your favorite songs, offering a richer, more fulfilling career path. +3. **Tech’s Cultural and Environmental Impact:** + Projects like [Movie Iris](https://news.ycombinator.com/item?id=42462348) show a unique blend of art, tech, and environmental consciousness. By using color extraction for film analysis, they encourage us to ponder the deeper implications of our cultural consumption and its environmental footprint. -## The Big Picture +### The Collective Pulse -The vibe in the tech community is one of cautious optimism. There’s excitement about new technologies, but also a wary eye on the practicalities and pitfalls. The discourse is balancing innovation with a critical view of hype, especially in AI and security. This blend of enthusiasm and skepticism is what keeps the industry grounded, ensuring that as we explore new horizons, we do so with an eye on what’s real and reliable. \ No newline at end of file +The atmosphere among the tech-savvy is one of cautious optimism. There's a tangible excitement about the transformative power of technology, tempered by a keen awareness of the ethical and societal challenges it brings. As we forge ahead, the focus remains on balancing innovation with responsibility, ensuring that the future we build is one we can be proud of. \ No newline at end of file diff --git a/hn_analysis_tweet.txt b/hn_analysis_tweet.txt index 673643f4..984852c8 100644 --- a/hn_analysis_tweet.txt +++ b/hn_analysis_tweet.txt @@ -1 +1 @@ -AI in your toaster is more exciting than AI in the cloud; it's the real-world leap we didn't see coming. As tech blends with everyday life, the mundane suddenly gets a brain. What happens when your toaster is smarter than your fridge? 🤔 #TechTrends #AI #Innovation \ No newline at end of file +AI is shaking up fields you wouldn't expect, like crossword puzzles—proof that the real magic lies in AI's unexpected versatility, not just its raw power. #AI #Innovation #TechTrends \ No newline at end of file diff --git a/playground/chatCompletion.tsx b/playground/chatCompletion.tsx new file mode 100644 index 00000000..4e57e59c --- /dev/null +++ b/playground/chatCompletion.tsx @@ -0,0 +1,23 @@ +import { gsx } from "@/index"; +import { createLLMService } from "@/llm"; + +const llm = createLLMService({ + model: "gpt-4", + temperature: 0.7, +}); + +interface ChatCompletionProps { + prompt: string; +} + +export const ChatCompletion = gsx.StreamComponent( + async ({ prompt }) => { + // Use the LLM service's streaming API + const result = await llm.completeStream(prompt); + + return { + stream: result.stream, + value: result.value, + }; + }, +); diff --git a/playground/index.tsx b/playground/index.tsx index 14321f4a..bea023f2 100644 --- a/playground/index.tsx +++ b/playground/index.tsx @@ -1,22 +1,25 @@ -import * as gsx from "@/index"; -import { BlogWritingWorkflow } from "./blogWriter"; +import { gsx } from "@/index"; import { HNAnalyzerWorkflow, HNAnalyzerWorkflowOutput, } from "./hackerNewsAnalyzer"; -import * as fs from "fs/promises"; - -async function main() { - console.log("🚀 Starting blog writing workflow"); +import { BlogWritingWorkflow } from "./blogWriter"; +import { ChatCompletion } from "./chatCompletion"; +import fs from "fs/promises"; +import type { Streamable } from "@/types"; - // Use the gensx function to execute the workflow and annotate with the output type. +// Example 1: Simple blog writing workflow +async function runBlogWritingExample() { + console.log("\n🚀 Starting blog writing workflow"); const result = await gsx.execute( , ); - console.log("✅ Final result:", { result }); - console.log("🚀 Starting HN analysis workflow..."); + console.log("✅ Blog writing complete:", { result }); +} - // Request all 500 stories since we're filtering to text-only posts +// Example 2: HN analysis workflow with parallel execution +async function runHNAnalysisExample() { + console.log("\n🚀 Starting HN analysis workflow..."); const { report, tweet } = await gsx.execute( , ); @@ -24,10 +27,48 @@ async function main() { // Write outputs to files await fs.writeFile("hn_analysis_report.md", report); await fs.writeFile("hn_analysis_tweet.txt", tweet); - console.log( "✅ Analysis complete! Check hn_analysis_report.md and hn_analysis_tweet.txt", ); } -await main(); +// Example 3: Streaming vs non-streaming chat completion +async function runStreamingExample() { + const prompt = + "Write a 250 word story about an AI that discovers the meaning of friendship through a series of small interactions with humans. Be concise but meaningful."; + + console.log("\n🚀 Starting streaming example with prompt:", prompt); + + console.log("\n📝 Non-streaming version (waiting for full response):"); + const finalResult = await gsx.execute( + , + ); + console.log("✅ Complete response:", finalResult); + + console.log("\n📝 Streaming version (processing tokens as they arrive):"); + await gsx.execute( + + + {async (response: Streamable) => { + // Print tokens as they arrive + for await (const token of { + [Symbol.asyncIterator]: () => response.stream(), + }) { + process.stdout.write(token); + } + process.stdout.write("\n"); + console.log("✅ Streaming complete"); + }} + + , + ); +} + +// Main function to run examples +async function main() { + await runBlogWritingExample(); + await runHNAnalysisExample(); + await runStreamingExample(); +} + +main().catch(console.error); diff --git a/src/component.ts b/src/component.ts index 74684335..57069493 100644 --- a/src/component.ts +++ b/src/component.ts @@ -1,5 +1,11 @@ import { JSX } from "./jsx-runtime"; -import { ComponentProps, MaybePromise, WorkflowComponent } from "./types"; +import type { + ComponentProps, + MaybePromise, + WorkflowComponent, + StreamComponent, + Streamable, +} from "./types"; export function Component( fn: (props: P) => MaybePromise, @@ -20,3 +26,25 @@ export function Component( return component; } + +export function StreamComponent( + fn: (props: P) => MaybePromise>, +): StreamComponent { + function StreamWorkflowFunction( + props: ComponentProps, + ): MaybePromise> { + return Promise.resolve(fn(props)); + } + + if (fn.name) { + Object.defineProperty(StreamWorkflowFunction, "name", { + value: `StreamWorkflowFunction[${fn.name}]`, + }); + } + + // Mark as stream component + const component = StreamWorkflowFunction as StreamComponent; + component.isStreamComponent = true; + + return component; +} diff --git a/src/index.ts b/src/index.ts index 30fa21e2..2cb3da17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,9 +12,10 @@ 7. Support parallel execution of steps (either dynamic via something liek a collector, or static via a few explicitly defined siblings) */ -import { Component } from "./component"; +import { Component, StreamComponent } from "./component"; import { execute } from "./resolve"; -import { Element, ExecutableValue } from "./types"; +import { Element, ExecutableValue, Streamable } from "./types"; +import { Stream } from "./stream"; // Collect component props export interface CollectProps { @@ -24,15 +25,17 @@ export interface CollectProps { // Export everything through gsx namespace export const gsx = { Component, + StreamComponent, execute, Collect, + Stream, }; // Export Component and execute directly for use in type definitions -export { Component, execute }; +export { Component, StreamComponent, execute, Stream }; // Also export types -export type { Element }; +export type { Element, ExecutableValue, Streamable }; // Collect component for parallel execution with named outputs export async function Collect>( diff --git a/src/jsx-runtime.ts b/src/jsx-runtime.ts index 718cf97f..f7376dc5 100644 --- a/src/jsx-runtime.ts +++ b/src/jsx-runtime.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-namespace */ import { resolveDeep } from "./resolve"; +import { isInStreamingContext } from "./stream"; +import type { Streamable } from "./types"; export namespace JSX { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -23,6 +25,18 @@ export const Fragment = (props: { return [props.children]; }; +// Helper to check if something is a streamable result +function isStreamable(value: unknown): value is Streamable { + return ( + typeof value === "object" && + value !== null && + "stream" in value && + "value" in value && + typeof (value as Streamable).stream === "function" && + value.value instanceof Promise + ); +} + export const jsx = < TOutput, TProps extends Record & { @@ -39,8 +53,31 @@ export const jsx = < // Return a promise that will be handled by execute() return (async (): Promise | Awaited[]> => { - // Execute component and deeply resolve its result + // Execute component const rawResult = await component(props ?? ({} as TProps)); + + // If this is a streaming result, handle it specially + if (isStreamable(rawResult)) { + if (!children) { + // When no children, return the value to be resolved later + return rawResult.value as Promise>; + } + if (isInStreamingContext()) { + // In streaming context, pass the streamable to children and return their result + // No need to await the value here - the stream completion is sufficient + const childrenResult = await children(rawResult); + const resolvedResult = await resolveDeep(childrenResult); + return resolvedResult as Awaited; + } else { + // Outside streaming context, resolve the value first + const resolvedValue = await rawResult.value; + const childrenResult = await children(resolvedValue as TOutput); + const resolvedResult = await resolveDeep(childrenResult); + return resolvedResult as Awaited; + } + } + + // For non-streaming results, resolve deeply const result = (await resolveDeep(rawResult)) as TOutput; // If there are no children, return the resolved result @@ -59,23 +96,14 @@ export const jsx = < // Handle child function if (typeof children === "function") { const childrenResult = await children(result); - return resolveDeep(childrenResult) as Awaited; + const resolvedResult = await resolveDeep(childrenResult); + return resolvedResult as Awaited; } // Handle single child (Fragment edge case) - return resolveDeep(children) as Awaited; + const resolvedResult = await resolveDeep(children); + return resolvedResult as Awaited; })(); }; -export const jsxs = < - TOutput, - TProps extends Record & { - children?: (output: TOutput) => MaybePromise; - }, ->( - component: (props: TProps) => MaybePromise, - props: TProps | null, - children?: (output: TOutput) => MaybePromise, -): Promise => { - return jsx(component, props, children); -}; +export const jsxs = jsx; diff --git a/src/llm.ts b/src/llm.ts index fa077cee..fc77d7a6 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -18,6 +18,11 @@ export interface LLMConfig { retryDelay?: number; } +export interface StreamResult { + value: Promise; + stream: () => AsyncIterator; +} + class LLMError extends Error { constructor( message: string, @@ -30,13 +35,109 @@ class LLMError extends Error { export function createLLMService(config: LLMConfig) { const { - model = "gpt-4o", + model = "gpt-4", temperature = 0.7, maxTokens, maxRetries = 3, retryDelay = 1000, } = config; + // Helper to create a streaming response + function createStreamingResponse( + getStream: () => AsyncIterable, + getValue: () => Promise, + ): StreamResult { + return { + stream: () => { + const stream = getStream(); + return { + async next() { + try { + const { done, value } = + await stream[Symbol.asyncIterator]().next(); + return { done, value: value || "" }; + } catch (error) { + console.error("Stream error:", error); + return { done: true, value: undefined }; + } + }, + [Symbol.asyncIterator]() { + return this; + }, + }; + }, + value: getValue(), + }; + } + + // Chat with streaming support + async function chatStream( + messages: ChatMessage[], + ): Promise> { + // Create a single streaming request + const response = await openai.chat.completions.create({ + model, + messages, + temperature, + max_tokens: maxTokens, + stream: true, + }); + + // Split the stream into two + const [stream1, stream2] = response.tee(); + + // Create a promise that will resolve with the full text + let fullText = ""; + const { + promise: valuePromise, + resolve: resolveValue, + reject: rejectValue, + } = (() => { + let resolve: (value: string) => void; + let reject: (error: Error) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve!, reject: reject! }; + })(); + + // Accumulate the full text in the background using stream1 + (async () => { + try { + for await (const chunk of stream1) { + const content = chunk.choices[0]?.delta?.content || ""; + if (content) { + fullText += content; + } + } + resolveValue(fullText); + } catch (e) { + rejectValue(e instanceof Error ? e : new Error(String(e))); + } + })(); + + // Create a stream generator function that yields chunks immediately from stream2 + const getStream = async function* () { + try { + for await (const chunk of stream2) { + const content = chunk.choices[0]?.delta?.content || ""; + if (content) { + yield content; + } + } + } catch (e) { + throw e instanceof Error ? e : new Error(String(e)); + } + }; + + return { + stream: getStream, + value: valuePromise, + }; + } + + // Original non-streaming chat function async function chat(messages: ChatMessage[]): Promise { let lastError: Error | undefined; @@ -56,6 +157,7 @@ export function createLLMService(config: LLMConfig) { return content; } catch (error) { + console.error("Request failed:", error); lastError = error instanceof Error ? error : new Error(String(error)); // Don't retry on invalid requests @@ -80,13 +182,21 @@ export function createLLMService(config: LLMConfig) { throw new LLMError("Unexpected end of chat function"); } + // Complete with streaming support + async function completeStream(prompt: string): Promise> { + return chatStream([{ role: "user", content: prompt }]); + } + + // Original non-streaming complete function async function complete(prompt: string): Promise { return chat([{ role: "user", content: prompt }]); } return { chat, + chatStream, complete, + completeStream, }; } diff --git a/src/resolve.ts b/src/resolve.ts index b557015b..1994dcc9 100644 --- a/src/resolve.ts +++ b/src/resolve.ts @@ -1,28 +1,46 @@ import { JSX } from "./jsx-runtime"; -import { ComponentProps, ExecutableValue, WorkflowComponent } from "./types"; +import type { + ComponentProps, + ExecutableValue, + WorkflowComponent, + StreamComponent, + Streamable, +} from "./types"; +import { isInStreamingContext } from "./stream"; + +type ComponentType = WorkflowComponent | StreamComponent; // Helper to check if something is a JSX element -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ function isJSXElement( element: unknown, ): element is JSX.Element & { - type: WorkflowComponent; + type: ComponentType; props: ComponentProps; } { + const el = element as { type: ComponentType }; return ( typeof element === "object" && element !== null && "type" in element && "props" in element && - typeof (element as any).type === "function" && - (element as any).type.isWorkflowComponent + typeof el.type === "function" && + (("isWorkflowComponent" in el.type && + el.type.isWorkflowComponent === true) || + ("isStreamComponent" in el.type && el.type.isStreamComponent === true)) + ); +} + +// Helper to check if something is a streamable value +function isStreamable(value: unknown): value is Streamable { + return ( + typeof value === "object" && + value !== null && + "stream" in value && + "value" in value && + typeof (value as Streamable).stream === "function" && + value.value instanceof Promise ); } -/* eslint-enable @typescript-eslint/no-unsafe-return */ -/* eslint-enable @typescript-eslint/no-explicit-any */ -/* eslint-enable @typescript-eslint/no-unsafe-member-access */ /** * Deeply resolves any value, handling promises, arrays, objects, and JSX elements. @@ -31,10 +49,17 @@ function isJSXElement( export async function resolveDeep(value: unknown): Promise { // Handle promises first if (value instanceof Promise) { - /* eslint-disable @typescript-eslint/no-unsafe-assignment */ const resolved = await value; return resolveDeep(resolved); - /* eslint-enable @typescript-eslint/no-unsafe-assignment */ + } + + // Handle streamable values + if (isStreamable(value)) { + if (isInStreamingContext()) { + return value as T; + } + const finalValue = await value.value; + return finalValue as T; } // Handle arrays @@ -69,24 +94,23 @@ export async function resolveDeep(value: unknown): Promise { * This is the main entry point for executing workflow components. */ export async function execute(element: ExecutableValue): Promise { - /* eslint-disable @typescript-eslint/no-unnecessary-condition */ if (element === null || element === undefined) { throw new Error("Cannot execute null or undefined element"); } - /* eslint-enable @typescript-eslint/no-unnecessary-condition */ // Handle JSX elements specially to support children functions if (isJSXElement(element)) { const componentResult = await element.type(element.props); - const resolvedResult = await resolveDeep(componentResult); - // Handle children after fully resolving the component's result + // Handle children if (element.props.children) { + const resolvedResult = await resolveDeep(componentResult); const childrenResult = await element.props.children(resolvedResult); return execute(childrenResult as ExecutableValue); } - return resolvedResult as T; + // No children, just resolve the result + return resolveDeep(componentResult); } // For all other cases, use the shared resolver diff --git a/src/stream.ts b/src/stream.ts new file mode 100644 index 00000000..893ba168 --- /dev/null +++ b/src/stream.ts @@ -0,0 +1,35 @@ +import { Element, ExecutableValue, StreamComponent } from "./types"; +import { execute } from "./resolve"; + +// Global state to track streaming context +let isStreaming = false; + +// Helper to check if a component is a stream component +export function isStreamComponent( + component: unknown, +): component is StreamComponent { + return ( + typeof component === "function" && + "isStreamComponent" in component && + component.isStreamComponent === true + ); +} + +// Component to enable streaming for its children +export async function Stream(props: { + children: Element; +}): Promise { + const prevIsStreaming = isStreaming; + isStreaming = true; + + try { + return await execute(props.children); + } finally { + isStreaming = prevIsStreaming; + } +} + +// Helper to check if we're in a streaming context +export function isInStreamingContext(): boolean { + return isStreaming; +} diff --git a/src/types.ts b/src/types.ts index 5647b658..959a9ff2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,9 @@ export interface OutputProps { output?: string; } +// Base props type without children +type BaseProps

= P & OutputProps; + // Make components valid JSX elements export interface WorkflowComponent extends JSX.ElementType { (props: ComponentProps): MaybePromise; @@ -24,7 +27,22 @@ export type ExecutableValue = /* eslint-enable @typescript-eslint/no-explicit-any */ /* eslint-enable @typescript-eslint/no-redundant-type-constituents */ -export type ComponentProps = P & - OutputProps & { - children?: (output: O) => MaybePromise; - }; +// Component props as a type alias instead of interface +export type ComponentProps = BaseProps

& { + children?: (output: O) => MaybePromise; +}; + +export interface Streamable { + value: Promise; + stream: () => AsyncIterator; +} + +// Stream component props as a type alias +export type StreamComponentProps = BaseProps

& { + children?: (output: Streamable) => MaybePromise; +}; + +export interface StreamComponent extends JSX.ElementType { + (props: StreamComponentProps): MaybePromise>; + isStreamComponent?: true; +}