Abstract
oneimage-csharp is an image library for .NET 10, written from first principles. The shipped package depends only on the .NET base class library and carries its own compression. It has two modes on one engine: one-line thumbnails, crops and conversions, and a lazy, tiled document you can load, edit and render on demand. It is published on NuGet as RecursiveAI.OneImage, with an ASP.NET Core middleware package, RecursiveAI.OneImage.Web.
Kurtis opened the first planning conversation at 12:04 on 21 October 2026. v1.0.0 was tagged 22 hours 56 minutes later, at 11:00 the next morning. Seven codecs, ICC color management, the op set and a fuzzed decoder for every format were in it. v1.1.0 followed 3 hours 25 minutes after that: ImageSharp’s effects, 24 blend modes, perspective transforms, the middleware and a vector drawing engine. This report stops at the v1.1.0 tag.
Agents on Alera did all of the planning, code, benchmarking, documentation and both releases: 66 agent runs and 5,626 model calls. Claude Sonnet 5.5 wrote the code. Claude Opus 5.5 planned, managed, verified, merged and released. At list prices the tokens would have cost about $460. The main findings:
- An agent can run the design interview, if the owner’s own words are kept apart. Kurtis set the direction in four messages. A project manager agent answered the planner’s 50 design questions on his behalf in nine minutes, and every decision in the record is tagged with who made it.
- A tiled engine split into parallel work. Up to nine engineers worked at once on separate units. The codecs, color management and ops of v1 were done in under four hours of agent activity.
- The release gate caught what each unit’s own checks missed: a JPEG encode regression and a concurrency bug, both fixed before the tag.
1. What was built
| Release | What it added | Tagged (US Central) |
|---|---|---|
| v1.0.0 | Lazy, immutable Image and a mutable tiled Canvas with O(1) snapshots. PNG (with APNG), JPEG (progressive, arithmetic, 12-bit, CMYK, shrink-on-load), WebP, GIF, TIFF (with BigTIFF and multi-page), BMP and ICO. ICC v2/v4 color management with a generic CMYK profile, resampling in linear light, geometry, tone and filter ops, compositing, and Image.Thumbnail. Own inflate/deflate and LZW. Size limits on by default. | 22 Oct, 11:00 |
| v1.1.0 | A 5×4 color matrix and ImageSharp’s presets, pixelate, oil paint, vignette, glow, bokeh blur and ten edge detectors. Histogram equalization and dithering, 24 blend modes plus 12 Porter–Duff operators, perspective transforms, seven more resamplers. Paths, fills, strokes, brushes, boolean operations and clipping. The RecursiveAI.OneImage.Web middleware. | 22 Oct, 14:25 |
The common job is one line:
// 400 px wide, upright, sRGB, no EXIF or GPS, as WebP
Image.Thumbnail("in.jpg", 400).Save("thumb.webp");
At v1.0.0 the library was 49,507 lines of C#, with 22,203 lines of tests. At v1.1.0 the core was 64,671 lines and the middleware 2,086. The core project has no package references. The middleware adds only the ASP.NET Core framework reference. The repository had 132 commits at v1.0.0 and 153 at v1.1.0. Agents wrote all of them, under Kurtis’s name as the repository’s author identity.
2. The problem
Kurtis opened the planning conversation with one message:
I want to design the ideal image library for c#. The current options are pretty mediocre, and there’s not really any good reason c# shouldn’t have an amazing image library. … So we’re going to do it all, from first principles. No external libraries.
Nine minutes later he set the scope: “essentially, I want this capable of powering an application like Photoshop if that were built on top of it.” Then he set the shape of the engine:
I want people to be able to generate thumbnails or crop something in code stupidly easily. But also have the ability to load something in and manipulate it in memory, then render it on demand. It’s not either/or
In .NET, the established pure managed library is ImageSharp. It moved to a split license, and its releases after 3.1.12 need a license key to build. The other choices (SkiaSharp, Magick.NET, NetVips) wrap native C and C++ libraries. The design answers both in one engine, modelled on GEGL, the GIMP’s engine. Images are stored as 128×128 tiles, sparse and copy-on-write, under a demand-driven graph of operations. A thumbnail pulls only the tiles it needs, and JPEGs decode at 1/2, 1/4 or 1/8 scale when that is enough. An editor works on the same tiles in place.
The bar for v1 was set in the design Q&A on Kurtis’s behalf: faster than ImageSharp on every benchmark task, with lower peak memory. The longer-term goal was to match or beat NetVips, the .NET wrapper of libvips, on thumbnails. A first checkpoint was within 1.5× of the fastest native wrapper on thumbnail and codec tasks.
3. How the work was organized
3.1 One agent interviewing another
The planner agent proposed a short Q&A. Kurtis corrected it twice first. Its plan to build a shared test kit before the library had oneyaml’s history backwards: “You’re wrong about oneyaml building the kit first. It built rust first, then built the kit based on those learnings.” Then he handed the interview to a project manager agent: “Walk through the q&a … Make sure the q&a is very thorough - prod the planner for more questions if needed.”
The manager pushed the planner from its estimated 6–8 questions to 50, in batches. They covered the API for both modes, tile size, precision and alpha, threading, memory and caching, the depth of each codec, color management, the op list, decoder security, the roadmap, packaging, license and name. It answered each batch as it came, often adding its own amendments. One was that Thumbnail strips EXIF, XMP and IPTC by default, “because thumbnails are usually published and leaking GPS is the classic privacy bug.” The Q&A closed at 12:33, nine minutes after the manager started.
The decision record marks every entry [Owner], [PM] or [Agent default]. The manager insisted on that. When the planner recorded the manager’s roadmap order as the owner’s, the manager corrected it: “that order was your proposal, which the owner accepted in general; record it as a PM decision.”
3.2 The team
Two agent-builder runs created the team in about seven minutes: a build orchestrator and an engineer. A documentation agent wrote the engineer’s user guide, and while doing so it filed a bug in the engineer’s own definition: a template value was not being filled in. The orchestrator was started at 12:43. The roles:
| Role | Model | Runs | Did |
|---|---|---|---|
| Planner | Claude Opus 5.5 | 1 | The design conversation and Q&A, then wrote the decision record and plan |
| Project manager | Claude Opus 5.5 | 1 | Answered the Q&A for the owner, made the decisions above the build, relayed the owner’s |
| Build orchestrator | Claude Opus 5.5 | 5 | Sliced milestones into units, briefed engineers, verified and merged, ran the gate, released. Four handoffs to a successor at about 400k tokens of context |
| Engineer | Claude Sonnet 5.5 | 51 | One unit each: implement, test, fuzz, benchmark, commit on a branch |
| Builders, docs, feedback, browser | Claude Opus 5.5 | 8 | Created and aligned the two agent definitions, wrote the engineer’s guide, processed three feedback reports, set up NuGet publishing in Kurtis’s browser |
3.3 Units in parallel
The core engine and the compression library came first, as two parallel units. Once the codec interfaces merged, at 13:37 on the first day, the work fanned out: JPEG, ICC color, resampling, WebP, PNG, GIF and TIFF ran at the same time, each in its own git worktree. At the peak, nine engineers were running at once. Each unit ended with a report the orchestrator checked before merging. The checks were the build, the full test suite, a NativeAOT publish with warnings as errors, and scans for package references and unsafe code outside the SIMD kernels. Heavy commands ran under a memory cap on the shared workstation.
An engineer whose context filled committed its work, wrote a handoff file and finished. A fresh engineer continued on the same worktree. The orchestrators did the same. The full release gate ran once, in a window with no other work on the host.
4. What the human did
Kurtis wrote no code and reviewed no code. Up to the v1.1.0 tag he sent 17 messages about oneimage: 4 to the planner and 13 to the project manager. Five more that afternoon started a separate PDF library. He never spoke to the orchestrators or engineers.
4.1 Setting direction
The four planning messages set the goals, the dual-mode engine, “Cpu-only, gpu ready”, and “Not the same performance targets” as oneyaml. Everything else in the design was settled by the agents and recorded as theirs.
4.2 Decisions only he could make
- The benchmark reference. ImageSharp 4.x needs a license key to build, even in a benchmark project. The orchestrator recommended applying for a free community key. Kurtis: “no imagesharp key. We just won’t run those benchmarks.” Every comparison uses ImageSharp 3.1.12, the last release that builds without one, and the published numbers say so.
- The name and the release. v1 was ready, held, at 03:51 on 22 October. At 08:23 Kurtis wrote: “Go with oneimage as the name. Ship v1.”
- The package ID. At 10:27 he proposed
RecursiveAI.OneImage: “it also does a bit of promoting for us.” The orchestrator renamed the package before the tag. - Publishing credentials. “drive my browser to set it up. I’m signed in”. A browser agent added the nuget.org Trusted Publishing policy in his signed-in browser about a minute later.
4.3 Changing the priority
Before approving v1 he asked, “Does v1 cover everything that ImageSharp does?” The manager said no and listed the gaps. Its advice was to fill the small effects in a later 1.x release. Kurtis made parity the next priority instead:
getting people to switch from ImageSharp is an opportunity right now. We don’t need to duplicate their formats, but definitely want the middleware, drawing and text, effects, blend modes, perspective transforms.
The orchestrator planned the parity work as five parallel op-family units plus the middleware, started them in branches before v1 was tagged, and merged most of them within ten minutes of the tag. That is why v1.1.0 came three and a half hours after v1.0.0.
4.4 Pointing at what the agents didn’t know about
A macOS-only test failure held up v1.1.0 while the agents probed it through repeated CI runs. Kurtis: “You have access to a mac locally.” The fix was confirmed on that machine.
5. Verification
5.1 Against reference implementations
Each unit tested against an outside reference where one existed. The agents reported these results (the agents’ own account, from the orchestrator’s reports):
- JPEG decode is bit-exact against libjpeg-turbo at every scale. Baseline encode is byte-identical to cjpeg.
- CMYK to sRGB has a mean CIEDE2000 error of 0.19 against an ICC-correct reference. This one is in the published benchmark page, with the other libraries’ figures (section 7.2).
- The color presets and histogram equalization match ImageSharp 3.1.12 within 1 level at 8 bits. Pixelate, the edge detectors and the default dither match exactly. Oil paint differs on purpose: it centers its window.
- The full suite passed with 1,618 tests at release prep, and again under ARM64 emulation. With the first parity units and the middleware merged, 2,017 passed.
5.2 Fuzzing
The repository has a fuzz target for each input surface: JPEG, PNG, WebP, GIF, TIFF, BMP, ICO, ICC, EXIF, inflate and LZW. The final campaign ran each one for 10 minutes on the finished v1 code, with no crashes, hangs or out-of-memory failures (agents’ record). An earlier campaign found two memory problems a long-running server would hit, and both were treated as release blockers (section 6.2).
5.3 The release gate
The gate is one script. It runs BenchmarkDotNet for time and one process per task and library for peak memory, and writes a report. The ground rules were fixed before the final run. Ties are published as ties, never as wins, and an outside load above about 2 stops the run. The final run took 00:19 to 02:00 on 22 October, on code that every engineer had finished with. A re-run of the rows changed afterward took 02:29 to 02:58. Both raw reports are in the repository.
5.4 Four operating systems
CI covers Windows x64, Linux x64, Linux ARM64 and macOS ARM64. It first ran after the repository went public on the morning of 22 October, and on that first run it found a real bug (section 6.2). v1.0.0 was tagged on a commit with CI green on all four.
Run the gate, and every OS, before the tag rather than trusting unit-level checks.
6. What went wrong
6.1 A host restart
Kurtis’s workstation had to be restarted on the first afternoon, and no agent ran for 5 hours 25 minutes. No library code was lost. The private planning repository was caught mid-commit: its branch ref, index and five objects were zeroed. The orchestrator repaired it from the intact working files. Fuzz corpora and partial benchmark runs were redone. Every engineer was resumed on its own instance and told what had happened.
6.2 Defects the gate and CI found
- A JPEG encode regression. The full gate run had JPEG encode at 145.2 ms against ImageSharp’s 100.6 ms. An earlier change had switched on optimized Huffman tables above 4 megapixels, and that path ran serially. With it parallelized, the re-run measured 87.4 ms, with byte-identical output at any thread count.
- A race in the TIFF block cache. The first CI run failed on Windows and ARM64. When a cache clear (run after every save) overlapped a block being published, the block was released twice, and a reader could see recycled memory. A new stress test failed 30 of 30 runs before the fix and passed 200 of 200 after.
- Two memory problems from fuzzing. The ICC transform cache was unbounded, at about 0.8 MB for each distinct embedded profile, and GIF decoding held about 1 GB in a long-running process (agents’ figures). The cache became a 32 MiB byte-bounded LRU. The GIF fix came with a 400-iteration soak test that shows steady memory.
6.3 A test that measured itself
A streaming-memory test failed intermittently on macOS only, and it held v1.1.0. The project manager’s rule was to fix causes and never raise the bounds. The cause turned out to be the measurement, not a leak, and the limits did not change. Finding it took several CI loops, and one probe hung because dumping the process deadlocks on macOS. Then Kurtis pointed out that a Mac was available.
6.4 Where the 1.1 numbers trail
Two clipping benchmarks trailed ImageSharp.Drawing 2.1.7 at the tag. Subtracting a 1000-point polygon from a star took 158 µs against 134 µs, while allocating a sixth as much. A blur clipped to a circle in place on a Canvas allocated 9.6 MB, where the same clip on an Image allocates 52 KB and ImageSharp.Drawing 423 KB. Both rows were in the drawing documentation at the tag. The benchmark page and the release notes listed them minutes after. The allocation fix landed after the tag and is on the 1.2 list.
7. Results
7.1 Timeline
| When (US Central) | What |
|---|---|
| 21 Oct, 12:04 | Kurtis opens the planning conversation |
| 12:24 – 12:33 | The project manager answers 50 design questions |
| 12:43 – 12:48 | Orchestrator started; first commit |
| 13:37 | Core engine and compression merged; the codecs fan out |
| 14:39 – 20:04 | Host restart, no agent activity |
| 21:51 | All codec, color and op milestones done |
| 22 Oct, 00:19 – 02:58 | Release gate run and re-run of changed rows |
| 03:51 – 08:23 | Ready; waiting for the name and the go |
| 09:25 | Repository public; first CI runs on four operating systems |
| 11:00 | v1.0.0 tagged and published to NuGet |
| 14:25 | v1.1.0 tagged; core and middleware published |
Dates come from git tags, the agents’ messages and the model-call records. Leaving out the restart and the overnight wait for the owner, agents worked for about 13 of the 23 hours to v1.0.0. When Kurtis asked how big the project was, the manager estimated that v1 would land at 65–80 thousand library lines. It shipped at 49.5 thousand.
7.2 Benchmarks
Every number below comes from the published performance page and its raw reports. They were measured on an AMD Ryzen 9 7950X3D (16 cores, 32 threads), on .NET 10.0.11, with AVX-512 off so the numbers describe the AVX2 path every x64 server has. The machine is a shared workstation, not a lab host. Load is recorded every minute, between 1.0 and 3.3 during the gate run. Time is the BenchmarkDotNet mean. Peak is the growth of resident memory during the operation, as a median of three processes. A time within 5% counts as a tie. The rivals are ImageSharp 3.1.12, SkiaSharp, NetVips and Magick.NET through their .NET packages. System.Drawing was not benchmarked: System.Drawing.Common is Windows-only, and it appears only in the migration guide.
Against ImageSharp, OneImage is faster on 27 of 29 tasks. Peak memory is lower on 24 and a tie on 3. A selection:
| Task | OneImage ms | ImageSharp ms | vs ImageSharp | Fastest native ms | Peak MiB, ours / ImageSharp / NetVips |
|---|---|---|---|---|---|
| 24 MP JPEG → 400 px thumbnail | 37.5 | 47.0 | 1.25× faster | Magick.NET 40.3 | 2.2 / 1.2 / 64.4 |
| 24 MP JPEG → 400 px WebP | 46.9 | 65.4 | 1.39× faster | Magick.NET 46.0 | 3.0 / 9.5 / 64.2 |
| PNG → WebP (natural) | 79.1 | 1,511.5 | 19.11× faster | NetVips 155.1 | 41.9 / 86.8 / 121.6 |
| JPEG decode | 41.2 | 77.2 | 1.87× faster | NetVips 102.3 | 0.5 / 3.5 / 360.6 |
| JPEG encode | 87.4 | 100.6 | 1.15× faster | NetVips 55.3 | 5.3 / 4.1 / 67.3 |
| PNG decode (natural) | 11.4 | 39.1 | 3.43× faster | SkiaSharp 19.6 | 0.2 / 0.3 / 90.3 |
| PNG encode (natural) | 219.6 | 1,064.3 | 4.85× faster | NetVips 96.0 | 11.2 / 33.6 / 24.1 |
| WebP lossy encode, 2000×1500 | 101.6 | 590.5 | 5.81× faster | Magick.NET 162.3 | 24.2 / 97.1 / 73.1 |
| Animated GIF resize | 40.4 | 47.1 | 1.17× faster | Magick.NET 802.4 | 2.4 / 2.6 / 81.6 |
| Multi-page TIFF read | 84.3 | 586.7 | 6.96× faster | NetVips 28.1 | 2.5 / 54.7 / 85.1 |
| CMYK JPEG → sRGB | 243.4 | 148.3 | skips ICC | NetVips 252.0 | 2.2 / 1.3 / 441.1 |
The two thumbnail rows also beat NetVips, at 37.5 against 49.5 ms and 46.9 against 55.1 ms, with under a twentieth of its peak memory. That meets the longer-term goal on the thumbnail tasks measured.
Where we lost. These are stated as the performance page states them:
- CMYK to sRGB is 1.64× slower than ImageSharp, which skips the embedded ICC profile. Its colors are off by a mean CIEDE2000 of 11.3 (maximum 27.2), against 0.19 for OneImage, 0.885 for NetVips and 0.889 for Magick.NET. Among the libraries that convert, OneImage is the fastest: 243.4 ms against NetVips’s 252.0 and Magick.NET’s 1,015.9.
- The JPEG thumbnail uses 1.0 MiB more peak memory than ImageSharp, 2.2 against 1.2 MiB. It is the one published peak exception. Five repeats on an earlier commit gave the same range, so it is an old gap, not a regression.
- Ties: the watermark composite on time (4.9 against 4.9 ms), and BMP decode and 2× upscale on peak. The JPEG encode peak (5.3 against 4.1 MiB) is inside its own run-to-run spread of ±3.2, so the page calls it a tie and doesn’t claim it as lower. The gate report’s stricter 5% rule lists it as a miss.
- Four rows are beyond the 1.5× checkpoint against the fastest native wrapper:
- multi-page TIFF read, 3.00× NetVips at the default limit of four threads per call (1.84× with all cores);
- PNG encode of the natural-content input, 2.29× NetVips;
- GIF decode, 2.23× SkiaSharp, which is 2.3 against 1.0 ms;
- JPEG encode, 1.58× NetVips, down from 2.72× before the gate fix.
These numbers were measured on the v1.0 code. The performance page and raw reports are identical at both tags. The 1.1 drawing rows are local smoke runs against ImageSharp.Drawing 2.1.7, at host loads from 0.4 to about 10. Fills, strokes and brushes were faster on every row, for example a 1000-point polyline at 1.15 against 8.87 ms. Clipping is covered in section 6.4.
Publish ties as ties and say which comparison isn’t like for like. The CMYK row would read as a loss without the color-error figures, and as a win if it were simply dropped.
7.3 Cost
Every model ran through a subscription, so the recorded spend is $0. The figures below are what the same tokens would have cost at each model’s list price. They come from the platform’s run and call records, from the first planning message to the v1.1.0 tag.
| Role | Runs | Model calls | At list prices |
|---|---|---|---|
| Engineers (Claude Sonnet 5.5) | 51 | 4,514 | ~$270 |
| Build orchestrators | 5 | 782 | ~$140 |
| Project manager | 1 | 162 | ~$39 |
| Planner, agent builders, docs, feedback, browser | 9 | 168 | ~$11.50 |
| Total | 66 | 5,626 | ~$460 |
That is about 31 million input tokens, 8 million output tokens and 972 million cache reads. The engineers averaged 91 turns each.
8. Recommendations
- Let an agent run the design interview for the owner, and tag every decision with who made it, so the owner’s own words stay distinguishable and anyone can revisit an agent default on evidence.
- Fix the gate and its tie rules before the final run, and run it once, in a reserved window, with the load recorded.
- Design for parallel units. Stable interfaces early (core, then codec interfaces) let seven or more engineers work without stepping on each other.
- Run CI on every target OS before tagging. The first four-OS run found a bug no Linux check had.
- Fix causes, not bounds. The macOS failure was the measurement. Raising the limit would have hidden that.
- Keep the human for what only he can decide or knows about: the name, money, licenses, the priority after v1, and the hardware in the room.
Limits of this report
Run counts, models, tokens and cost come from the platform’s run and call records, cut off at the v1.1.0 tag. Calls after the tag, from runs that continued into the 1.2 work, are excluded: about 270 calls and $33. The project manager also managed the start of a separate PDF library in its last hour before the tag. Its sub-agents for that are excluded, but its own calls in that hour (about $8.50) are counted. Dates, line counts and commit counts come from git. Line counts include comments and blank lines. Benchmark figures come from the published performance page and the raw reports in the repository. The test counts, fuzz results and reference-match claims in section 5 are the agents’ own reports and were not re-run for this report.
What isn’t shown yet:
- “Photoshop-class” remains a goal. Layers, PSD and text are on the roadmap, not in these releases.
- There are no ARM64 timings; ARM64 is checked for correctness only, under emulation.
- ImageSharp 4.x was not measured.
Work continued after the cut-off: the text package (font parsing, shaping and line breaking), the remaining piece of ImageSharp parity, is in progress for 1.2.
Links
- oneimage: recursiveai.org/oneimage
- oneimage-csharp: github.com/recursive-ai-oss/oneimage-csharp
- Performance page: docs/performance.md at v1.1.0
- NuGet: RecursiveAI.OneImage and RecursiveAI.OneImage.Web
- Earlier case studies: Building oneyaml-rust with a team of AI agents and Building oneyaml-go and oneyaml-csharp with a team of AI agents