Last week, I became a meme.
I posted a screenshot of some code powerful (yet unphotogenic) code that was co-created by a CopilotKit engineer and… by a Copilot (Cursor IDE’s).
I had a feeling it would be polarizing - and truth be told I leaned into that slightly (as you’ll see below…). But didn’t think it would become a niche coding meme!
But as I said last week, this is good code and I will die on that hill!
It’s also a fascinating piece of code - and on first glance, it’s a little surprising that what it enables is even possible.
Here’s why 👇
But first: the memes
My tweet got dunked on quite a few times, by a few familiar faces:
There were some pretty funny memes:
And a few based takes:
My own overall response…😉:
With the memes behind us (for now),
let’s see why: (it’s pretty interesting!)
First of all, what does this code do?
Essentially it's a if/else tree with some recursion - for figuring out the types of variables in user code during build time.
(in other words it’s a conditional, recursive type expression).
Worth noting off the bat - that type expressions code doesn’t actually run as part of the application - and so cannot contain if/else blocks, only the ternary operator.
Why does it exist?
In a moment we’ll get to a detailed analysis of how this code works. But first let’s see what it enables.
Background: CopilotKit
CopilotKit is an open-source platform providing plug-and-play, fully customizable Copilot building blocks-
for building app-aware AI chatbots, AI Textareas, and AI agents that can interact with your app - based on application context (and quite a bit more).
You can bring your own LLM (GPT-4 by default), and as of 2 weeks ago, you can bring your own LangChains & LangGraphs too as Copilot Skills.
useCopilotAction
One of CopilotKit’s frontend features is the useCopilotAction
react hook. Each component in the application can define high-level actions for Copilot interactivity.
A simple example:
a food delivery app could add support for addToCurrentOrder
which specifies accepting a dishName
and quantity
arguments, and then allow users to ask for “a starter order for a very hungry family of 5, with 2 picky children, mostly but not only vegetarian”.
Specifying this in code is extremely straightforward. And it works! The Copilot engine is aware of the variables, their types, etc. - and calls the handler as needed (plain typescript/javascript code)
The only problem: the type information isn’t propagated into user code. dishName
, quantity
etc., all appear as any
.
Furthermore: if users of the framework make even a minor error - e.g. if they specify the variable names in the wrong order, or with a small typo — their end-users are going to get a runtime error. 😱
This gets quite a bit worse when you go beyond the simplest case - with nested objects, arrays, etc:
What about Zod Schema?
We have a variant of this react hook which does use Zod!
But in talking to users, a significant portion of them had not used Zod before, or did not like it. It’s cognitive overhead that we don’t want to force on users.
And truth be told: I get it.
As you saw in The Screenshot… we like types, and we can handle the tradeoffs.
But the code below is not an ideal dev experience - especially for the uninitiated.
Zod should not be a CopilotKit adoption blocker.
A Free Lunch! (for our users)
Ironically, the whole point of this scary complex “superior mega brained” type code is… to let the “anti-typists” have their cake and eat it too.
We realized that we can provide all of the safety + convenience benefits of Zod to those who choose plain JSON schemas - with zero overhead for users of the framework.
And all it takes is… 30 lines of conditional recursive type dust in the framework.
In the service of making *user-code* as simple and pedestrian as possible - good framework code can — and routinely does — pull a few tricks.
Framework engineers know this!
(What about debugging, etc? As you’ll see below, this code is really not so complex; it is hard to write, but it’s not too hard to read. But furthermore type expressions are structurally some of the safest code imaginable: they only affects types, are verified by the compiler, and they even get “compiled” out of existence on the executable code.)
Here’s what this code enables, as you can see in the image below:
- type information is propagated ✅
- variables can be specified in any order (they match on names, not positions) ✅
- typos are statically called out as type errors ✅
- This also works recursively with objects and arrays: ✅
As framework authors, our users' dev experience >> our own dev experience. We can handle 30 lines of complex type code (that we understand well). The upside is a magical dev experience for thousands of devs every day. Obviously, this is the right tradeoff.
Bonus: render
By the way, you might have noticed the render
function sitting after the handler. Yes: actions can render arbitrary react components into the chat window.
And of course: the action types continue to hold over there (with even more structure - e.g. they are automatically designated as optional when the function status is ‘still-streaming’).
Special call-out to Aceternity UI (open-source)- we made the sexy function preview component below in minutes, just for this post)
THE Code…🥁
The dunks and the memes were honestly hilarious.
Good job fellas 😂
But for all the geniuses who dunked on the code…
please indulge me a small counter-dunk:
Now, you know what we need:
To get type information out of the raw JSONs above, with support for recursion, arbitrary parameter ordering, typo detection, etc.
Let’s be honest for a moment:
how many of us could write this actual feature (not a “close-enough” demo) from scratch? Even after seeing the code?
I stand by my meme- VERY few! 😉 (if you can do it - we are hiring).
And if few could implement it - fewer still would implement it (I certainly wouldn’t).
How does it work?
Mea Culpa
First a small Mea Culpa - I am guilty!
The GitHub PR indentation makes the code appear more monolithic than it is.
In reality there 4 clearly distinct parts here - and the code is not that hard to understand.
As I alluded to earlier… this is not entirely accidental. Twitter is a funny world: things need to have meme-punch or they get 0 visibility. That’s the game. I purposefully chose the GitHub PR screenshot (not an IDE screenshot), where indentation is less clear- I wanted a little bit of a shock factor.
So - I deserve the roast.
(I’d hoped folks would see what’s going on on 2nd glance. A handful did… 😅)
But there really is something special going on here. This is a piece of code which significantly boosts the developer experience - which would likely not have been written (by us) in a GPT-less world. So I stand by it: the intellectual ceiling is being raised.
Had the code looked visually boring at a glance (rather than striking) - it would not have done justice to this singular moment in the history of technological evolution.
Let’s look at the code:
Below is exactly the same code, with more visible indents (and 4 newlines - sue me 😉).
Once again, it’s a recursive + conditional type expression.
A couple of implications of this being a type expression:
- In type expressions, if/else is not available - you have to use the ternary operator.
- All type code gets “compiled” out of existence on builds. It’s entirely about the developer experience.
If you zoom out, you’ll see this is a simple if / else with 4 distinct cases (implemented via the ternary operator):
- enums (one of a few fixed strings - e.g. ”international” or “domestic”).
- a single object
- an array of objects (with/without attributes specified- subtle difference for another post)
- the base case: specific parameters, can be required or optional
The object cases recurse back on MappedParameterTypes
for each of its own properties’ types.
A second Mea Culpa?
Truth be told, I also had another iteration of this code which is even more actually complex, but also more boring-looking version when I tweeted The Screenshot. (and — you guessed it — the Copilot helped us arrive at this version in minutes.).
Here’s this version:
I suspect DHH would be even less happy with this code if he read through it - types galore. And yet… it doesn’t quite pack the same visual punch. I’d also be first to admit there are also some abstraction complexity tradeoffs here - one could argue it’s not a net improvement over the original version.
As for us?
If 30 lines of complex type code can make our users’ dev experience substantially better - we’ll take that trade any day. Most frameworks have some code of this sort (at least the good ones).
In fact I personally pushed to merge this to main asap despite a few spots for minor improvement. Meta-infra PRs of this sort have a tendency to grow stale and never be merged — unless they are merged rapidly. We are a startup, and we ship like hell - stale is not an option.
If you are excited by what human / AI collaboration can achieve, and if you leave no stone unturned in service of a better developer experience, we are hiring. Bonus points for extensive Cloud Native experience: we are building Copilot Cloud.
Personal remarks
GPT didn’t come up with the core idea for how to implement this. In fact it initially resisted our engineer who had the idea - told him it’s not possible. And it didn’t get it working the first time either (or the 3rd).
This was a deeply collaborative exercise.
Where GPT tech was helpful - is in taking the abstract core insight - and applying it while taking lots of details into account.
This makes the situation even more special if you pause to think for a moment. Todays’ AI can’t do the deep thinking for us humans - but it CAN take care of minutia at all levels of abstraction, and help us focus on the most interesting parts of our work. Truly - Steve Jobs’ “bicycle for the mind”.
Enjoy this moment while it lasts.
Ceiling is being raised: The $1000 Prize Competition!
I said it’s good code - not that it’s perfect code.
In between the dunks, a few folks on twitter had some legitimately interesting ideas for improvements. Cause for a competition!
We actually have yet to release a public version of CopilotKit that includes this change (if you want to use it, clone master
) - which makes for a natural competition deadline!
Submit a PR to our repo your best version of this code. Winner gets $1000 and eternal glory via a PR merge. Bonus points if some GPT / Copilot helps you arrive there and if you share the screenshots.
The PR must be submitted sufficiently early to make it to our next release. 1-2 weeks, but no exact date. So hurry up!
‘Till next time.
Get notified of the latest news and updates.