-
Notifications
You must be signed in to change notification settings - Fork 6
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
feat: Add support for object returns and nested JSX elements #28
Conversation
I think this makes sense functionally, and is necessary to work around the tediousness required for returning multiple values down the tree. But it gets closer and closer to just nested function calls. When someone asks why they should return a JSX component instead of returning the value from a function call, do we have a really good answer? I'm not sure answers like "you get caching" or "that's how the framework works" hold enough water. |
@@ -31,30 +32,39 @@ export const jsx = < | |||
component: (props: TProps) => MaybePromise<TOutput>, | |||
props: TProps | null, | |||
children?: (output: TOutput) => MaybePromise<JSX.Element | JSX.Element[]>, | |||
): Promise<TOutput | TOutput[]> => { | |||
): Promise<Awaited<TOutput> | Awaited<TOutput>[]> => { |
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.
TOutput is not a PromiseLike, we don't want Awaited here I don't think (even if the compiler really thinks we do).
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.
But the component could return a direct value or a promise, no?
Something I had to solve to get this to work is traversing every element since it can contain:
- a plain value
- an array or object of promises or elements
And then making sure that every promise is awaited, and that every element is evaluated recursively until every returned value is plain. Though very possible I am missing something here.
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.
But the component returns MaybePromise<TOutput>
, so I believe that it should already be unwrapped.
@jmoseley this is a great question. JSX components get us two things automatically:
A major goal of this PR is to make it much easier to share components. The most common case for LLM workflows by far is simple chaining. This change is focused on making it easier for the 20% use case to use components in whatever shape you need them in. Think about the world where there is a critical mass of components. It needs to be easy to massage them into whatever output shape you need them in. This change enables that. Standardized components create an ecosystem. Shape flexibility enables component reuse. Both need to be true for a marketplace of LLM tools to emerge. |
} | ||
|
||
if (fn.name) { | ||
Object.defineProperty(WorkflowFunction, "name", { | ||
value: `WorkflowFunction[${fn.name}]`, |
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.
Totally unrelated to this change, but I wonder if we should change the name of this to GsxComponent
.
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.
We can if we have a good reason.
We should work backwards from what we want the examples code to look like. gsx.Component
feels good IMO, users can always import { Component as GsxComponent ..
if they want, but I'm not married to the current convention either.
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.
oh sorry, I meant specifically this string (and the name of the WorkflowFunction
wrapper). So if the user does console.log
they get something specific to gsx and not generic like WorkflowFunction[MyFunction]
export type { Element }; | ||
|
||
// Collect component for parallel execution with named outputs | ||
export async function Collect<T extends Record<string, unknown>>( |
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.
Can we move this to its own file? Let's try to keep index.ts
as just a barrel file.
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.
I think this code gets deleted in a later PR. It is no longer needed.
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ | ||
/* eslint-disable @typescript-eslint/no-unsafe-argument */ |
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.
You can move these to the top of the (collect.ts
) file, all this JSX code is effectively untyped, so no need to worry much about linting.
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.
Good call.
import { ComponentProps, ExecutableValue, WorkflowComponent } from "./types"; | ||
|
||
// Helper to check if something is a JSX element | ||
/* eslint-disable @typescript-eslint/no-unsafe-return */ |
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.
Might as well just move these to the top of the file I think.
"type" in element && | ||
"props" in element && | ||
typeof (element as any).type === "function" && |
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.
We don't use the .type
pattern to store a reference to the original function (I think it comes from React).
// Mark as workflow component and JSX element type | ||
const component = WorkflowFunction as WorkflowComponent<P, O>; | ||
component.isWorkflowComponent = true; |
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.
nit, let's make it clear this is an internal thing:
// Mark as workflow component and JSX element type | |
const component = WorkflowFunction as WorkflowComponent<P, O>; | |
component.isWorkflowComponent = true; | |
// Mark as workflow component and JSX element type | |
const component = WorkflowFunction as WorkflowComponent<P, O>; | |
component.__isGsxComponent = true; |
|
||
// Handle JSX elements | ||
if (isJSXElement(value)) { | ||
const componentResult = await value.type(value.props); |
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.
I can't figure out where .type
is being assigned. I've noticed that claude likes to write this sort of code, I think it's inspired by the React codebase (which uses this pattern under the hood).
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.
And I'm not sure we need to follow the same pattern, I've avoided the extra complexity so far.
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.
Yeah, I thought it was being assigned at the framework layer, but it looks like that must've been lost at some intermediate step.
I'm surprised that it works at all with type being undefined, but it looks like isJSXElement always returns false, but resolveDeep ends up doing the right thing.
We should talk through the right refactor here. Maybe we can just remove most of this code.
closing in favor of #36 |
Add two features to streamline workflow authorship:
Element
Element[]
andstring
Elements
inside of objectsDirect Object Return
return { summary, commentAnalysis }
instead of needing a<CombineOutput>
componentNested JSX Resolution in Objects
Together both of these changes elimintate the need for wrapper components, boilerplate, and reduces the awkwardness of having to deal with weakly typed tuples in fanout and parallel operations.