Brian Hung
Software Engineer. At the intersection of human interfaces and model harnesses.
Personal Software
YapGPT
Chat with your favorite creators.
Importing a profile
Workflow, one per posteach step checkpointed, retried on its ownWorkflow, one per videoreads the description, never the video twiceContent storeUserMedia APIProfile + postsFetchDescribeEmbedD1R2VectorizeReadFilterLocatepastes a linkWorkereverything behind itpage, captionsGeminione per segmentrows, segmentsimages, videosegment vectorswhat was seen and saidnotable places onlyGoogle Mapslinkexpandeach postrows, media, vectorsdescriptionsplaces
Answering a question
Content storeUserQuery APID1R2VectorizeRank fusionGeminia questionWorkerrows, segmentsimages, videosegment vectorsone post, its segmentswrites the replyaskFTS5semanticcandidatescontextanswer, with citations
Prism
Multimodal media enrichment on Cloudflare, the shared backend that YapGPT and other apps build on.
Ways in and out
one per media itema hash seen before is linked, not redoneScrapeWorkflowa whole profileIngestWorkflowone URL, text or fileENRICH_Quploads, fair per ownerTransformWorkflowdedup-check, describe, embedD1 · R2 · Vectorizemedia rows and enrichment JSONbytes by sha256, 1408-dim vectorsPOST /v1/searchsimilar, places_likeGET /v1/placespins for a mapPOST /v1/chatRAG over an ownerGET /v1/r/{url}LLM-ready text
Enriching one media item
not yet enrichedLoad and hashsha256, mime, EXIFdedup-checkhash enriched before?Already enrichedlink the run, stoppersist-bytesR2 key by sha256, media rowdescribeGemini, image or videoembedVertex, best effortpersist-segmentswindows: FTS rows, vectorspersist-enrichmentenrichment JSON, vectorresolve placesGPS, or agentic if askedEach box is a checkpointed step. Steps hand on R2 keys, never bytes.
Pinning a place
matches, each with a gatepins onlyAgentic resolvermodel searches PlacesExtract and geocodefallback, and non-imagesVideo placesbackfill-video-places.tsenrichments.placesgate: auto, review or hiddena pin is auto, with a locationplaces_ftssearch by wordssegment_placesmoments to windowsPLACE_VECTORSPOST /v1/places/indexGET /v1/placespins for a map
Finding places for an answer
vectorwordsthree rankingsRetrieved windowseach post's best firstThe questiontext and its vectorsegment_placesplaces in those windowsrankByMeaningreorders, never addsplaces_ftsname, address, what was saidFuse by rankRRF, keeps the top 40Attach to postsplus up to 8 posts that did not rank
Palate
An iPhone app that turns your photos, shares and screenshots into a private map of places, with a concierge, Atlas, that plans around them.
Reading a photo
Cloudevery photoPOST /v1/photos/upload/v1/sync/changesbelow 0.80.8 or moreNew photospicker, share, screenshotOCR and resolveVision, /extract-placesSuggestionDbPhotoPlaceSuggestionImageUploaderbackground URLSessionPhotoAppliercaption, tags, placesprismcaption, tags, storefrontreconcileEnrichmentcron, plus GPS nearbyLinked placeDbPlacePhotoEnrichment comes back through sync, so the places reach every device, not only the one that uploaded.
Syncing the library
iPhonetaste-apiWorker and Postgresoutbox, once signed inack clears pendingOpIdchanges since cursoroptimisticupsertappendread in orderSyncEngine.writedomain row + OutboxItemLocal SQLiteGRDB, what the UI readsEntityApplierskips un-acked rowsapplyMutationsPOST /v1/sync/mutationssync_logPostgres, ordered by seqgetChangesGET /v1/sync/changesSync runs on launch and after every write. Pulls run for everyone; the outbox waits for a real account.
Asking Atlas
iPhonetaste-apiCloudflare Workerturn, tool resultsevents, client-tool-callstreamTexttext, tool callssendPushDevice toolsGPS, calendar, my placesRemoteChatClient/v1/chat/connect socketPush notificationreply arrives via syncServer toolsweb_search, search_placesChatAgenta Durable Object per chatBackground researchDO alarm, no devicegemini-3.7-flashAI SDK streamTextDevice tools run on the phone mid-turn. Research runs later, with no phone, and pushes the answer.
Visual Lookup
GeoGuessr agent that looks up places from text or GPS data in your photos.
Architect and editor
Editorsearches through the candidatesimageplaces to look forcandidatesno match:search againa strong matchmore than one placePhotocaption, signage, GPSArchitectreads the imagesearchPlacesa name or descriptionnearbySearcha place type, in the areafilterPlacesevidence, and how stronganswerthe place, and whythoughtbefore trying againfilterPlacesVisuallycompares their photos
The loop every harness runs
viewmodeltoolupdatestateeverything rememberedpromptwhat the model seesactiona tool callresultwhat the tool returnsThe model draws one arrow. The harness draws the other three.
Dispatch
Run Claude Code and Codex on your own machine, and drive them from any browser.
spawnssession WebSocketeach promptWeb appphone or laptopWorkerhono HTTP routesMachineDOsends create-sessionDaemonlocal or cloud sandboxSessionDOseq and replay logSessionRunnerone per sessionHarnessAgentClaude Code or CodexThe browser and the runner each hold a socket to the same SessionDO. It numbers every event andkeeps a replay log, so a client that reconnects receives only what it missed.
Agent Infrastructure
ai-sdk-harness
Sandbox providers and durable runners for the Vercel AI SDK harness, published as separate npm packages.
Moving a working tree between sandboxes
createSession()spawn: tar czf -writeBinaryFile, then tar xzfLocalSandboxProviderchild_process and fsDockerSandboxProviderdocker exec in a containerCloudflareSandboxProviderSandbox Bridge HTTP APISession in sandbox Aat a turn boundarySession in sandbox Bfresh, from any providercheckpointSandboxstreams out, stages nothingCheckpointStorememory or R2, by keyrestoreSandboxoverlays the treeOnly the file tree moves, not running processes. Files made since the checkpoint are left in place.
Running a turn on any engine
session.call: the state as JSONready_for_next_step: run the next slicerunHarnessSessionpure orchestration, JSON onlyCloudflare WorkflowsworkflowDurableSessionTemporaltemporalDurableSessionIn-processlocalDurableSessionharness:sliceup to 750 s of the turnSessionLogchunks, per sliceagent in its sandboxClaude Code or CodexEach slice is one checkpointed step. Between slices the turn is suspended as JSON, so a restartresumes from the last checkpoint and the next slice reattaches to the sandbox that is still running.
One Durable Object per session
prompts, answersevents, permission requestssingle-threaded: no locksevery frame, with its seqClientsphone, laptop, chat threadsRunnerexactly one, fenced by generationAgentSessionDOappendAndFanout: the log assigns the next seqdurableObjectSessionLog, on the DO's SQLiteConnected clientseach frame, liveReplayfrom a lastSeq cursorSSE readersLast-Event-ID cursorExport and fork?until=N, ?seal=1Text deltas go out live as text-chunk frames and are stored once, as one assistant-message.
Serving local tools to a remote session
Outcomesevery call ends in exactly onehost-tool-callhost-tool-resulttool-calltool-resultRunnermay live in a cloud sandboxAgentSessionDOone host lease, per-call budgetsconnectToolHostrelay-host, on your machinecompletedthe result, capped at byteBudgetunavailableno host attached: safe to retrytimeoutdeadlineAt passed: call cancelledambiguoushost left mid-call: never retriedA repeated callId replays the journaled result, so the host never runs a call twice.
Evals
A TypeScript framework for evaluating language models, built on the Vercel AI SDK.
The platform
online scorers rate each call as it landsProvider proxyswap a base URLAI SDK appstraceAISDKModelOpenTelemetryOTLP to /v1/tracesOther proxiesPOST /api/captureevals serveone store you host: files, SQLite or D1viewer · review · feedback · API · MCPScorejudges over stored callsReplaycalls become eval dataRun and gateCLI, vitest, CIExportOTLP, webhooks, HTMLRuns execute locally, on Temporal, or on Cloudflare Workflows, and write back to the same store.
The loop, as Visual Lookup runs it
prompta record per photomissesa better promptcandidate promptGEPA proposes oneevals runone run per candidatescorescity and name, half eachreflectionreads why each miss missedgolden set222 photosPlaces responses are cached, so scoring another candidate costs only model tokens.
Dataset, runner, actor
Runnera few examples at a time; a rerun skips what is doneexampleappendedat the endDatasetid, input, expectedactor.act(example)returns metrics, metadataSamplerthe model, every call tracedresponsesstored as JSONLtraceswhat the model sawactor.aggregaterun metricsReportersconsole, webhook, CI gate
With a sandbox and an external harness
Runnera few examples at a timeSandboxa fresh one per exampleat the endDatasettask, repo snapshotrestore the repofrom its snapshotHarnessAgent.generateClaude Code or Codexgraderstests and checks, in the sandboxtracesreportGenerateResultresponsestests passed, the diffactor.aggregaterun metricsReportersconsole, webhook, CI gateThe harness runs its own tool loop in the sandbox. The actor sees its stepswhen the turn ends, and grades the workspace before the sandbox is destroyed.
Geoguessr Arena
Evaluation of various LLMs on street view photos.
Playing a round
One run per modelParallelRunnerGWS15k sample128 of 15k panoramas, by seedgetStreetViewContentPartthe panorama, cached in R2Single shotone guess at temperature 0N-shotn guesses, geodesic medoidTool callinggetStreetViewPhoto, answerScore the guessdistance in km, correctCountryanalyze-cross-modelranks runs, 95% CIgpt-5, claude-sonnet-4, gemini-2.5-pro and grok-4 each play the same 128 panoramas.The leaderboard ranks by correct country first, then by mean distance.
Grading a screenshot answer
matchMatchesLabelnonononomatchThe agent's picka place_id from its searchesAudited labelsGoogle ids on 361 of 369Same place_idthe exact Google listingWithin 150 mof the label's coordinatesSame street address70% of tokens, same numberName tokens overlaphalf of them, within 25 kmCorrectgrade 1Missgrade 0A negative row, with no real place, is right only when the agent answers none_found or below 85 confidence.
Spreadsheet SWE-bench
A SWE-bench-compatible benchmark where each task is implementing one spreadsheet function in the IronCalc Rust engine.
Workdira fresh clone per prediction, deleted afterinstanceone row of the datasetpredictioninstance_id, model_patchpath checkgold's files, never testsgit cloneIronCalc at base_commitsetup_patchstubs out the functionmodel_patchthe prediction's diffreset the teststhen apply test_patchcargo testFAIL_TO_PASS, PASS_TO_PASSresolvedif every test passesSynthesis tasks start from a working engine, and setup_patch takes the function out.
Harness
MCP servers for prime-agent's recursive language model and continual harness, usable from any host.
rlm_deep_querythe context never enters the model windowquestionanswerrlm_store_contextfull textcodestdout, first 4,000 charssliceanswerMCP hostClaude Code, Codexcontext storerlm_store_contextroot modelgenerateText tool loopPython kernelPROMPT holds the contextsub-callone depth downThe answer is the model's reply, or FINAL from the kernel when it is too long to write out.Below RLM_MAX_DEPTH a sub-call is a child RLM with its own kernel; at it, one plain model call.On Docker and Cloudflare, the model calls a recurse tool instead.
Composer
A web app that runs Claude Code in a Cloudflare Sandbox and talks to it over the Agent Client Protocol.
Starting a task
Cloudflare Workercalls sandbox.execPOST /api/startnot yetalready runningsandboxIdChatEditorhandleSubmit, new taskIdupdateSandboxIdthe task gets its sandboxACPTaskProviderinitialize, newSessiongetSandboxa container per sandboxIdisWebsocketdRunningcurl localhost:8080setupWebsocketdnpx claude-code-acp on 8080setupWebsocketd polls every 200 ms, for up to 3 s, until websocketd answers.
Sending a prompt
Browserthe task pageSandboxa containersendPromptstdinstdoutsessionUpdateChatEditorreact-prosemirrorClientSideConnection@agentclientprotocol/sdkZustandACPClientwrites into useACPStoreWorkerwsConnect to port 8080websocketdWebSocket to stdioclaude-code-acpClaude Code over ACPReplies ride the same socket: message chunks, tool calls, plan updates and permission requests.
Oncall
One inbox for the decisions that many Claude Code and Codex sessions need from you.
a Candidatea cardyour answerPreToolUse hookPOST /classifyhub.requestDecisiona worker asks for a callclassifyrules in triage-policy.yamlAUTOapproved, never shownINBOXa decision cardFRICTIONa deliberate confirmdecision busholds the call until you answerInboxone card at a timeRules pick every lane. An optional model call (TRIAGE_LLM=1) refines INBOX cards and never moves one.
mcp-components-ui
Turns a team's React components into MCP tools that render in chat and return what the user chose.
expense({ … })json-render specthe user clicksthe answer anda confirmTokenModelpicks a componentMCP serverZod-checks the propsChat hostsandboxed ui:// iframesubmit_answercalled by the UIcomponents/ui/one tool per fileThe model never calls submit_answer. A guarded write needs the token that only a click mints.
Cloudflare computer-use
A remote MCP server that gives a model a real Linux desktop and browser inside a Cloudflare Sandbox.
desktop toolsbrowser toolsMCP clientClaude Code, CursorWorker /mcpcomputerUseMcpFetchBun daemon:8788, one X11 linkX11 on XvfbXTest, MIT-SHMPlaywright sidecarChrome over CDPThe Worker reaches the container over containerFetch. The daemon holds onepersistent X11 connection; the browser tools run in a separate Node sidecar.
Browser cookies daemon
A Bun daemon that imports a browser's cookies into a remote sandbox and keeps them in sync.
reads prevon pushSourcechrome, firefox, CDPFilter + dedupeallow-list, identityDiffagainst the snapshotSinkCDP, HTTP, WebSocketSnapshotcookieKey → value hashIn watch mode the source reports each change; only real changes are pushed.
Atlas
An agent-native super app where each user gets a persistent VM, a durable database, and mini-apps installed as packs.
deniedMini-app UIcalls bridge.call(cap)Capability checkgrantedCapabilitiesTransportin-process · WebKit · mockHosthonours the granted callRefusednot-declaredUser's rowsSQLite in a DOWorkspacefiles in the VMAgentprompt · transcriptThe bridge refuses any capability the manifest did not grant, then a transport carries the rest to the host.
auto-distill
Turns Claude Code and Codex sessions into reusable skills, scored against the work people already did.
fitness, 0 to 1top 3revisionscandidate skills6 drafts from one taskmixedScoretests, and a rubric judgerank by fitnessrankAll, keep the bestrevise the bestalone, and pooledheld-out taskswhat the expert didbest skillsaveSkill to sqliteThe model writes every candidate; only the grounded score decides which survive.The loop stops at 100% or after three rounds; publishing the best skill is a separate, human step.
Claude Code Proxy
A Cloudflare Worker that lets Claude Code run on any model the Vercel AI SDK supports.
Cloudflare WorkerPOST /v1/messages, a Hono routeSSE eventsClaude Codethe CLI, unchangedtoModelMessages, toToolsAnthropic JSON to AI SDKresolveModelIdsonnet to SONNET_MODEL_IDwrapLanguageModelswaps in native web searchtoAnthropicStreamAI SDK parts to Anthropic SSEproviders.languageModelopenai, google, xai, anthropicWithout stream: true, generateText and toAnthropicResponse return one JSON message.
Collaborative Software
Coauthor
A multiplayer markdown editor where people and Claude Code agents edit, comment, and reply as peers.
CRDT changestool callsBrowser editorProseMirror on AutomergeClaude Coderemote MCP clientagent-hostresponder.ts, coder.ts/syncAutomerge sync, WebSocket/mcpdoc_* tools over lib/opsthe same Automerge docNode repo or a Durable Objectcomments anchored as marksAgent edits reach every open tab over /sync, and browser edits reach agents on their next call.Each tool reply carries _heads; passing them back returns a diff of what peers changed.
Spreadsheet MCP
An MCP server for spreadsheets whose formulas IronCalc really evaluates, with a live demo where an agent and a person edit the same sheet.
One core, two runtimes
src/coreno I/OClaude Desktop, Cursorlaunches it over stdioRemote agentMCP over HTTP or SSEsrc/local/stdio.tsMemoryStore, optional WB_DIRworker.ts, WorkbookDOone Durable Object per idcreateSpreadsheetServer20 tools, spreadsheet://{id}Workbook.edit()stamps the author, logs a versionIronCalc engine (WASM)real formulas, .ic and .xlsx
An agent and a person on one sheet
WebSocket diffsMCP tool callsYour browser tabthe @ironcalc/workbook gridChat paneluseChat, POST /api/chatHarness, server.tsstreamText, MCP client over SSEWorkbookDOone Durable Object per workbook idserializes edits, stamps user or agentOther tabsapplyExternalDiffsMCP subscribersresources/updatedChange feed/changes for hooksDO storagethe workbook as .icThe server owns the sheet and its tools. The harness owns the agent loop.
Workflow
Workspace for integrated chats and notes between you and your LLMs.
Storing chats and notes
changesNote editorProseMirror, syncPluginChat threadappendToChatMutatorAutomerge Repoa root doc lists every note and chateach one is its own documentIndexedDBIndexedDBStorageAdapterOther open tabsBroadcastChannelCloudflare WorkerDurable Object storageStorage and peers sync both ways. The arrows follow a local change.The dashed link runs only when VITE_WEBSOCKET_CLIENT_ADAPTER is set.
Editing a note with the model
messagesa tool callan editthe doc, as JSONmergeBranchChatSideBaruseChat, POST /api/docstreamTextAnthropic, OpenAI, GoogleonToolCallruns tools in the browserBranch in the editorrepo.clone of the noteThe noteuntouched until mergedThe model's edits land on a clone of the note. mergeBranch folds them in only when the user asks.
Chat with Spreadsheet
Spreadsheet that can be used normally, or with LLMs.
Editing a cell
the cell's textbatchCellContentschanged cellsvaluesUpdatedGlide data gridDataEditor, provideEditorFormulaEditorCodeMirror, Lezer grammarcolors each cell referenceHyperFormula, in a workerrecalculates dependent cellseditorView.changessent with the next messageapi/autocompletea model suggests formulasThe Lezer parse drives the editor. HyperFormula parses the formula again to evaluate it.
Asking the model
messagesa tool callA1 rangescell valuesvaluesUpdatedChatBoxadds sheets, selection, editsstreamTextapi/chat.ts, an edge functiononToolCallruns tools in the browserHyperFormula workerthe engine the grid usesGlide data gridrepaints the changed cellsThe model picks each tool. The browser runs it on the same engine the grid uses.
Threads
A social note-taking app: write posts in a rich-text editor, gather them into lists, and follow and like other people's notes.
How the services fit together
HTTPS, WebSocket/auth/hasura/collabwebhookusersdoc_stateNext.js appApollo Client, ProseMirrornginxroutes by pathauth-serviceGo: sign up, sign inHasuraGraphQL over Postgrescollab-serviceNode, live Yjs docsredis-authrevoked tokensPostgres 13users, posts, lists, likesredis-collablive doc updatesThe six services run from one docker-compose.yml; nginx on the host routes to them.
Saving a live edit
local copyYjs update, WebSocketeach updatebroadcastbindState: load when openedwriteState: last editor leavesProseMirror editorySyncPlugin makes Yjs updatesIndexedDBy-indexeddbWSSharedDoccollab-service, one per postredis-collaby-redisOther editorssame postPostgres postsdoc_state and contentOnly the post's owner can join its room, so the other editors are the owner's other tabs and devices.
Write
Text editor built on ProseMirror.
Building an editor from extensions
schemapluginsnodeViewsstateextensionsNode(), Mark(), Extension() or a PluginschemaFromExtensionsNodeSpecs, MarkSpecs, attrsdefaultPluginsinputRules, keymaps, pluginsNodeViews, MarkViewscustom DOM per node typeEditorState.createschema, doc, pluginsEditorextends EditorViewInput rules run first, then each extension's keymap, then ProseMirror's baseKeymap.
Editing a selection with AI
a docpromptmarkdown replya doctoMarkdownfromMarkdownstate.selectioncontent() as a docMarkdownSerializerselection to markdown/api/chatan LLM via streamTextExtension packagesheading, bold, link, …MarkdownParsermarkdown-it, fromMarkdownReplace selectionor insert below itdemo/markdown.ts gathers each package's toMarkdown and fromMarkdown into one serializer and one parser.
Automerge PartyServer
Automerge documents synced over WebSockets to a PartyServer Durable Object, which keeps them in its own storage.
AutomergeServerCBOR over WebSocketrelayed as issync messagessave, loadRangeBrowser tabBrowserWebSocketClientAdapterOther browsersin the same roomWorkerWebSocketAdapterone per connection, onConnectonMessagebroadcast to the othersRepopeerId worker-<ctx.id>DurableObjectStorageAdaptersplits and joins by keyctx.storagea value per 128 KB chunkroutePartykitRequest sends each room to its own AutomergeServer Durable Object.
git-zero
Browse and chat with a GitHub repo, cloned into the browser and stored as Zero rows.
Cloning into the browser
Browser tabthrough a CORS proxyz.query.fsNodegithub.comowner/repo from the URLgit.cloneisomorphic-git, depth 1createFS(zero, dir)a Node fs over Zero rowsZerofsNode rows, in IndexedDBFile treerows to nested foldersCodeMirroropens a file in a tabA repo already in IndexedDB is read back from Zero without cloning again.
Asking about the code
messagestool schemastool callresult, next requestuseChat@ai-sdk/reactPOST /chatstreamText on a WorkerGemini 2.0 Flash@ai-sdk/googleonToolCallglob, grep, list, readcreateFSthe repo, as Zero rowsThe Worker declares the tools with no execute, so the browser runs them against the repo in Zero.
Experiments
Fieldnotes
Turns a photo into an engraved field-guide plate, captioned with where and when it was taken.
Workerthe photo,not redrawnpreparePhotodownscale to 1536pxreadExifGPS, capture dategenerateImageart from the photogenerateTexttitle and tagsreverseGeocodeGPS to a place namerenderPosterphoto, paper, art, caption on a canvasThe model draws the art and nothing else. The photo, paper and caption are painted in the browser.
Chinese Food Embeddings
Collects photos of 307 regional Chinese dishes and plots them with eight image embedding models, to see which model best tells the cuisines apart.
accepted photos<model>.jsoncollect.tssearches social postsAesthetic scoredrops under 4.5classifier.tsGemini: right dish?dataset.json307 dishes03-embed.py8 models, per-dish mean04-umap-export.py2D layout, silhouetteEmbeddingScatterdishes colored by regionEach dot is one dish, the average of its photos. Switch models to see which keeps each region together.
Cadence
If Linear made a music player.
Loading the library
addAlbumaddArtistalbum.jsonpublic/albums/<name>/<name>.jsonpublic/artists/importAlbumfetch, resolve file urlsimportArtistfetch, resolve photo urlMusicStoreMaps of tracks, albums, artistsids are uuid v5 of the namesAlbum, artist pageslook up ids in the MapsSearchViewmatches title, artist, albumPlayerStoregetCollectionTracks
Playing a track
src, play()endedplayTrack(next)Controls, Queueand track listsMedia SessionOS media keysPlayerStoreplayTrack, togglePlayPause<audio> elementload(), then play()Audio eventsset currentTime, isPlayinggetNextTrackIndexshuffle, repeat, or i + 1MobX reactions mirror the current track, play state and position to navigator.mediaSession.