Skip to main content
Crashbytes logoCrashbytes
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Browse Articles
HomeArticlesByte Sized ExamplesOpen SourceServicesAboutContact
Network
Theme
Browse Articles
Crashbytes logoCrashbytes

Expert insights on web development, technology trends, and programming best practices. Learn from real-world experiences and cutting-edge techniques that help you build better software.

Follow Us

Our Sites

  • ๐Ÿ”ฎ Predictions
  • ๐Ÿ“ฐ Breaking News
  • ๐ŸŽจ AI Art
  • ๐Ÿ“– Short Stories
  • View All โ†’
  • Products โ†’

Sitemap

  • Home
  • All Articles
  • Open Source
  • Services
  • About Us
  • Contact
  • Donate Compute

Popular Topics

  • Serverless
  • Cloud Architecture
  • DevOps
  • Kubernetes
  • Platform Engineering

Resources

  • Privacy Policy
  • Terms of Service
  • Sitemap
  • RSS Feed
  • PGP Key

Stay Updated

Get the latest articles, tutorials, and insights delivered to your inbox. Join our community of developers and never miss an update.

ยฉ 2021-2026 Crashbytesยฎ by Blackhole Software, LLC. All rights reserved.
| Reg. U.S. Pat. & Tm. Off.

Made for the developer community

  1. Home
  2. /
  3. Articles
  4. /
  5. Parse Streaming JSON From an LLM: A Tolerant Partial-JSON Parser in TypeScript
TutorialJuly 20, 202621 min readโ€ข By Michael Eakins

Parse Streaming JSON From an LLM: A Tolerant Partial-JSON Parser in TypeScript

A hands-on TypeScript tutorial that builds a tolerant partial-JSON parser so an LLM's streamed structured output renders field by field instead of stalling on the closing brace. Tokenizer, auto-closing parser, and 49 offline tests.

Quick Takeaways

What you'll learn in this article

21 min read
Intermediate
  • 1

    A hands-on TypeScript tutorial that builds a tolerant partial-JSON parser so an LLM's streamed structured output renders field by field instead of stalling on the closing brace

  • 2

    Tokenizer, auto-closing parser, and 49 offline tests

Keep reading for detailed implementation, code examples, and real-world results

Structured output is how most LLM features actually ship. You do not ask the model for prose and hope; you ask it for a JSON object with a fixed shape โ€” a triage result, an extraction, a set of tool arguments โ€” and you render the fields. The moment you also want that output to stream, so the interface feels alive instead of frozen behind a spinner, you hit a wall that has nothing to do with the model and everything to do with the language runtime: JSON.parse throws on anything that is not a complete document.

Watch what that means in practice. The model streams the object one token at a time. After the first few tokens the buffer holds {"category":"bil. That is a perfectly reasonable prefix of the final answer, and a human can already see where it is going. JSON.parse cannot. It throws. It throws on the next chunk too, and the one after that. For a small support-ticket object serialized to 210 characters, JSON.parse throws on 209 of the 210 prefixes โ€” every single one except the last. Your progressive UI has exactly one moment when it can render anything, and it is the moment the stream ends, which is the moment you no longer needed streaming at all.

This tutorial builds the missing piece: a tolerant partial-JSON parser that takes any prefix of a JSON document and returns the best-effort value parsed so far, closing whatever structure is still open. Feed it {"category":"bil and it returns { category: "bil" }. Feed it the buffer one chunk longer and it returns one field more complete. It never throws, it has zero runtime dependencies, and it is covered by 49 offline tests you can run without an API key. The complete, runnable project lives in the CrashBytes ByteSizedExamples repository.

Why this problem is worth a library

The naive fixes all look tempting and all fail in ways that surface in production, not in the demo.

The first instinct is to wait. Buffer the whole stream, parse once at the end, render. This works and is what most teams ship first, but it throws away the entire point of streaming: the user stares at a spinner for the full generation time even though the first field was decided in the first few hundred milliseconds. On a long object โ€” an extraction with a paragraph-length summary field in the middle โ€” the fields after the long one are invisible until the long one finishes, which is the worst possible ordering.

The second instinct is to count braces. Track how many { and [ you have seen, append the matching number of } and ], and parse that. This handles the shallow happy path and breaks the instant a brace appears inside a string. The buffer {"note":"a } b is not an object with an extra close brace; the } is a literal character inside a string value, and a brace-counter will either close the object early or miscount the depth. Strings are where naive JSON handling goes to die, because every structural character โ€” {, }, [, ], ,, :, " โ€” is also a legal character inside a string.

The third instinct is to reach for a regex. There is no regular expression that parses JSON; JSON is a recursive grammar and regex is not, and the moment you have nested objects the approach collapses. People try anyway. It is always a mistake.

The honest solution is a small, purpose-built parser that understands JSON's grammar well enough to know where it is when the buffer runs out โ€” inside a string, between a key and its colon, halfway through a number โ€” and repairs the buffer accordingly. That is what we build, in three parts: a tokenizer that tracks string and escape state precisely, a tolerant parser that auto-closes open containers, and a thin accumulator that re-parses the growing buffer on every streamed chunk.

Top-level fields readable vs percent of the stream received (7-field triage object)

Top-level fields readable vs percent of the stream received (7-field triage object)
pctPartial parserJSON.parse
1010
2020
3030
4030
5030
6040
7040
8040
9050
10077

Those are not invented numbers โ€” they are measured against the exact demo object in the companion project. The tolerant parser exposes the first field about a tenth of the way through the stream and has three of the seven fields readable by the one-third mark. JSON.parse sits flat on zero until the final byte and then jumps to all seven at once. The flat stretch in the partial curve between 30 and 60 percent is real and instructive: the summary field in the middle is a long string, and while it streams, no new top-level field can resolve โ€” but, crucially, the three fields you already had stay stable and rendered the whole time.

What you will build

The library is three modules' worth of responsibility, and you implement them in order:

  1. A tokenizer that scans the raw buffer left to right into JSON tokens, marking only the final token as incomplete when the buffer ends mid-token.
  2. A tolerant parser that walks those tokens with an explicit stack of open objects and arrays, and auto-closes everything still open at the end.
  3. A StreamingJsonAccumulator that holds the growing buffer and re-parses it on every push, so a UI can bind directly to the latest best-effort value.

Everything is synchronous and pure. There is no network, no async, no dependency on any particular model SDK. You drive it with strings, which means you can test every edge case deterministically and offline โ€” and the 49 tests in the repo do exactly that.

We build in TypeScript with strict compiler settings, ESM modules, and Vitest for the test suite, mirroring the toolchain used across the other TypeScript LLM tutorials in the CrashBytes companion repo โ€” for example the type-safe LLM tool-calling library built on Zod, which produces the very kind of structured output this parser consumes as it streams.

Advertisement

The shape of the data

Before any parsing, define what a token is. A JSON token is one of the structural punctuation marks, a string, a number, one of the three literals, or โ€” the part that matters for streaming โ€” an incomplete tail.

type Token =
  | { type: '{' | '}' | '[' | ']' | ':' | ','; complete: true }
  | { type: 'string'; value: string; complete: boolean }
  | { type: 'number'; value: number; complete: boolean }
  | { type: 'true' | 'false' | 'null'; value: boolean | null; complete: true }
  | { type: 'literal'; value: boolean | null; complete: false }
  | { type: 'garbage'; complete: false }

The complete flag is the whole design in one field. Because the tokenizer scans strictly left to right and stops at the end of the buffer, only the last token can ever be incomplete: everything before it was, by definition, followed by more characters, so it terminated. That single invariant โ€” only the tail can be incomplete โ€” collapses an intimidating problem into a tractable one. The parser handles the body of the token stream as ordinary complete JSON and applies its special tolerance to exactly one token: the tail.

A literal token is a partial true/false/null (say the buffer ended at tr), and garbage is a run of letters that is not a prefix of any literal. Both are, by construction, never complete โ€” they only ever appear as that final tail token.

Part 1: a tokenizer that respects strings

The tokenizer is where correctness is won or lost, because strings are where the structural characters hide. Here is the scanner for a single string literal, which is the most important function in the library:

const HEX4 = /^[0-9a-fA-F]{4}$/

function scanString(
  s: string,
  start: number
): { raw: string; end: number; complete: boolean } {
  let i = start + 1
  let contentEnd = i // exclusive end of the safely-scanned content

  while (i < s.length) {
    const c = s[i]

    if (c === '\\') {
      const next = s[i + 1]
      if (next === undefined) break // lone trailing backslash: drop it
      if (next === 'u') {
        const hex = s.slice(i + 2, i + 6)
        if (!HEX4.test(hex)) break // incomplete \uXXXX at buffer end: drop it
        i += 6
        contentEnd = i
        continue
      }
      i += 2 // simple escape: \" is an escaped quote, NOT a terminator
      contentEnd = i
      continue
    }

    if (c === '"') {
      return { raw: s.slice(start, i + 1), end: i + 1, complete: true }
    }

    i += 1
    contentEnd = i
  }

  // Buffer ended before the closing quote: close it at the last safe point.
  return {
    raw: s.slice(start, contentEnd) + '"',
    end: s.length,
    complete: false,
  }
}

Three things in this function are doing real work. First, the backslash branch: when it sees \, it consumes two characters, so an escaped quote \" can never be mistaken for the string terminator. This is the single most common bug in hand-rolled JSON handling, and it is fixed here by construction. Second, the \u sub-branch checks that four hex digits actually follow; if the buffer ended in the middle of a \u00 escape, it stops before the broken escape and closes the string at the last valid character, so the repaired literal is always parseable. Third, contentEnd tracks the last position that formed valid string content, so when the buffer runs out mid-string the function returns s.slice(start, contentEnd) + '"' โ€” the good part, re-closed. The returned raw is always a syntactically valid JSON string, which means the tokenizer can lean on the platform's own JSON.parse to decode escapes rather than re-implementing Unicode handling:

if (c === '"') {
  const r = scanString(s, i)
  // r.raw is always a valid JSON string literal, so this never throws.
  tokens.push({
    type: 'string',
    value: JSON.parse(r.raw) as string,
    complete: r.complete,
  })
  i = r.end
  continue
}

Reusing JSON.parse on a guaranteed-valid fragment is a deliberate choice. The library is tolerant at the grammar level, where the streaming problem lives, and strict at the lexical level, where the platform already has a correct, fast, battle-tested implementation. You do not want to be the person who re-implemented surrogate-pair decoding and got it subtly wrong.

Part 2: numbers and literals โ€” the ambiguity rule

Numbers force a decision that goes to the heart of what "best effort" means. A number is complete only if it matches the JSON number grammar:

const VALID_NUMBER = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/
const NUMBER_CHAR = /[-+0-9.eE]/

function scanNumber(s: string, start: number) {
  let i = start
  while (i < s.length && NUMBER_CHAR.test(s[i]!)) i += 1
  const raw = s.slice(start, i)
  return { raw, end: i, complete: VALID_NUMBER.test(raw) }
}

If the buffer ends at 1., or 1e, or a lone -, that number is incomplete and โ€” this is the design decision โ€” it is dropped along with its key. The reason is that a partial number is genuinely ambiguous in a way a partial string is not. The buffer {"amount":1. could be resolving to 1.0 or 1.5 or 1.999; there is no honest "so far" value. Showing 1 would be inventing data. So the parser declines to guess, drops the field, and lets it appear a chunk later when the number is unambiguous. A partial string, by contrast, has a truthful prefix โ€” "hel" really is the first three characters of whatever is coming โ€” so it is kept.

Literals get the opposite treatment, and for a precise reason:

const LITERALS = ['true', 'false', 'null'] as const

function scanLiteral(s: string, start: number) {
  let i = start
  while (i < s.length && /[a-z]/.test(s[i]!)) i += 1
  const raw = s.slice(start, i)

  if (raw === 'true' || raw === 'false' || raw === 'null') {
    return {
      token: { type: raw, value: literalValue(raw), complete: true },
      end: i,
    }
  }
  const match = LITERALS.find(l => raw.length > 0 && l.startsWith(raw))
  if (match) {
    // Unambiguous prefix -> resolve it. t->true, f->false, n->null.
    return {
      token: { type: 'literal', value: literalValue(match), complete: false },
      end: i,
    }
  }
  return { token: { type: 'garbage', complete: false }, end: i }
}

The three JSON literals start with three different letters โ€” t, f, n โ€” so any non-empty prefix identifies exactly one of them. A buffer ending in tr can only be heading toward true; there is no other legal continuation. That makes the resolution safe, so {"done":tr yields { done: true }. This is the mirror image of the number rule: literals are resolved because their prefix is unambiguous, numbers are dropped because theirs is not. Getting these two opposite-looking rules from the same underlying principle โ€” keep what is truthful, drop what is a guess โ€” is the tutorial's central lesson.

JSON.parse on all 210 prefixes of a 210-character object: 209 throw, 1 parses

JSON.parse on all 210 prefixes of a 210-character object: 209 throw, 1 parses
outcomecount
Prefixes JSON.parse rejects209
Prefixes JSON.parse accepts1
Advertisement

Part 3: the tolerant, auto-closing parser

With tokens in hand, the parser walks them using an explicit stack of frames. An object frame remembers the object being built, the current key, and what it expects to see next; an array frame remembers the array and its state.

type ObjectFrame = {
  type: 'object'
  obj: Record<string, unknown>
  currentKey: string | undefined
  state: 'key' | 'colon' | 'value' | 'comma'
}
type ArrayFrame = { type: 'array'; arr: unknown[]; state: 'value' | 'comma' }
type Frame = ObjectFrame | ArrayFrame

Tracking the object's state โ€” is it expecting a key, a colon, a value, or a comma next? โ€” is what lets the parser make the right call about the tail token. The same incomplete string means different things in different positions: {"na is a partial key and gets dropped, while {"name":"Ad is a partial value and gets kept. The state machine is how the parser knows which it is looking at.

The main loop processes every token except the incomplete tail as ordinary, complete JSON, pushing frames on {/[, popping and attaching them on }/], and routing strings to either the key slot or a value depending on the frame's state:

case 'string': {
  const top = stack[stack.length - 1]
  if (top && top.type === 'object' && top.state === 'key') {
    top.currentKey = tok.value
    top.state = 'colon'
  } else {
    attachValue(tok.value)
  }
  break
}

After the main loop, the parser handles the one incomplete tail token according to the frame it lands in โ€” keeping partial values, dropping partial keys โ€” and then does the thing that makes the whole approach work: it auto-closes every container still on the stack, innermost first.

// Incorporate the trailing incomplete token according to grammar position.
if (tail) {
  const top = stack[stack.length - 1]
  if (
    top?.type === 'object' &&
    top.state === 'value' &&
    (tail.type === 'string' || tail.type === 'literal')
  ) {
    top.obj[top.currentKey as string] = tail.value
    top.state = 'comma'
    top.currentKey = undefined
  } else if (
    top?.type === 'array' &&
    top.state === 'value' &&
    (tail.type === 'string' || tail.type === 'literal')
  ) {
    top.arr.push(tail.value)
  }
  // partial key / number / garbage -> dropped by omission
}

// Auto-close every still-open container from the innermost outward.
while (stack.length > 0) {
  const frame = stack.pop()!
  attachClosed(frame.type === 'array' ? frame.arr : frame.obj)
}

return hasRoot ? root : undefined

Because the frames hold real, progressively-filled objects and arrays, closing them is just a matter of attaching each finished container to its parent. A deeply nested partial like {"user":{"profile":{"name":"Ada closes from the inside out โ€” the innermost object, then the middle, then the root โ€” and produces { user: { profile: { name: "Ada" } } }. Arbitrary nesting comes for free because the stack is doing the bookkeeping.

The public entry point wraps this in a belt-and-suspenders guard so the never-throw guarantee holds even against inputs the author did not foresee:

export function parsePartialJson(buffer: string): unknown {
  try {
    return parseInternal(buffer)
  } catch {
    return undefined
  }
}

A parser you call on every streamed chunk must never throw, because a throw in that path is a crash in the render loop. The internal parser is written so it cannot throw on any prefix of valid JSON; the outer try/catch is there so that even a genuinely malformed model output degrades to undefined instead of taking down the component. Resilience at the boundary is a theme across these tutorials โ€” the same instinct drives the retry and failover logic in the resilient multi-provider LLM client.

Part 4: the streaming accumulator

The parser is pure and stateless. Streaming needs a thin stateful wrapper that holds the growing buffer and re-parses on each chunk:

export class StreamingJsonAccumulator {
  #buffer = ''
  #value: unknown = undefined

  push(chunk: string): unknown {
    this.#buffer += chunk
    this.#value = parsePartialJson(this.#buffer)
    return this.#value
  }

  get buffer(): string {
    return this.#buffer
  }
  get value(): unknown {
    return this.#value
  }
  reset(): void {
    this.#buffer = ''
    this.#value = undefined
  }
}

That is the entire streaming interface. Every time a token arrives from the model, you push it and get back the best-effort object so far. Re-parsing the full buffer on every chunk is O(n) per chunk and O(nยฒ) over a whole stream, which sounds alarming until you remember the actual sizes: structured LLM outputs are kilobytes, not megabytes, and re-parsing a few kilobytes on each of a few hundred chunks is microseconds of work next to the tens of milliseconds between tokens. Simplicity wins here; if you ever genuinely stream a megabyte object, an incremental parser is a worthwhile complication, but you will almost certainly never need it.

Wiring it to a real model stream is then trivial. Whatever your SDK's streaming primitive is โ€” an async iterator of text deltas is the common shape โ€” you feed each delta straight in:

const acc = new StreamingJsonAccumulator()

for await (const delta of model.streamStructuredOutput(prompt)) {
  const partial = acc.push(delta.text)
  render(partial) // bind your UI to the latest best-effort object
}

render gets called with an object that grows a field at a time. Your framework diffs it and updates only what changed, so the category badge appears, then the priority, then the summary types itself in โ€” the interface behaves the way users already expect streaming to behave, because they have watched chat responses stream for years. The only reason structured output usually doesn't stream is that JSON.parse made it hard, and now it is not.

Part 5: proving it with tests

The test suite is where a parser like this earns trust, because the failure modes are all edge cases and edge cases are invisible until someone hits them. The companion project ships 49 Vitest cases; the most important one is not any single example but a property: for every prefix of a valid document, the parser does not throw.

import { describe, it, expect } from 'vitest'
import { parsePartialJson } from './partial-json'

describe('never throws on any prefix', () => {
  const docs = [
    '{"category":"billing","tags":["a","b"],"confidence":0.93,"escalate":true}',
    '[1,-2,3.5,true,false,null,"x",{"nested":[{"deep":true}]}]',
    '{"msg":"a \\"quoted\\" word and a \\n newline","n":-12.5e3}',
  ]
  for (const doc of docs) {
    for (let i = 1; i <= doc.length; i++) {
      const prefix = doc.slice(0, i)
      it(`prefix length ${i} of ${doc.length}`, () => {
        expect(() => parsePartialJson(prefix)).not.toThrow()
      })
    }
  }
})

This single generator produces hundreds of assertions, one per prefix length across several documents chosen to exercise escapes, negatives, exponents, and nesting. If any prefix ever throws โ€” a broken escape, a miscounted depth, a number scanned wrong โ€” one of these fails and names the exact prefix length, so you can slice the document there and see precisely what tripped it. It is the cheapest, highest-value test in the suite, and it is the one that lets you refactor the parser fearlessly.

The targeted cases then pin down the deliberate behaviors so a future edit cannot quietly change them:

it('closes and keeps a partial string value', () => {
  expect(parsePartialJson('{"msg":"hel')).toEqual({ msg: 'hel' })
})

it('drops a partial key', () => {
  expect(parsePartialJson('{"name":"Ada","ag')).toEqual({ name: 'Ada' })
})

it('resolves an unambiguous partial literal', () => {
  expect(parsePartialJson('{"done":tr')).toEqual({ done: true })
})

it('drops an ambiguous partial number', () => {
  expect(parsePartialJson('{"n":1.')).toEqual({})
})

it('never mistakes an escaped quote for a terminator', () => {
  expect(parsePartialJson('{"note":"a \\" b')).toEqual({ note: 'a " b' })
})

it('closes deep nesting from the inside out', () => {
  expect(parsePartialJson('{"u":{"p":{"name":"Ada')).toEqual({
    u: { p: { name: 'Ada' } },
  })
})

Each of these is a specification as much as a test. The behavior table earlier in this article is not documentation that can drift from the code; it is exactly what these assertions enforce, and if someone "fixes" the partial-number rule to show 1 instead of dropping the field, the fourth test goes red and the pull request has to justify the change on purpose.

The design decisions, stated plainly

A tolerant parser makes choices, and hiding them is how you get a library people cannot reason about. Here are the ones this library makes, and why.

Empty or whitespace-only input returns undefined, never throws. There is no value in an empty buffer, and the streaming caller wants a benign "nothing yet," not an exception on the first chunk.

Partial string values are kept; partial keys are dropped. A value prefix is truthful โ€” the characters you have really are the start of the value. A key prefix is not usable: you cannot attach a value to half a key without inventing the other half, and a key that changes identity as more characters arrive would make the object's shape flicker. So a value streams in character by character and a key only appears once it is whole.

Unambiguous partial literals resolve; ambiguous partial numbers are dropped. Same principle from two directions: t/f/n each identify one literal, so resolving them is truthful, while 1. could become any of infinitely many numbers, so showing anything is a guess. Keep the truth, drop the guess.

Re-parse the whole buffer per chunk. O(nยฒ) over a stream sounds worse than it is at kilobyte scale, and the simplicity buys you a stateless, trivially-testable core. Optimize only if a profiler, not an instinct, tells you to.

Reuse JSON.parse for lexical decoding. The library is tolerant about grammar and strict about lexing, delegating escape and Unicode handling to the platform on fragments it has already proven valid.

These are the same disciplines that show up whenever you build the plumbing under an agent rather than the agent itself โ€” the unglamorous layer decides whether the glamorous layer feels good. It is the same reason a durable agent memory layer is worth building carefully: the interesting behavior on top is only as reliable as the boring machinery underneath.

Where this fits

Streaming structured output is quietly becoming the default way LLM features present themselves, because the model economics finally allow it โ€” tokens are cheap and fast enough that the interface, not the inference, is the bottleneck users feel. A chat bubble that streams text is table stakes; a form that fills itself in, a table whose rows appear as they are extracted, a plan whose steps materialize one at a time โ€” that is the current frontier of LLM UX, and every one of those interfaces needs to parse a JSON object that is not finished yet.

The parser in this tutorial is small on purpose. It is a few hundred lines, it has no dependencies, and it does exactly one thing: turn any prefix of a JSON document into the best value available so far, without ever throwing. That is the kind of component you write once, cover with tests, and never think about again โ€” while it quietly makes every streaming feature you build on top of it feel instantaneous.

Clone the complete project from the CrashBytes ByteSizedExamples repository, run npm test to watch all 49 cases pass without a key, and then run npm run demo to see a support-ticket object stream in field by field. Point it at your own model's structured output next, and give your interface back the streaming it was always supposed to have.

Signed by Michael Eakins

PGP key fingerprint ends in 08E8 8F19 ยท signed 2026-07-20

Verify โ†’.sig
Advertisement

Was this article helpful?

Your feedback helps us improve our content and create more valuable resources

We appreciate honest feedback - it helps us serve you better

Work with us

This analysis is what we do for clients

CrashBytes consults on enterprise AI strategy and implementation, builds custom web and mobile software, and places senior engineers on corp-to-corp engagements.

See Services

Enjoyed this? Get the next one.

Join developers getting CrashBytes articles, tutorials, and predictions in their inbox. No spam, unsubscribe anytime.

Related Topics

TypeScriptLLMstreamingstructured outputJSONtutorial
Back to Articles
โ† PreviousThe Durability Discount: Nvidia Lost First Place While Data Center Grew 92%Next โ†’The Wake Word Is the Moat: The EU Pried Open Android's Assistant Slot

From across the CrashBytes network

More than the blog โ€” predictions, news, fiction, and AI art.

PredictionCustom AI Chips Reach Commodity Status by Q4 2027: Cloud Provider Competition Drives Democratization
NewsWeek In Review July 19-25, 2026 - The Week The Money Moved To The Metering Layer
Short StoryThe Answer Key
AI ArtThe Room That Remembers

Continue Your Learning Journey

Explore more articles related to Tutorial and expand your knowledge.

๐Ÿ“„Tutorial

Cut LLM Token Costs with Anthropic Prompt Caching in TypeScript

A hands-on TypeScript tutorial on Anthropic prompt caching. Build a cache-aware request planner, audit for silent invalidators, and measure real savings from the usage fields, offline and fully tested.

27 min readRead more
๐Ÿ“„Engineering

Build a Type-Safe LLM Tool-Calling Layer in TypeScript: Zod Validation and Auto-Repair

A hands-on TypeScript tutorial: validate LLM tool-call arguments with Zod, auto-repair malformed calls in a bounded loop, and dispatch fully-typed args to your handlers โ€” provider-agnostic, tested, and offline.

41 min readRead more
๐Ÿ“„Engineering

Build a Resilient Multi-Provider LLM Client in TypeScript: Timeouts, Retries, Circuit Breakers, and Failover

A hands-on TypeScript tutorial for building a production LLM client that survives slow upstreams, rate limits, transient 5xx errors, and whole-provider outages โ€” a per-attempt timeout, exponential backoff with jitter, a per-provider circuit breaker, and automatic failover, in about 250 lines of typed, fully tested code.

22 min readRead more
๐Ÿ“„Engineering

Build a Parallel Subagent Orchestrator in TypeScript: Fan-Out, Retries, Pipelines

A hands-on TypeScript tutorial for building a parallel subagent orchestrator: bounded-concurrency fan-out, automatic retries, schema-validated structured output, and barrier-free pipelines โ€” provider-agnostic, with tests.

27 min readRead more