AI Engineer World's Fair 2025
CI in the Era of AI: From Unit Tests to Stochastic Evals
Read the talk
CI in the Era of AI: From Unit Tests to Stochastic Evals
Zed’s streaming editor shows how broad agent evaluations can expose failures that become focused model tests and, ultimately, deterministic regression tests.
From a talk by Nathan Sobo
Before you start: Familiarity with automated tests, LLM tool calls, and basic Rust syntax will help with the code examples.
How do you test an editor that edits for you?
An agent that searches a repository and edits its files adds a new reliability problem to a code editor: the editor must work correctly even when the model produces unexpected output. Nathan Sobo introduces that problem with a massively sped-up recording of agentic editing in Zed. Built from scratch in Rust rather than forked from VS Code, Zed organizes its rendering around the GPU, much like a video game. Sobo describes roughly 1,200 lines of shader code and a rendering target of 120 frames per second. Making generated edits dependable requires the same empirical attention as making the editor itself responsive.
Zed already has an extensive testing culture. Sobo estimates tens of thousands of tests; his joke that the editor would otherwise crash every eight seconds conveys how much the product depends on them. One collaborative editing test starts a server and two clients, passing in a simulated scheduler to control concurrent activity. The shown test runs 50 randomized iterations.
The scheduler makes failures reproducible. If a particular interleaving of network packets breaks the system, the team can replay that interleaving, freeze it, and investigate it under full control. Sobo’s reference to a failure on a hypothetical 375,000th iteration illustrates why replay matters; it is not the run count of the displayed test. Until the AI features arrived, he reports that this control let Zed keep even concurrency tests deterministic and avoid flaky CI tests.
An LLM changes that arrangement. Even if the team controlled sampling directly, changing an input token could change the generated output. A reproducible software harness does not make model behavior stable across changed inputs. Zed therefore needed to embrace stochastic behavior. Its first major agent evaluation resembled SWE-bench: a broad task that asks an agent to make a repository change.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
A broad failure becomes a search test
A data-driven evaluation starts with inputs and examines outputs. A conventional software test usually expresses a more specific contract through pass/fail assertions. Zed brought those approaches together: its evaluation program compiles a headless copy of the editor, checks out a repository, and runs the agent. The conversation itself can be a function, allowing test code to drive the interaction and make granular assertions about what the agent did.
That granularity matters when a large task fails. There are many possible causes, and an overall failure gives little guidance about which one to repair. In the demonstrated task, the agent must add a window argument to a tool’s run trait method in a specified file. The original grep tool returned too little useful context around the match. The problem lay partly in what the model could see.
The repair became a deterministic search test: set up the project, search for fn run, and check the returned context. Zed uses Tree-sitter to expand matches to syntactic boundaries. The displayed expected result includes the full function signature through its return type, giving the agent a coherent unit of code to inspect. Sobo reports a substantial improvement in agent behavior from this change, without supplying a numerical score. A stochastic evaluation had exposed an ordinary algorithmic defect.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Make edits stream, then test the protocol
Editing initially happened through tool calls. In the implementation Sobo describes, their key/value content arrived together rather than streaming usefully into the editor; this is a limitation of that implementation, not a claim about every tool-calling API. Zed changed the interaction to a two-step protocol:
- Make a small tool call describing the edits.
- Request old-text/new-text blocks that can stream back incrementally.
At this stage, Zed reused the same model for the second request because the context was already in its cache. The tradeoff was a new parsing responsibility: the editor now had to interpret streamed text and tolerate irregular model output.
The team added an evaluation function to its ordinary test suite. In the shown evaluation, Sobo requests 200 runs and says the eventual build gate requires all 200 to pass. That was a threshold the team reached over time, not its starting point. The test loads a conversation with the model and uses an LLM judge to assess the result; other cases use programmatic assertions. Repeated evaluation establishes a measurable acceptance condition, while failures identify smaller problems to investigate.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Three algorithms beneath a reliable streamed edit
Much of the work underneath the model is conventional software engineering. The input may vary, but the implementation can still have precise, testable behavior:
- Incremental parsing. The demonstrated parser test runs 100 iterations, randomly splitting the input at arbitrary chunk boundaries. A packet boundary must not change how the completed text is interpreted.
- Approximate matching. A dynamic programming algorithm finds an approximate match when model-generated old text differs slightly from the buffer. Sobo credits this tolerance with preventing many tool-call failures. The matching algorithm itself can be tested deterministically.
- Streaming diffs. As new text arrives, the editor compares it with the old text. If an old passage is absent from the current output, the editor must distinguish a deletion from text that has not arrived yet. That decision is another deterministic part of the system.
These tests isolate the machinery that turns model output into edits. Live-model evaluations remain useful, but they need not carry the entire burden of validating parsing, matching, and diff behavior.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Prompt improvements leave cases the code must handle
Insertions at the beginning or end of a document revealed a weakness in the old-text/new-text protocol. The model sometimes supplied an empty old-text tag. Without old text, the matching algorithm has no useful anchor for locating the change. A prompt instruction improved the behavior, but Sobo reports that empty old-text tags still appeared roughly 1–2% of the time after the prompt change. The team therefore also simulated that output in tests to exercise robust handling of the case.
Another failure crossed XML tag names: an old-text block would close with a new-text tag. Sobo reports that mismatched tags initially caused failures about 40% of the time; adding “Always close all tags properly” brought success to about 95%, leaving roughly 5% to handle. These figures concern the described tag-handling case, with no model version, sample size, or sampling settings specified. The remaining malformed output became input to a deterministic robustness test. Prompting reduced the frequency of a failure; parser behavior determined what happened when it still occurred.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Recover an edit whose indentation is wrong
Indentation exposed a different kind of near miss. The evaluation places an inner function inside an outer function and asks the model to replace todo!() with return 42. A minimal Rust version of that starting shape is:
rust
fn outer() -> i32 {
fn inner() -> i32 {
todo!()
}
inner()
}
The model could identify the right function and the right replacement while flattening its indentation. Its old text might start with fn inner at the left margin, even though the buffer contains leading spaces. Rejecting the edit solely for that difference would discard otherwise useful output.
Zed handles this by detecting an indentation delta between the matched buffer text and the model’s text, then normalizing the edit. For the Rust example, a flattened replacement such as:
rust
fn inner() -> i32 {
return 42;
}
needs to retain its relative indentation while moving into the outer function’s indentation level. The resulting buffer should be:
rust
fn outer() -> i32 {
fn inner() -> i32 {
return 42;
}
inner()
}
The important operation is to reconcile the model’s indentation with the existing buffer, rather than treating every whitespace difference as a failed match. Sobo then drives that behavior into a deterministic fixture using lorem ipsum dolor sit with an unusual indentation pattern. An outdented replacement changes ipsum dolor sit to ipsum dolor sit amet; indentation normalization lets the test handle it correctly.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Escaping stops at a prompt repair
Rust raw strings introduced another failure mode. Their syntax can preserve quotation marks inside the string, but a simple request to tweak the content sometimes prompted the model to transform the escaping as well. Sobo particularly observed Gemini producing HTML escape codes, adding backslashes, and double-escaping newlines. Those transformations could change text that the edit was supposed to preserve.
For this case, the team used a prompt fix. At the time of the talk, it had not added further algorithmic detection or repair for the escaping behavior. That marks the actual boundary of the implementation: indentation had a normalization algorithm backed by deterministic tests, while the escaping problem remained addressed through model instructions.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Move each understood failure into a smaller test
Rigorous testing still supplies the foundation. What changes is the need to run model interactions repeatedly and define a pass/fail threshold. The search and streaming-edit examples show that many failures arise in familiar software concerns: the context a tool returns, the boundaries a parser accepts, or the way text is matched and transformed.
The development process moves through three levels:
| Test level | What it exercises | What a failure reveals |
|---|---|---|
| Broad agent eval | A complete repository task | A broken user experience |
| Focused stochastic test | One behavior with a live model | A recurring model-output problem |
| Deterministic regression test | A specific algorithm or captured case | A reproducible implementation defect |
The narrower test does not replace the broader one. It makes an understood failure cheaper to reproduce and more precise to diagnose. At the time of the talk, Zed ran these evaluations in its existing test suite and infrastructure, without a special external evaluation framework. The empirical method and much of the engineering skill remained the same; model variability became another input the system had to accommodate.
Sobo closes by inviting contributions to the Zed source repository, describing the editor as open source under the GPL. His practical outcome is personal: with the Claude 4 models, he reports finally being able to write Rust agentically with high efficiency. The parser fixes, matching behavior, focused evaluations, and ordinary regression tests are the work that made that experience possible.
Suggest correction
This note stays in this page until you copy or download it. Nothing is submitted; reloading clears the draft.
Resources
From the talk
Zed's source code, build instructions, contribution guidance, and component licensing.
Documentation for generating parsers and maintaining syntax trees incrementally as source code changes.
The original benchmark paper evaluates models on editing repositories to resolve real GitHub issues.
Further reading
- Zed's agentic editing launchArticle
The May 2025 launch announcement introduces the Agent Panel and demonstrates agentic editing in Zed.
- Maintaining 120 FPS in GPUIArticle
A technical account of Metal rendering, buffering, and macOS display synchronization problems behind smooth scrolling.
Read the complete timestamped transcript
- 0:00
[upbeat music] Hey, everybody.
- 0:15
Thanks for coming to my talk. Uh, I'm Nathan Sobo. I'm the co-founder of Zed. We are an AI-enabled code editor, and what sets us apart is we are not a fork of VS Code.
- 0:27
We are implemented from scratch. [laughs] [clapping] Uh, implemented from scratch in Rust. We literally engineered the entire system like a video game around about 1,200 lines of shader program that run on the GPU, and the rest of the system is organized to deliver frames at 120 frames per second really quickly.
- 0:47
And recently, I recorded a video because time is short, and I-- this is obviously sped up massively, but we launched agentic editing in Zed, and I wanted to talk about the approach that we took to test this, um, and be empirical ab-- and deliver a reliable product that does this effectively.
- 1:04
So here's what we're gonna be talking about. The rest of the talk is gonna be looking at code and just talking about our experience. Um, so
- 1:13
testing, evaluation, what's the difference? Um, first, I just wanna start by saying, like, without an empirical approach to the software that we've been building since twenty-eighteen, twenty twenty-one, depends on how long you measure it, Zed would crash every eight seconds.
- 1:29
Like, uh, we have probably tens of thousands of tests at this point. And here's an example of the extreme to which we take empirical software development. We literally are starting a server, creating two clients, and we have a simulated scheduler that gets passed in here, and we run fifty different iterations of this with every random interleaving of
- 1:48
anything that could possibly be concurrent. Um, so that if the three hundred and seventy-fifth thousandth, and this is only fifty, but, uh, iteration of this particular interleaving of network packets, um, goes wrong, we can replay that over and over again, freeze it, and have full control.
- 2:06
So we're very hardcore about testing and being empirical at Zed. But up until very recently, until shipping some of these AI-related features, we've been able to be fully deterministic.
- 2:17
Even with sort of non-deterministic things, we've been able to really lock things down and never have flaky tests on CI. But as soon as an LLM enters the picture, that's all out the window, right?
- 2:28
Because we could like, you know, assuming that [chuckles] we're using, you know, uh, frontier models, but s- even if we were to use our own model and to, like, control the sampling off the logits, you change one token in the input, and you're gonna get a completely different output.
- 2:43
So it's just a fundamentally different problem where we have to embrace stochastic behavior. And so as we built this system, our first eval that we really hammered on was something that if you've seen something like SWE-bench probably looks pretty familiar.
- 2:57
It's very data-driven, and it seems like in the machine learning world, like this is what an eval is, input, output. But in the programmatic software world, an eval is more like a test that passes or fails.
- 3:11
Like, we come from this very different perspective of automated testing. And so right away, you know, we had this traditional data-driven eval, and then backing that, you know, in the same program we als-- uh, that runs all these evals basically compiles a headless copy of Zed, checks out a repo, runs the agent, tries to make it do
- 3:30
things. Um, we right away got into, like, making the e- eval more programmatic. So you can see here the conversation is literally a function. And then our ability to sort of come in here and write code that performs assertions about what the agent did, even, you know, getting, uh, quite a bit more granular.
- 3:49
'Cause the problem is when that big eval fails, it's like, what do we do? [chuckles] There's a million ways that that thing could go wrong. And so when we wrote this eval, we were able to drive out, uh, one really simple failure mode, which is when we run the grep tool, this is what our original, like, dumb implementation
- 4:06
of the grep tool looked like. So you can see in this case, we're saying we wanna add, uh, a window argument to this tool run trait method in this particular file name, right?
- 4:17
But if this is what the model sees, then we're in trouble, right? And so what we ended up, you know, driving from this stochastic test here is a more deterministic test, right?
- 4:31
Where I'm gonna go ahead and set up the project, uh, perform this search for FN run, and then, uh, we use Tree-sitter, which is, um, Max's parsing, uh, parser generator framework, to actually expand out the match to the syntactic boundaries.
- 4:48
Um, and so that was able to, you know, drive a sub-- a substantial improvement in the behavior of the agent. So right there, like, that was an interesting discovery of just being empirical and driving out what was ultimately an algorithmic problem with a stochastic test.
- 5:04
But then we g- moved on to the editing, and when we first implemented editing, it was just done with tool calls. But the problem with tool calls, if anybody's worked with them, is they don't really stream very well.
- 5:14
Like, you get key, value, key, value, and it all comes back at once. So we, what we ended up doing is deciding instead we perform a small tool call that just describes the edits, then we loop back around for the moment to just the same model because it has everything already loaded in its cache and ask it
- 5:33
to emit these old text, new text blocks. Um, but there's some challenges there. Like, now we have to parse that data is coming back and all kinds of weird stuff that the model's gonna be doing.
- 5:45
So I'll just dive in and show you, um, some of the examples. So here's like--
- 5:51
uh, there are a bunch of different, uh, tests. Like, these are part of our main regular old test suite, and we just added this eval function, and we say, "I wanna run this eval two hundred times."
- 6:02
And in this particular case-
- 6:04
Didn't start this way, [chuckles] but eventually we got this way. We want 100% of these 200 examples to pass, or this test should, like, literally fail to build. So we kind of set a watermark that way, and then here you can see I'm just, like, loading in a conversation with the LLM, um, and then ultimately, uh, in this
- 6:24
case, I'm using, uh, eval as judge. But there are some cases you saw earlier where we do things more programmatically. This case, we're just, like, verifying this thing worked correctly.
- 6:34
Um, and so we go from a test like this, a stochastic test, into the particular problems, um, that drive this. So I mean... But it's funny, a lot of the things that went wrong here were really just, like, basic things, right?
- 6:48
Like, and so again, these are things that we can-- they're non-deterministic, but the non-determinism is b- non-determinism is sort of bounded. So in this case, we're gonna run 100 iterations of this very simple parsing test, where we're randomly chunking up the input into arbitrary boundaries and just making sure that the parser for this data, um, you know,
- 7:12
can handle arbitrary chunking o- of the text. Similarly here, fuzzy matching algorithm, that's a critical piece of, you know, if the model's fuzzy or, uh, generates something slightly wrong, being able to do this dynamic programming that gets us an, a, a, an approximate match really saved a lot of the tool calling failures for us.
- 7:34
But again, something that could be deterministically tested. And then finally, there's this idea of a streaming diff that as the model is emitting new text, we need to be care- comparing the new text it's emitting with the old text and then making a dynamic decision i- of if I don't see some text, is that in-- that was
- 7:53
present in the old text, is that because it was deleted or it just hasn't been streamed out yet? So that's another, you know, completely, uh, deterministic thing that still was critical to making this system work correctly.
- 8:06
So yet another, like, deterministic test. Um, but then we get into some of the fun stuff with, uh, uh, the behavior of the model itself. So one thing that we noticed right away was, um, the model when it was trying to insert at the top or the end of the document in some cases would just have an
- 8:27
empty old text tag, which like if you're doing old text, new text matching is not very useful. And so we found that we were able to just add this, you know, simple thing to the prompt and improve the eval.
- 8:39
But again, [chuckles] it would still do it, you know, one or two percent of the time. And so just being able to test that, uh, you know, we can handle that case robustly, um, you know, ended up being really helpful.
- 8:52
And this is just like, again, I'm in this case simulating the output of the LLM. Um,
- 8:59
uh, another case that we encountered a lot was that it would mismatch the XML tags, and so we would have old text, and then it would end like this with new text.
- 9:10
Um, and so again, we were able to get a certain distance with the prompt. We initially were sort of 40% of the time we were getting this mismatch and blowing up.
- 9:20
We were able to move that by saying, "Always close all tags properly," to like 95%, but we still have that last like five percent. What do we do with that?
- 9:30
And so again, it's just like driving it into a deterministic test where we have old text, new text, you know, and we need to just assert that even though we got...
- 9:40
You know, we just need to be robust and accept crazy stuff from the LLM, like, uh, in its output. Um, another interesting case was indentation. So here's an eval that, uh, that we wrote, which was this idea of having an outer function, and then inside here there's an inner function, and we have this todo macro here.
- 10:02
And we wanna say, "Replace this todo with return 42," and we would see the model doing things like this, right? Where the indentation level was completely flattened out, right?
- 10:13
It says, "Replace fn_inner," but fn_inner has like all this leading indentation on the front of it. Um, but otherwise, like it's perfectly fine. And so in this case, what we did is this strategy of like detecting this indent delta.
- 10:29
So we figure out basically sort of re-normalize the indent, if that makes sense. So if the text in the buffer is indented by a certain amount but we otherwise match, then we also detect the text in the, that the LLM emitted and compute this delta, and then again, driving it back to a deterministic test of, um, here
- 10:51
we build, uh, a buffer with lorem ipsum dolor sit with this very interesting indentation pattern. And then you can see here, we wanna replace ipsum dolor sit with ipsum dolor sit amet where it's out dented, right?
- 11:07
And you can see that when we actually do our, uh, indentation normalization, we're able to handle that correctly. Um, another tricky thing that we ran into was this weird escaping behavior where in these certain cases where we have weird constructs, like this is a Rust, um, sort of a raw string basically that can include, uh, things like
- 11:30
quotes inside of it due to this interesting escaping syntax. And doing something as simple as saying, just like, "Tweak this," we would notice this is the behavior we want.
- 11:40
We would notice, especially Gemini doing all kinds of crazy stuff like doing HTML escape codes, um, doing this sort of backslash escaping. Yeah, e- with new lines and stuff, we would see like, you know, uh, crazy escaping that it would do, [chuckles] uh, double escaping the new lines.
- 11:59
Um, and so in this case, like we-- that was just a pure prompt fix and didn't really... We didn't try yet, at least, to kind of detect- any of this escaping any further, although obviously, like, that's an opportunity, yeah, to keep going even further.
- 12:15
Um, so yeah, I mean from, from my perspective, like, the lessons we learned from this entire experience is that, like,
- 12:24
rigorous testing is fundamental to building reliable software, period. Uh, we have this new fancy parlance of, like, an eval, which comes, I think, out of the machine learning field of kind of input/output and having lots of examples of that.
- 12:38
But I think a lot of the techniques of just traditional, good old-fashioned software engineering are still really applicable. But we have to embrace this more statistical approach, where we're running it 100 times, 200 times, and asserting a threshold of pass versus fail.
- 12:54
Um, but I think, I mean, this is a real-world example of just the stuff that we saw trying to implement streaming edits or an agent that can go search and edit.
- 13:04
A lot of those problems are not, I don't know, they're not advanced [laughs] machine learning problems or anything. It's just stupid things that the model will try to do that we need to account for.
- 13:15
Um, and so this, I, I think this motion of sort of starting with the zoomed-out eval, then zooming into a sort of stochastic unit test that's still random and interacting with the model but is more focused in on a particular aspect of the experience, and then finally, like driving that even further to an actual good old-fashioned test,
- 13:35
that's been the process that we've discovered. But we so far haven't needed to use, like, special external tools, um, or eval frameworks or anything of like that. It's just in our test suite so far, and kind of using the same underlying infrastructure that's used to do any other kind of software testing.
- 13:54
And honestly, using just a lot of those same skills. Um, so for us, it was like just be empirical, just like we've always been, but it's this new X factor of the LLM doing all these crazy things.
- 14:09
Um, yeah, so that's, that's a little story of how we ... And this is all open source, so you can check it out. Uh, Zed is under the GPL license.
- 14:18
I'd love help improving it all, actually. Um, maybe some of you are like, "What are you doing? You could do better in the following five ways." I'd love, uh, contributions, if that's interesting to folks.
- 14:28
And, um, yeah, it's working. I'm, uh, with the Claude 4 models, I'm finally able to write Rust agentically really efficiently, and I'm just loving it. So, um, that's how we got there.
- 14:42
I appreciate your attention. [audience applauds] [upbeat music]