Skip to content

Instantly share code, notes, and snippets.

@tkersey
Last active October 13, 2025 16:49
Show Gist options
  • Select an option

  • Save tkersey/69c36bcf40e23f7ef5c82a9c63983a13 to your computer and use it in GitHub Desktop.

Select an option

Save tkersey/69c36bcf40e23f7ef5c82a9c63983a13 to your computer and use it in GitHub Desktop.

autoscale: true

[fit] Algebraic Structures
in TypeScript

Beyond Monoids


[fit] What We Know

Monoid = combine + empty

interface Monoid<A> {
  readonly empty: A;
  readonly concat: (x: A, y: A) => A;
}

[fit] The Journey Ahead

Core Abstractions

  1. Semigroup - Associative operation
  2. Monoid - Semigroup + identity
  3. Functor - Transform values in a context
  4. Applicative - Apply functions in a context
  5. Alternative - Choice and failure
  6. Monad - Chain computations in a context

Advanced Structures

  1. Foldable - Collapse structures
  2. Traversable - Turn structures inside-out
  3. Group - Reversible operations
  4. Ring - Addition and multiplication with inverses
  5. Semiring - Two operations that distribute
  6. Semi-Lattice - Partial order with one operation
  7. Lattice - Complete order with bounds

[fit] Part 0: Semigroup

The Foundation


[fit] What's a Semigroup?

Associative binary operation (no identity needed)

interface Semigroup<A> {
  concat: (x: A, y: A) => A;
}

// Law: Associativity
// concat(concat(a, b), c) ≡ concat(a, concat(b, c))

[fit] Real-World:
Non-Empty Lists

type NonEmptyArray<A> = readonly [A, ...A[]];

const nonEmptyArraySemigroup = <A>(): Semigroup<NonEmptyArray<A>> => ({
  concat: (x, y) => [x[0], ...x.slice(1), ...y] as NonEmptyArray<A>
});

// Always safe - no empty result possible!
const combined = nonEmptyArraySemigroup<string>().concat(
  ["error1"],
  ["error2", "error3"]
); // ["error1", "error2", "error3"]

[fit] Real-World:
Max/Min Values

const maxSemigroup: Semigroup<number> = {
  concat: (x, y) => Math.max(x, y)
};

const minSemigroup: Semigroup<number> = {
  concat: (x, y) => Math.min(x, y)
};

// Finding bounds without initial value
const prices = [10.99, 25.50, 7.99, 15.00] as NonEmptyArray<number>;
const maxPrice = prices.reduce(maxSemigroup.concat); // 25.50
const minPrice = prices.reduce(minSemigroup.concat); // 7.99

[fit] Real-World:
First/Last

const firstSemigroup = <A>(): Semigroup<A> => ({
  concat: (x, _) => x  // Always keep first
});

const lastSemigroup = <A>(): Semigroup<A> => ({
  concat: (_, y) => y  // Always keep last
});

// Conflict resolution strategies
const configs = [
  { theme: "dark", fontSize: 12 },
  { theme: "light", fontSize: 14 },
  { theme: "auto", fontSize: 16 }
] as NonEmptyArray<Config>;

const firstWins = configs.reduce(firstSemigroup<Config>().concat);
// { theme: "dark", fontSize: 12 }

const lastWins = configs.reduce(lastSemigroup<Config>().concat);
// { theme: "auto", fontSize: 16 }

[fit] Part 1: Functor

The Gateway Abstraction


[fit] What's a Functor?

A structure you can map over

interface Functor<F> {
  map: <A, B>(fa: F<A>, f: (a: A) => B) => F<B>;
}

[fit] Functor Laws

// Identity
map(fa, x => x)  fa

// Composition
map(map(fa, f), g)  map(fa, x => g(f(x)))

[fit] Array is a Functor

const arrayFunctor: Functor<Array> = {
  map: <A, B>(fa: A[], f: (a: A) => B): B[] => 
    fa.map(f)
};

// Usage
const numbers = [1, 2, 3];
const doubled = arrayFunctor.map(numbers, x => x * 2);
// [2, 4, 6]

[fit] Option is a Functor

type Option<A> = 
  | { type: "none" }
  | { type: "some"; value: A };

const optionFunctor: Functor<Option> = {
  map: <A, B>(fa: Option<A>, f: (a: A) => B): Option<B> => {
    switch (fa.type) {
      case "none": return { type: "none" };
      case "some": return { type: "some", value: f(fa.value) };
    }
  }
};

// Transform without unwrapping!
const maybeNumber: Option<number> = { type: "some", value: 42 };
const maybeString = optionFunctor.map(maybeNumber, n => `The answer is ${n}`);

[fit] Promise is a Functor

const promiseFunctor: Functor<Promise> = {
  map: <A, B>(fa: Promise<A>, f: (a: A) => B): Promise<B> => 
    fa.then(f)
};

// Chain transformations
const userPromise = fetchUser(123);
const userName = promiseFunctor.map(userPromise, user => user.name);
const upperName = promiseFunctor.map(userName, name => name.toUpperCase());

[fit] Result is a Functor

type Result<E, A> = 
  | { type: "error"; error: E }
  | { type: "ok"; value: A };

const resultFunctor = {
  map: <E, A, B>(
    fa: Result<E, A>, 
    f: (a: A) => B
  ): Result<E, B> => {
    switch (fa.type) {
      case "error": return fa;
      case "ok": return { type: "ok", value: f(fa.value) };
    }
  }
};

// Transform success, propagate errors
const parseResult = parseJSON(input);
const validated = resultFunctor.map(parseResult, validate);
const transformed = resultFunctor.map(validated, transform);

[fit] Real-World:
Form Validation

type ValidationResult<A> = Result<string[], A>;

// Parse and transform in a pipeline
const validateAge = (input: string): ValidationResult<number> => {
  const parsed = parseInt(input);
  if (isNaN(parsed)) return { type: "error", error: ["Invalid number"] };
  if (parsed < 0) return { type: "error", error: ["Age must be positive"] };
  return { type: "ok", value: parsed };
};

const validateForm = (data: FormData): ValidationResult<User> => {
  const age = validateAge(data.age);
  const ageInMonths = resultFunctor.map(age, a => a * 12);
  const category = resultFunctor.map(age, a => 
    a < 18 ? "minor" : "adult"
  );
  // ...
};

[fit] Part 2: Applicative

Functions in a Context


[fit] The Problem

// We have:
const maybeAdd: Option<(a: number) => (b: number) => number>;
const maybeX: Option<number>;
const maybeY: Option<number>;

// We want:
const maybeSum: Option<number>;

// But map doesn't help here!

[fit] Enter Applicative

interface Applicative<F> extends Functor<F> {
  of: <A>(a: A) => F<A>;
  ap: <A, B>(fab: F<(a: A) => B>, fa: F<A>) => F<B>;
}

[fit] Option Applicative

const optionApplicative: Applicative<Option> = {
  ...optionFunctor,
  
  of: <A>(a: A): Option<A> => 
    ({ type: "some", value: a }),
    
  ap: <A, B>(
    fab: Option<(a: A) => B>, 
    fa: Option<A>
  ): Option<B> => {
    if (fab.type === "none") return { type: "none" };
    if (fa.type === "none") return { type: "none" };
    return { type: "some", value: fab.value(fa.value) };
  }
};

// Now we can combine!
const add = (a: number) => (b: number) => a + b;
const maybeAdd = optionApplicative.of(add);
const maybe5 = optionApplicative.of(5);
const maybe3 = optionApplicative.of(3);

const maybeSum = optionApplicative.ap(
  optionApplicative.ap(maybeAdd, maybe5),
  maybe3
); // Some(8)

[fit] Validation Applicative

type Validation<E, A> = 
  | { type: "failure"; errors: E[] }
  | { type: "success"; value: A };

const validationApplicative: Applicative<Validation> = {
  of: <A>(a: A): Validation<never, A> => 
    ({ type: "success", value: a }),
    
  ap: <E, A, B>(
    fab: Validation<E, (a: A) => B>,
    fa: Validation<E, A>
  ): Validation<E, B> => {
    if (fab.type === "failure" && fa.type === "failure") {
      // Accumulate ALL errors!
      return { type: "failure", errors: [...fab.errors, ...fa.errors] };
    }
    if (fab.type === "failure") return fab;
    if (fa.type === "failure") return fa;
    return { type: "success", value: fab.value(fa.value) };
  }
};

[fit] Real-World:
Parallel Validation

// Validate all fields, accumulate all errors
const validateUser = (
  name: string,
  email: string,
  age: string
): Validation<string, User> => {
  const validName = validateName(name);   // Validation<string, string>
  const validEmail = validateEmail(email); // Validation<string, Email>
  const validAge = validateAge(age);       // Validation<string, number>
  
  // Create user if all valid, collect all errors if not
  const createUser = (name: string) => (email: Email) => (age: number) =>
    ({ name, email, age });
    
  return pipe(
    validationApplicative.of(createUser),
    fab => validationApplicative.ap(fab, validName),
    fab => validationApplicative.ap(fab, validEmail),
    fab => validationApplicative.ap(fab, validAge)
  );
};

// If multiple fields invalid, get ALL errors at once!
validateUser("", "not-an-email", "-5");
// Failure(["Name required", "Invalid email", "Age must be positive"])

[fit] Promise Applicative

const promiseApplicative: Applicative<Promise> = {
  ...promiseFunctor,
  
  of: <A>(a: A): Promise<A> => 
    Promise.resolve(a),
    
  ap: async <A, B>(
    fab: Promise<(a: A) => B>,
    fa: Promise<A>
  ): Promise<B> => {
    const [f, a] = await Promise.all([fab, fa]);
    return f(a);
  }
};

// Parallel async operations!
const fetchUserData = async (userId: string) => {
  const combine = (profile: Profile) => (posts: Post[]) => (friends: User[]) =>
    ({ profile, posts, friends });
    
  return pipe(
    promiseApplicative.of(combine),
    fab => promiseApplicative.ap(fab, fetchProfile(userId)),
    fab => promiseApplicative.ap(fab, fetchPosts(userId)),
    fab => promiseApplicative.ap(fab, fetchFriends(userId))
  );
  // All three requests happen in parallel!
};

[fit] Part 3: Monad

The Big One


[fit] The Problem

// We have:
const parseJSON: (s: string) => Option<unknown>;
const validate: (data: unknown) => Option<User>;
const fetchProfile: (user: User) => Promise<Profile>;

// How do we compose these?
// map gives us Option<Option<User>> 😢

[fit] Enter Monad

interface Monad<M> extends Applicative<M> {
  flatMap: <A, B>(ma: M<A>, f: (a: A) => M<B>) => M<B>;
}

// Also called: bind, chain, >>=, then

[fit] Option Monad

const optionMonad: Monad<Option> = {
  ...optionApplicative,
  
  flatMap: <A, B>(
    ma: Option<A>,
    f: (a: A) => Option<B>
  ): Option<B> => {
    switch (ma.type) {
      case "none": return { type: "none" };
      case "some": return f(ma.value);
    }
  }
};

// Now we can chain!
const result = pipe(
  parseJSON(input),
  json => optionMonad.flatMap(json, validate),
  user => optionMonad.flatMap(user, fetchFromCache)
);
// Option<Profile>, not Option<Option<Option<Profile>>>!

[fit] Result Monad

const resultMonad: Monad<Result> = {
  ...resultApplicative,
  
  flatMap: <E, A, B>(
    ma: Result<E, A>,
    f: (a: A) => Result<E, B>
  ): Result<E, B> => {
    switch (ma.type) {
      case "error": return ma;
      case "ok": return f(ma.value);
    }
  }
};

// Chain operations that might fail
const processPayment = (amount: number): Result<string, Payment> =>
  pipe(
    validateAmount(amount),
    amt => resultMonad.flatMap(amt, checkBalance),
    balance => resultMonad.flatMap(balance, deductFunds),
    funds => resultMonad.flatMap(funds, recordTransaction)
  );
// First error short-circuits the chain

[fit] Promise Monad

const promiseMonad: Monad<Promise> = {
  ...promiseApplicative,
  
  flatMap: <A, B>(
    ma: Promise<A>,
    f: (a: A) => Promise<B>
  ): Promise<B> => 
    ma.then(f)
};

// This is just async/await!
const getGrandchildren = (userId: string): Promise<User[]> =>
  pipe(
    fetchUser(userId),
    user => promiseMonad.flatMap(user, u => fetchChildren(u.id)),
    children => promiseMonad.flatMap(
      children, 
      cs => Promise.all(cs.map(c => fetchChildren(c.id)))
    ),
    grandchildren => promiseMonad.map(grandchildren, gcs => gcs.flat())
  );

[fit] Real-World:
API Chain

type ApiResult<A> = Promise<Result<ApiError, A>>;

// Monad for async operations that can fail
const apiMonad = {
  flatMap: <A, B>(
    ma: ApiResult<A>,
    f: (a: A) => ApiResult<B>
  ): ApiResult<B> =>
    ma.then(result => {
      switch (result.type) {
        case "error": return Promise.resolve(result);
        case "ok": return f(result.value);
      }
    })
};

// Complex API workflow
const createOrder = (request: OrderRequest): ApiResult<Order> =>
  pipe(
    validateOrder(request),
    req => apiMonad.flatMap(req, checkInventory),
    items => apiMonad.flatMap(items, reserveStock),
    reserved => apiMonad.flatMap(reserved, chargePayment),
    payment => apiMonad.flatMap(payment, createShipment),
    shipment => apiMonad.map(shipment, s => ({
      ...s,
      status: "confirmed"
    }))
  );

[fit] Do Notation

// Helper for readable monad chains
function* doOption<A>(
  gen: () => Generator<Option<any>, A, any>
): Option<A> {
  const iterator = gen();
  let state = iterator.next();
  
  while (!state.done) {
    const option = state.value;
    if (option.type === "none") return { type: "none" };
    state = iterator.next(option.value);
  }
  
  return { type: "some", value: state.value };
}

// Much cleaner!
const result = doOption(function* () {
  const json = yield parseJSON(input);
  const user = yield validate(json);
  const profile = yield fetchFromCache(user.id);
  return profile;
});

[fit] Bonus: Alternative

Choice and Failure


[fit] What's Alternative?

interface Alternative<F> extends Applicative<F> {
  zero: <A>() => F<A>;  // Identity for alt
  alt: <A>(fx: F<A>, fy: () => F<A>) => F<A>;  // Choice
}

// Laws:
// Associativity: alt(a, () => alt(b, () => c)) ≡ alt(alt(a, () => b), () => c)
// Identity: alt(zero(), () => fa) ≡ fa
// Distributivity: ap(alt(a, () => b), c) ≡ alt(ap(a, c), () => ap(b, c))

[fit] Option Alternative

const optionAlternative: Alternative<Option> = {
  ...optionApplicative,
  
  zero: <A>(): Option<A> => ({ type: "none" }),
  
  alt: <A>(fx: Option<A>, fy: () => Option<A>): Option<A> => 
    fx.type === "some" ? fx : fy()
};

// Try alternatives until one succeeds
const findUser = (id: string): Option<User> =>
  optionAlternative.alt(
    findInCache(id),
    () => optionAlternative.alt(
      findInDatabase(id),
      () => findInArchive(id)
    )
  );

[fit] Array Alternative

const arrayAlternative: Alternative<Array> = {
  ...arrayApplicative,
  
  zero: <A>(): A[] => [],
  
  alt: <A>(fx: A[], fy: () => A[]): A[] => [...fx, ...fy()]
};

// Combine multiple searches
const searchResults = arrayAlternative.alt(
  searchByName(query),
  () => arrayAlternative.alt(
    searchByEmail(query),
    () => searchByPhone(query)
  )
);

[fit] Parser Alternative

const parserAlternative: Alternative<Parser> = {
  ...parserApplicative,
  
  zero: <A>(): Parser<A> => 
    _ => ({ type: "none" }),
    
  alt: <A>(px: Parser<A>, py: () => Parser<A>): Parser<A> =>
    input => {
      const result = px(input);
      return result.type === "some" ? result : py()(input);
    }
};

// Parse number or string
const value = parserAlternative.alt(
  parseNumber,
  () => parseString
);

// Parse different date formats
const date = parserAlternative.alt(
  parseISO8601,
  () => parserAlternative.alt(
    parseUSDate,
    () => parseEUDate
  )
);

[fit] Real-World:
Fallback Chains

// Configuration with fallbacks
const getConfig = <T>(key: string): IO<Option<T>> =>
  ioAlternative.alt(
    readEnvVar(key),
    () => ioAlternative.alt(
      readConfigFile(key),
      () => ioAlternative.alt(
        readDatabase(key),
        () => readDefaults(key)
      )
    )
  );

// API with retry logic
const fetchWithFallback = (url: string): Task<Response> =>
  taskAlternative.alt(
    fetchFromPrimary(url),
    () => taskAlternative.alt(
      delay(1000).chain(() => fetchFromSecondary(url)),
      () => delay(2000).chain(() => fetchFromCache(url))
    )
  );

[fit] Part 4: Foldable

Collapsing Structures


[fit] What's Foldable?

A structure you can reduce to a single value

interface Foldable<F> {
  reduce: <A, B>(
    fa: F<A>,
    b: B,
    f: (b: B, a: A) => B
  ) => B;
}

[fit] Array is Foldable

const arrayFoldable: Foldable<Array> = {
  reduce: <A, B>(
    fa: A[],
    b: B,
    f: (b: B, a: A) => B
  ): B => fa.reduce(f, b)
};

// Sum all numbers
const sum = arrayFoldable.reduce([1, 2, 3, 4], 0, (acc, n) => acc + n);

[fit] Tree is Foldable

type Tree<A> = 
  | { type: "leaf"; value: A }
  | { type: "branch"; left: Tree<A>; right: Tree<A> };

const treeFoldable: Foldable<Tree> = {
  reduce: <A, B>(
    tree: Tree<A>,
    b: B,
    f: (b: B, a: A) => B
  ): B => {
    switch (tree.type) {
      case "leaf": 
        return f(b, tree.value);
      case "branch":
        const leftResult = treeFoldable.reduce(tree.left, b, f);
        return treeFoldable.reduce(tree.right, leftResult, f);
    }
  }
};

// Count all nodes
const nodeCount = treeFoldable.reduce(myTree, 0, (count, _) => count + 1);

[fit] Option is Foldable

const optionFoldable: Foldable<Option> = {
  reduce: <A, B>(
    fa: Option<A>,
    b: B,
    f: (b: B, a: A) => B
  ): B => {
    switch (fa.type) {
      case "none": return b;
      case "some": return f(b, fa.value);
    }
  }
};

// Get value or default
const value = optionFoldable.reduce(
  maybeNumber,
  0,
  (_, n) => n
);

[fit] Derived Operations

// Build useful operations from reduce
const foldableOps = <F>(F: Foldable<F>) => ({
  toArray: <A>(fa: F<A>): A[] =>
    F.reduce(fa, [] as A[], (arr, a) => [...arr, a]),
    
  exists: <A>(fa: F<A>, predicate: (a: A) => boolean): boolean =>
    F.reduce(fa, false, (found, a) => found || predicate(a)),
    
  forAll: <A>(fa: F<A>, predicate: (a: A) => boolean): boolean =>
    F.reduce(fa, true, (all, a) => all && predicate(a)),
    
  find: <A>(fa: F<A>, predicate: (a: A) => boolean): Option<A> =>
    F.reduce(
      fa,
      { type: "none" } as Option<A>,
      (opt, a) => opt.type === "some" ? opt : 
        predicate(a) ? { type: "some", value: a } : opt
    ),
    
  length: <A>(fa: F<A>): number =>
    F.reduce(fa, 0, (count) => count + 1)
});

[fit] Part 5: Traversable

Inside-Out Transformations


[fit] The Problem

// We have:
const promises: Array<Promise<User>>;

// We want:
const promiseOfArray: Promise<Array<User>>;

// Or:
const results: Array<Result<Error, Data>>;
// Want:
const resultOfArray: Result<Error, Array<Data>>;

[fit] Enter Traversable

interface Traversable<T> extends Functor<T>, Foldable<T> {
  traverse: <F, A, B>(
    A: Applicative<F>,
    ta: T<A>,
    f: (a: A) => F<B>
  ) => F<T<B>>;
}

[fit] Array Traversable

const arrayTraversable: Traversable<Array> = {
  ...arrayFunctor,
  ...arrayFoldable,
  
  traverse: <F, A, B>(
    A: Applicative<F>,
    ta: A[],
    f: (a: A) => F<B>
  ): F<B[]> => {
    // Start with empty array in F
    let result: F<B[]> = A.of([]);
    
    // Add each transformed element
    for (const a of ta) {
      const fb = f(a);
      result = A.ap(
        A.map(result, (arr) => (b: B) => [...arr, b]),
        fb
      );
    }
    
    return result;
  }
};

[fit] Real-World:
Batch Validation

// Validate array of inputs, get back validation of array
const validateAll = (
  inputs: string[]
): Validation<string, Email[]> =>
  arrayTraversable.traverse(
    validationApplicative,
    inputs,
    validateEmail
  );

validateAll(["[email protected]", "[email protected]"]);
// Success([Email("[email protected]"), Email("[email protected]")])

validateAll(["[email protected]", "not-an-email", "also-bad"]);
// Failure(["Invalid email: not-an-email", "Invalid email: also-bad"])

[fit] Real-World:
Parallel Fetching

// Fetch all users in parallel
const fetchAllUsers = (
  userIds: string[]
): Promise<User[]> =>
  arrayTraversable.traverse(
    promiseApplicative,
    userIds,
    fetchUser
  );

// All requests happen in parallel!
const users = await fetchAllUsers(["123", "456", "789"]);

[fit] Sequence

// Special case: when f is identity
const sequence = <T, F, A>(
  T: Traversable<T>,
  A: Applicative<F>,
  tfa: T<F<A>>
): F<T<A>> =>
  T.traverse(A, tfa, x => x);

// Turn array of promises into promise of array
const promises = [fetchUser("1"), fetchUser("2"), fetchUser("3")];
const promiseOfUsers = sequence(
  arrayTraversable,
  promiseApplicative,
  promises
);

// Turn array of results into result of array
const results = [parseNumber("1"), parseNumber("2"), parseNumber("3")];
const resultOfNumbers = sequence(
  arrayTraversable,
  resultApplicative,
  results
);

[fit] Part 6: Group

Reversible Operations


[fit] From Monoid to Group

// Monoid: combine + identity
interface Monoid<A> {
  empty: A;
  concat: (x: A, y: A) => A;
}

// Group: monoid + inverse
interface Group<A> extends Monoid<A> {
  inverse: (a: A) => A;
}

[fit] Additive Group

const additiveGroup: Group<number> = {
  empty: 0,
  concat: (x, y) => x + y,
  inverse: x => -x
};

// Now we can subtract!
const subtract = (x: number, y: number) =>
  additiveGroup.concat(x, additiveGroup.inverse(y));

subtract(10, 3); // 7

[fit] Multiplicative Group

// For non-zero numbers
type NonZero = number & { readonly __brand: "NonZero" };

const multiplicativeGroup: Group<NonZero> = {
  empty: 1 as NonZero,
  concat: (x, y) => (x * y) as NonZero,
  inverse: x => (1 / x) as NonZero
};

// Now we can divide!
const divide = (x: NonZero, y: NonZero) =>
  multiplicativeGroup.concat(x, multiplicativeGroup.inverse(y));

[fit] XOR Group

const xorGroup: Group<boolean> = {
  empty: false,
  concat: (x, y) => x !== y,  // XOR
  inverse: x => x             // Self-inverse!
};

// Perfect for toggle operations
const toggle = (state: boolean) =>
  xorGroup.concat(state, true);

// Encryption/decryption with same operation
const encrypt = (bit: boolean, key: boolean) =>
  xorGroup.concat(bit, key);
  
const decrypt = (encrypted: boolean, key: boolean) =>
  xorGroup.concat(encrypted, key); // Same operation!

[fit] Real-World:
Undo/Redo

// Operations that can be undone
type Operation<State> = {
  apply: (state: State) => State;
  undo: (state: State) => State;
};

const operationGroup = <State>(): Group<Operation<State>> => ({
  empty: {
    apply: x => x,
    undo: x => x
  },
  concat: (op1, op2) => ({
    apply: state => op2.apply(op1.apply(state)),
    undo: state => op1.undo(op2.undo(state))
  }),
  inverse: op => ({
    apply: op.undo,
    undo: op.apply
  })
});

// Track document changes
const moveOp: Operation<Doc> = {
  apply: doc => ({ ...doc, cursor: doc.cursor + 1 }),
  undo: doc => ({ ...doc, cursor: doc.cursor - 1 })
};

[fit] Part 7: Semiring

Two Operations


[fit] What's a Semiring?

interface Semiring<A> {
  add: (x: A, y: A) => A;
  zero: A;
  mul: (x: A, y: A) => A;
  one: A;
}

// Laws:
// (a + b) + c = a + (b + c)  -- addition associative
// a + 0 = a                  -- addition identity
// a + b = b + a              -- addition commutative
// (a * b) * c = a * (b * c)  -- multiplication associative
// a * 1 = a                  -- multiplication identity
// a * (b + c) = a*b + a*c    -- distribution

[fit] Number Semiring

const numberSemiring: Semiring<number> = {
  add: (x, y) => x + y,
  zero: 0,
  mul: (x, y) => x * y,
  one: 1
};

// Normal arithmetic

[fit] Boolean Semiring

const booleanSemiring: Semiring<boolean> = {
  add: (x, y) => x || y,  // OR
  zero: false,
  mul: (x, y) => x && y,  // AND
  one: true
};

// Logic operations distribute!
// a && (b || c) = (a && b) || (a && c)

[fit] Tropical Semiring

// Min-plus algebra (for shortest path algorithms)
const tropical: Semiring<number> = {
  add: (x, y) => Math.min(x, y),
  zero: Infinity,
  mul: (x, y) => x + y,
  one: 0
};

// Used in optimization problems
const shortestPath = (distances: number[][]): number => {
  // Floyd-Warshall with tropical semiring
  return distances.reduce((d, _) =>
    d.map((row, i) =>
      row.map((_, j) =>
        d[i].reduce((min, _, k) =>
          tropical.add(min, tropical.mul(d[i][k], d[k][j])),
          tropical.zero
        )
      )
    )
  )[0][n-1];
};

[fit] Real-World:
Regex Matching

// Regex as semiring
type Regex = 
  | { type: "empty" }              // Matches nothing
  | { type: "epsilon" }            // Matches empty string
  | { type: "char"; value: string }
  | { type: "concat"; left: Regex; right: Regex }
  | { type: "union"; left: Regex; right: Regex }
  | { type: "star"; inner: Regex };

const regexSemiring: Semiring<Regex> = {
  zero: { type: "empty" },         // Matches nothing
  add: (a, b) => ({ type: "union", left: a, right: b }), // a|b
  one: { type: "epsilon" },        // Matches empty string
  mul: (a, b) => ({ type: "concat", left: a, right: b }) // ab
};

// Build complex patterns
const digit = { type: "char", value: "[0-9]" };
const digits = { type: "star", inner: digit };      // [0-9]*
const number = regexSemiring.mul(digit, digits);    // [0-9][0-9]*

[fit] Bonus: Ring

Semiring + Subtraction


[fit] What's a Ring?

interface Ring<A> extends Semiring<A> {
  sub: (x: A, y: A) => A;  // Subtraction
}

// Derived from Group + Semiring
// sub(x, y) = add(x, negate(y))

[fit] Integer Ring

const integerRing: Ring<number> = {
  ...numberSemiring,
  sub: (x, y) => x - y
};

// Polynomial evaluation
const evalPolynomial = (coeffs: number[], x: number): number =>
  coeffs.reduce((acc, coeff, i) => 
    integerRing.add(
      acc,
      integerRing.mul(coeff, Math.pow(x, i))
    ),
    integerRing.zero
  );

// 3x² - 2x + 1 at x = 4
evalPolynomial([1, -2, 3], 4); // 41

[fit] Matrix Ring

type Matrix2x2 = [[number, number], [number, number]];

const matrix2x2Ring: Ring<Matrix2x2> = {
  zero: [[0, 0], [0, 0]],
  one: [[1, 0], [0, 1]],
  
  add: ([[a, b], [c, d]], [[e, f], [g, h]]) => 
    [[a + e, b + f], [c + g, d + h]],
    
  sub: ([[a, b], [c, d]], [[e, f], [g, h]]) => 
    [[a - e, b - f], [c - g, d - h]],
    
  mul: ([[a, b], [c, d]], [[e, f], [g, h]]) => [
    [a*e + b*g, a*f + b*h],
    [c*e + d*g, c*f + d*h]
  ]
};

// Linear transformations
const rotate90: Matrix2x2 = [[0, -1], [1, 0]];
const scale2: Matrix2x2 = [[2, 0], [0, 2]];

const combined = matrix2x2Ring.mul(rotate90, scale2);

[fit] Polynomial Ring

// Polynomials as arrays of coefficients
type Polynomial = number[];

const polynomialRing: Ring<Polynomial> = {
  zero: [],
  one: [1],
  
  add: (p1, p2) => {
    const len = Math.max(p1.length, p2.length);
    const result = [];
    for (let i = 0; i < len; i++) {
      result[i] = (p1[i] || 0) + (p2[i] || 0);
    }
    return result;
  },
  
  sub: (p1, p2) => {
    const len = Math.max(p1.length, p2.length);
    const result = [];
    for (let i = 0; i < len; i++) {
      result[i] = (p1[i] || 0) - (p2[i] || 0);
    }
    return result;
  },
  
  mul: (p1, p2) => {
    const result = new Array(p1.length + p2.length - 1).fill(0);
    for (let i = 0; i < p1.length; i++) {
      for (let j = 0; j < p2.length; j++) {
        result[i + j] += p1[i] * p2[j];
      }
    }
    return result;
  }
};

// (x + 1) * (x - 1) = x² - 1
const xPlus1 = [1, 1];    // 1 + x
const xMinus1 = [-1, 1];   // -1 + x
polynomialRing.mul(xPlus1, xMinus1); // [-1, 0, 1] = -1 + 0x + x²

[fit] Part 8: Semi-Lattice

Partial Order with One Operation


[fit] What's a Semi-Lattice?

// Join semi-lattice: only has join (least upper bound)
interface JoinSemiLattice<A> {
  join: (x: A, y: A) => A;  // Least upper bound
}

// Meet semi-lattice: only has meet (greatest lower bound)
interface MeetSemiLattice<A> {
  meet: (x: A, y: A) => A;  // Greatest lower bound
}

// Laws (for join):
// Associative: join(a, join(b, c)) = join(join(a, b), c)
// Commutative: join(a, b) = join(b, a)
// Idempotent: join(a, a) = a

[fit] Real-World:
Version Control

type Version = {
  major: number;
  minor: number;
  patch: number;
};

// Join semi-lattice for versions (max version)
const versionJoinSemiLattice: JoinSemiLattice<Version> = {
  join: (v1, v2) => {
    if (v1.major > v2.major) return v1;
    if (v1.major < v2.major) return v2;
    if (v1.minor > v2.minor) return v1;
    if (v1.minor < v2.minor) return v2;
    if (v1.patch >= v2.patch) return v1;
    return v2;
  }
};

// Find minimum required version
const requiredVersion = dependencies.reduce(
  (min, dep) => versionJoinSemiLattice.join(min, dep.minVersion),
  { major: 0, minor: 0, patch: 0 }
);

[fit] Real-World:
Event Timestamps

// Lamport timestamps for distributed systems
type LamportTimestamp = number;

const timestampSemiLattice: JoinSemiLattice<LamportTimestamp> = {
  join: (t1, t2) => Math.max(t1, t2)
};

// Vector clocks for causality
type VectorClock = Map<NodeId, number>;

const vectorClockSemiLattice: JoinSemiLattice<VectorClock> = {
  join: (vc1, vc2) => {
    const result = new Map(vc1);
    for (const [node, time] of vc2) {
      result.set(node, Math.max(time, result.get(node) || 0));
    }
    return result;
  }
};

// Merge concurrent events
const mergedClock = vectorClockSemiLattice.join(event1.clock, event2.clock);

[fit] Real-World:
CRDT Sets

// Grow-only set (G-Set) - join semi-lattice
type GSet<T> = Set<T>;

const gsetSemiLattice = <T>(): JoinSemiLattice<GSet<T>> => ({
  join: (s1, s2) => new Set([...s1, ...s2])  // Union only
});

// Two-phase set - can add and remove
type TPSet<T> = {
  added: Set<T>;
  removed: Set<T>;
};

const tpsetSemiLattice = <T>(): JoinSemiLattice<TPSet<T>> => ({
  join: (s1, s2) => ({
    added: new Set([...s1.added, ...s2.added]),
    removed: new Set([...s1.removed, ...s2.removed])
  })
});

// Current elements = added - removed
const elements = <T>(tpset: TPSet<T>): Set<T> =>
  new Set([...tpset.added].filter(x => !tpset.removed.has(x)));

[fit] Real-World:
Access Control

// Capabilities that can only increase
type Capabilities = {
  read: boolean;
  write: boolean;
  execute: boolean;
  admin: boolean;
};

const capabilitiesSemiLattice: JoinSemiLattice<Capabilities> = {
  join: (c1, c2) => ({
    read: c1.read || c2.read,
    write: c1.write || c2.write,
    execute: c1.execute || c2.execute,
    admin: c1.admin || c2.admin
  })
};

// Merge permissions from multiple roles
const userCapabilities = userRoles
  .map(role => role.capabilities)
  .reduce(capabilitiesSemiLattice.join, {
    read: false,
    write: false,
    execute: false,
    admin: false
  });

[fit] Real-World:
Knowledge Tracking

// What a node knows about the system
type Knowledge = {
  knownNodes: Set<NodeId>;
  knownFacts: Map<FactId, Fact>;
  timestamp: number;
};

const knowledgeSemiLattice: JoinSemiLattice<Knowledge> = {
  join: (k1, k2) => ({
    knownNodes: new Set([...k1.knownNodes, ...k2.knownNodes]),
    knownFacts: new Map([...k1.knownFacts, ...k2.knownFacts]),
    timestamp: Math.max(k1.timestamp, k2.timestamp)
  })
};

// Gossip protocol - merge knowledge
const updateKnowledge = (
  local: Knowledge,
  remote: Knowledge
): Knowledge =>
  knowledgeSemiLattice.join(local, remote);

[fit] Part 9: Lattice

Complete Order


[fit] Lattice = Both Operations

interface Lattice<A> extends JoinSemiLattice<A>, MeetSemiLattice<A> {
  join: (x: A, y: A) => A;  // Least upper bound
  meet: (x: A, y: A) => A;  // Greatest lower bound
}

// Additional law:
// Absorption: join(a, meet(a, b)) = a
//           meet(a, join(a, b)) = a

[fit] Set Lattice

const setLattice = <T>(): Lattice<Set<T>> => ({
  join: (a, b) => new Set([...a, ...b]),    // Union
  meet: (a, b) => new Set(
    [...a].filter(x => b.has(x))
  )                                          // Intersection
});

// Permissions as sets
const adminPerms = new Set(["read", "write", "delete"]);
const userPerms = new Set(["read", "write"]);

const combined = setLattice.join(adminPerms, userPerms);  // All permissions
const common = setLattice.meet(adminPerms, userPerms);    // Shared permissions

[fit] Security Levels

type SecurityLevel = 
  | "public"
  | "internal" 
  | "confidential"
  | "secret"
  | "top-secret";

const securityLattice: Lattice<SecurityLevel> = {
  join: (a, b) => {
    const levels = ["public", "internal", "confidential", "secret", "top-secret"];
    const aIndex = levels.indexOf(a);
    const bIndex = levels.indexOf(b);
    return levels[Math.max(aIndex, bIndex)];
  },
  meet: (a, b) => {
    const levels = ["public", "internal", "confidential", "secret", "top-secret"];
    const aIndex = levels.indexOf(a);
    const bIndex = levels.indexOf(b);
    return levels[Math.min(aIndex, bIndex)];
  }
};

// Information can only flow up
const canAccess = (userLevel: SecurityLevel, docLevel: SecurityLevel) =>
  securityLattice.join(userLevel, docLevel) === userLevel;

[fit] Bounded Lattice

interface BoundedLattice<A> extends Lattice<A> {
  top: A;     // Maximum element
  bottom: A;  // Minimum element
}

const intervalLattice: BoundedLattice<[number, number]> = {
  join: ([a1, a2], [b1, b2]) => 
    [Math.min(a1, b1), Math.max(a2, b2)],
  meet: ([a1, a2], [b1, b2]) => 
    [Math.max(a1, b1), Math.min(a2, b2)],
  top: [-Infinity, Infinity],
  bottom: [Infinity, -Infinity]
};

// Interval arithmetic for bounds checking
const priceRange: [number, number] = [10, 50];
const discountRange: [number, number] = [0, 0.3];

const possiblePrices = intervalLattice.join(
  priceRange,
  [10 * 0.7, 50]  // With max discount applied
);

[fit] Real-World:
Type Inference

type Type = 
  | { kind: "any" }      // Top type
  | { kind: "unknown" }
  | { kind: "string" }
  | { kind: "number" }
  | { kind: "literal"; value: string | number }
  | { kind: "never" };   // Bottom type

const typeLattice: BoundedLattice<Type> = {
  top: { kind: "any" },
  bottom: { kind: "never" },
  
  join: (a, b) => {
    // Type union - least common supertype
    if (a.kind === "never") return b;
    if (b.kind === "never") return a;
    if (a.kind === "any" || b.kind === "any") return { kind: "any" };
    if (a.kind === b.kind) return a;
    if (a.kind === "literal" && b.kind === "literal") {
      if (typeof a.value === typeof b.value) {
        return { kind: typeof a.value as "string" | "number" };
      }
    }
    return { kind: "unknown" };
  },
  
  meet: (a, b) => {
    // Type intersection
    if (a.kind === "any") return b;
    if (b.kind === "any") return a;
    if (a.kind === "never" || b.kind === "never") return { kind: "never" };
    if (a.kind === b.kind) return a;
    return { kind: "never" };
  }
};

[fit] Putting It All Together


[fit] Parser Combinators

// Parser is a monad!
type Parser<A> = (input: string) => Option<[A, string]>;

const parserMonad: Monad<Parser> = {
  of: <A>(a: A): Parser<A> => 
    input => ({ type: "some", value: [a, input] }),
    
  map: <A, B>(pa: Parser<A>, f: (a: A) => B): Parser<B> =>
    input => {
      const result = pa(input);
      if (result.type === "none") return result;
      const [a, rest] = result.value;
      return { type: "some", value: [f(a), rest] };
    },
    
  flatMap: <A, B>(pa: Parser<A>, f: (a: A) => Parser<B>): Parser<B> =>
    input => {
      const result = pa(input);
      if (result.type === "none") return result;
      const [a, rest] = result.value;
      return f(a)(rest);
    },
    
  ap: <A, B>(pf: Parser<(a: A) => B>, pa: Parser<A>): Parser<B> =>
    parserMonad.flatMap(pf, f =>
      parserMonad.map(pa, a => f(a))
    )
};

[fit] Building Complex Parsers

// Applicative style for parallel parsing
const parseUser = 
  (name: string) => (age: number) => (email: Email) => 
    ({ name, age, email });

const userParser = pipe(
  parserMonad.of(parseUser),
  pf => parserMonad.ap(pf, parseString),
  pf => parserMonad.ap(pf, parseNumber),
  pf => parserMonad.ap(pf, parseEmail)
);

// Alternative for choice
const valueParser = alternative(
  parseNumber,
  parseString,
  parseBoolean
);

// Monadic sequencing
const configParser = doParser(function* () {
  yield string("{");
  const key = yield parseIdentifier;
  yield string(":");
  const value = yield valueParser;
  yield string("}");
  return { [key]: value };
});

[fit] State Management

// State monad for complex state transitions
type State<S, A> = (state: S) => [A, S];

const stateMonad = <S>(): Monad<State<S, any>> => ({
  of: <A>(a: A): State<S, A> => 
    state => [a, state],
    
  map: <A, B>(sa: State<S, A>, f: (a: A) => B): State<S, B> =>
    state => {
      const [a, newState] = sa(state);
      return [f(a), newState];
    },
    
  flatMap: <A, B>(sa: State<S, A>, f: (a: A) => State<S, B>): State<S, B> =>
    state => {
      const [a, newState] = sa(state);
      return f(a)(newState);
    }
});

// Game state management
type GameState = {
  player: Position;
  enemies: Position[];
  score: number;
};

const movePlayer = (direction: Direction): State<GameState, void> =>
  state => [
    undefined,
    { ...state, player: move(state.player, direction) }
  ];

[fit] Effect Systems

// IO monad for controlled side effects
type IO<A> = () => A;

const ioMonad: Monad<IO> = {
  of: <A>(a: A): IO<A> => 
    () => a,
    
  map: <A, B>(ioa: IO<A>, f: (a: A) => B): IO<B> =>
    () => f(ioa()),
    
  flatMap: <A, B>(ioa: IO<A>, f: (a: A) => IO<B>): IO<B> =>
    () => f(ioa())()
};

// Compose effects
const program = doIO(function* () {
  const name = yield readLine("What's your name?");
  yield writeLine(`Hello, ${name}!`);
  const age = yield readLine("What's your age?");
  const nextYear = parseInt(age) + 1;
  yield writeLine(`Next year you'll be ${nextYear}`);
  return { name, age };
});

// Effects only happen when executed
const result = program(); // Now side effects happen

[fit] Type-Level Programming

// Functor at the type level
type Functor<F> = {
  map: <A, B>(f: (a: A) => B) => (fa: HKT<F, A>) => HKT<F, B>;
};

// Higher-kinded types simulation
interface HKT<F, A> {
  _F: F;
  _A: A;
}

// Natural transformations
type NaturalTransformation<F, G> = {
  <A>(fa: HKT<F, A>): HKT<G, A>;
};

// Transform Option to Array
const optionToArray: NaturalTransformation<"Option", "Array"> = 
  <A>(option: Option<A>): A[] =>
    option.type === "some" ? [option.value] : [];

// Transform Array to Option (head)
const arrayToOption: NaturalTransformation<"Array", "Option"> =
  <A>(arr: A[]): Option<A> =>
    arr.length > 0 
      ? { type: "some", value: arr[0] }
      : { type: "none" };

[fit] Key Takeaways

Basic Building Blocks

  • Semigroup - Combine values (no identity needed)
  • Monoid - Semigroup + empty element
  • Group - Monoid + inverse

Functorial Structures

  • Functor - Transform values in context
  • Applicative - Apply functions in context
  • Alternative - Choose between computations
  • Monad - Chain dependent computations

Advanced Patterns

  • Foldable/Traversable - Generic iteration
  • Semiring/Ring - Arithmetic-like operations
  • Semi-Lattice/Lattice - Order and merging

[fit] Why Should I Care?

  • Composability - Small pieces that fit together
  • Reusability - Write generic operations once
  • Correctness - Laws guide implementation
  • Abstraction - Focus on structure, not details

[fit] Start Small

  1. Recognize map patterns → Use Functor
  2. See parallel operations → Try Applicative
  3. Need sequencing → Reach for Monad
  4. Spot reduction → Apply Foldable

The abstractions are already in your code!


[fit] Thank You!

Questions?

Resources:

autoscale: true

[fit] Abstraction


[fit] A total function is a function
that is defined for every element in its domain.
In other words, for every possible input value from the domain,
the function produces exactly one output value in its codomain.


[fit] (Int, Int) → (Int, Int)


[fit] (a, a) → (a, a)


[fit] (a, b) → (b, a)


[fit] Parametricity

[fit] being able to make (non-trivial) statements
about programs by knowing nothing more than their type.


[fit] Parametric
polymorphism

[fit] where a type variable behaves the same
regardless of instantiation of the type variable.


// Define a Transform interface
interface Transform<T> {
  transform(value: T): T;
}

// Implementation for numbers
class NumberTransform implements Transform<number> {
  transform(value: number): number {
    return value + 1;
  }
}

// Implementation for strings
class StringTransform implements Transform<string> {
  transform(value: string): string {
    return value.toUpperCase();
  }
}

[fit] Adhoc
polymorphism

[fit] where a single function name
can refer to different implementations
based on the types of its arguments


[fit] a → a


[fit] String → String


[fit] (a → Bool) → [a] → [a]


[fit] (a → Maybe b) → [a] → [b]


[fit] Algebraic

[fit] Thinking


[fit] Curry-Howard-Lambek
correspondence

Logic Types Categories
propositions types objects
proofs terms arrows
proof of proposition A inhabitation of type A arrow f: t → A
implication function type exponential
conjunction product type product
disjunction sum type coproduct
falsity void type (empty) initial object
truth unit type (singleton) terminal object T

[fit] Numbers

[fit] Multiplication

[fit] Addition

[fit] Exponentiation

[fit] Division

[fit] in Types! 😮


[fit] Numbers


[fit] Zero


[fit] Void


[fit] type Void = never;


[fit] One


[fit] Unit


[fit] type Unit = void;


[fit] Two


[fit] 2 = 1 + 1


[fit] type Two = Void | Unit;


[fit] type Bool = false | true;


[fit] Three


[fit] type RGB = "red" | "green" | "blue";


[fit] Multiplication


[fit] a × b


[fit] Product


[fit] type Pair<First, Second> = [First, Second];


[fit] How many values of type

[fit] Pair<Bool, RGB>

[fit] are there?


[fit] Pair<Bool, RGB>

const all: Pair<Bool, RGB>[] = [
  [true, "red"],
  [true, "green"],
  [true, "blue"],
  [false, "red"],
  [false, "green"],
  [false, "blue"],
];

[fit] ≅


[fit] Isomorphism


[fit] Two types are isomorphic if they
have the same external behavior.


[fit] One type can be implemented in
terms of the other or vice versa.


[fit] Laws


[fit] 0 × X = 0


[fit] Pair<Void,X> ≅ Void


[fit] 1 × X ≅ X


[fit] Pair<Unit,X> ≅ X


[fit] Commutativity


[fit] X × Y ≅ Y × X


[fit] Pair<X,Y> ≅ Pair<Y,X>


[fit] Associativity


[fit] (a × b) × c ≅ a × (b × c)


[fit] Pair<Pair<A, B>, C> ≅ Pair<A, Pair<B, C>>


[fit] Addition


[fit] a + b


[fit] Sum


[fit] Coproduct


[fit] type Left<L> = { kind: "l"; value: L };

[fit] type Right<R> = { kind: "r"; value: R };

[fit] `type Either<L, R> = Left | Right;``


[fit] How many values of type

[fit] Either<Bool, RGB>

[fit] are there?


[fit] Either<Bool, RGB>

const all: Either<Bool, RGB>[] = [
  { k: "l", v: false },
  { k: "l", v: true },
  { k: "r", v: "red" },
  { k: "r", v: "green" },
  { k: "r", v: "blue" },
];

[fit] 1 + a


[fit] type Nothing = { k: "nothing" };

[fit] type Just<T> = { k: "just"; v: T };

[fit] type Maybe<T> = Nothing | Just<T>;


[fit] Laws


[fit] 0 + X ≅ X


[fit] Either<Void,X> ≅ X


[fit] Commutativity


[fit] X + Y ≅ Y + X


[fit] Either<X,Y> ≅ Either<Y,X>


[fit] Associativity


[fit] (a + b) + c ≅ a + (b + c)


[fit] Either<Either<A, B>, C> ≅ Either<A, Either<B, C>>


[fit] coproduct ←→ product


[fit] Duality


[fit] (V → F, V → S) → (V → Pair<F, S>)


[fit] (L → V, R → V) → (Either<L,R> → V)


[fit] Either<A,B> → C ← Pair<A,B>


[fit] Exponentiation


[fit] RA


[fit] type Reader<A, R> = (a: A) => R;


[fit] How many values of type

[fit] RGBBool

[fit] are there?


[fit] (b: Bool) => RGB

const boolToRGB: ((b: Bool) => RGB)[] = [
  (_) => "red",
  (_) => "green",
  (_) => "blue",
  (b) => (b ? "red" : "green"),
  (b) => (b ? "red" : "blue"),
  (b) => (b ? "green" : "red"),
  (b) => (b ? "green" : "blue"),
  (b) => (b ? "blue" : "red"),
  (b) => (b ? "blue" : "green"),
];

[fit] Laws


[fit] 1A ≅ 1


[fit] A → Unit ≅ Unit


[fit] A1 ≅ A


[fit] Unit → A ≅ A


[fit] (B × C)ᴬ = Bᴬ × Cᴬ


[fit] A → Pair<B,C> ≅ Pair<A → B, A → C>


[fit] Cᴮᴬ = (Cᴮ)ᴬ


[fit] Pair<A,B> → C ≅ A → B → C


[fit] Curry ≅ Uncurry


[fit] Curry

const curry =
  <A, B, C>(fn: (a: A, b: B) => C): ((a: A) => (b: B) => C) =>
  (a: A) =>
  (b: B) =>
    fn(a, b);

[fit] Uncurry

const uncurry =
  <A, B, C>(fn: (a: A) => (b: B) => C): ((a: A, b: B) => C) =>
  (a: A, b: B) =>
    fn(a)(b);

[fit] Division


[fit] Recursion


[fit] Lists


[fit] 1 + A × List<A>


class Nil<A> {
  readonly _tag = "Nil" as const;
  private _phantom!: A;
}

class Cons<A> {
  readonly _tag = "Cons" as const;
  constructor(
    readonly head: A,
    readonly tail: List<A>,
  ) {}
}

type List<A> = Nil<A> | Cons<A>;

[fit] List<A> = 1 + A × List<A>


[fit] List<A> - A × List<A> = 1


[fit] List<A> × (1 - A) = 1


[fit] List<A> = 1 / (1 - A)


[fit] A Taylor series is a way to represent a function
as an infinite sum of polynomial terms,
calculated from the function's derivatives at a single point.


[fit] List<A> = 1 + A × List<A>


[fit] List<A> = 1 + A × (1 + A × List<A>)


[fit] List<A> = 1 + A + (A×A) × List<A>


[fit] List<A> = 1 + A + (A×A) × (1 + A × List<A>)


[fit] List<A> = 1 + A + (A×A) + (A×A×A) × List<A>


[fit] List<A> = 1 + A + (A×A) + (A×A×A) × (1 + A × List<A>)


[fit] List<A> = 1 + A + (A×A) + (A×A×A) + (A×A×A×A) × List<A>


[fit] List<A> = 1 / (1 - A)

List<A> = 1 / (1 - A)
        = 1 + A + A×A + A×A×A + A×A×A×A + ...
        = 1 + A + A² + A³ + A+ ...

[fit] Algebraic
structures


[fit] Group-like

[fit] one binary operation


[fit] Ring-like

[fit] two binary operations,

[fit] often called addition and multiplication,

[fit] with multiplication distributing over addition.


[fit] Lattice-like

[fit] two or more binary operations,

[fit] including operations called meet and join,

[fit] connected by the absorption law


[fit] Module-like

[fit] composite systems involving two sets

[fit] and employing at least two binary operations


[fit] Algebra-like

[fit] composite system defined over two sets,

[fit] a ring R and an R-module M equipped with

[fit] an operation called multiplication


[fit] Group-like

[fit] structures


[fit] Magma


[fit] A type with a
(closed) binary operation


[fit] A × A → A


[fit] A × A → A

interface Magma<A> {
  readonly concat: (x: A, y: A) => A;
}

[fit] Semigroup


[fit] A magma where
the operation is associative


[fit] A × A → A

interface Semigroup<A> extends Magma<A> {}

[fit] concat(concat(a, b), c) ≅ concat(a, concat(b, c))


[fit] Monoid


[fit] A semigroup with
an identity element


[fit] A × A + 1 → A

interface Monoid<A> extends Semigroup<A> {
  readonly empty: A;
}

[fit] concat(empty, x) ≅ x

[fit] concat(x, empty) ≅ x


[fit] Monoid
Examples


[fit] Sum

const Sum: Monoid<number> = {
  concat: (x, y) => x + y,
  empty: 0
};
```

---

# [fit] **Sum**

```typescript
const values = [5, 10, 3];

let total = Sum.empty; // start at 0

for (const n of values) {
  total = Sum.concat(total, n); // accumulate via concat
}

console.log(total); // 18

[fit] Product

const Product: Monoid<number> = {
  concat: (x, y) => x * y,
  empty: 1
};
```
---

# [fit] **Product**

```typescript
const values = [5, 10, 3];

let total = Product.empty; // start at 1

for (const n of values) {
  total = Product.concat(total, n); // accumulate via concat
}

console.log(total); // 150

[fit] Endo

type Endo<A> = (a: A) => A;

const getEndoMonoid = <A>(): Monoid<Endo<A>> => ({
  concat: (f, g) => (x) => f(g(x)),
  empty: (x) => x,
});

[fit] Endo

const EndoNumber = getEndoMonoid<number>();

const multiplyBy =
  (n: number): Endo<number> =>
  (x) =>
    x * n;
const functions = values.map(multiplyBy); // [x => x*5, x => x*10, x => x*3]

let composedFunction = EndoNumber.empty;
for (const fn of functions) {
  composedFunction = EndoNumber.concat(composedFunction, fn);
}

const endoResult = composedFunction(1);
console.log("Endo result:", endoResult); // 150

[fit] List

const arrayMonoid = <T>(): Monoid<T[]> => ({
  concat: (x: T[], y: T[]) => [...x, ...y],
  empty: [],
});

const stringArrayMonoid = arrayMonoid<string>();

const a = ["hello", "world"];
const b = ["foo", "bar"];
const c = ["baz"];

const result1 = stringArrayMonoid.concat(a, b);
console.log(result1); // ["hello", "world", "foo", "bar"]

const result2 = stringArrayMonoid.concat(a, stringArrayMonoid.empty);
console.log(result2); // ["hello", "world"]

[fit] Algebraic
Thinking


[fit] No means no


// ❌ BAD: Using null/undefined to mean something specific
interface BadSubscriptionPlan {
  name: string;
  maxUsers: number | null; // null means "unlimited users"
  maxStorage: number | null; // null means "unlimited storage"
}

const enterprisePlan: BadSubscriptionPlan = {
  name: "Enterprise",
  maxUsers: null, // This means unlimited, not "we don't know"
  maxStorage: null, // This means unlimited, not "not set"
};

function checkUserLimit(
  plan: BadSubscriptionPlan,
  currentUsers: number,
): boolean {
  if (plan.maxUsers === null) {
    // null means unlimited, so always allow
    return true;
  }
  return currentUsers <= plan.maxUsers;
}

// ✅ GOOD: Be explicit about what you mean
type Limited = { type: "limited"; max: number };
type Unlimited = { type: "unlimited" };
type StorageLimit = Limited | Unlimited;

interface GoodSubscriptionPlan {
  name: string;
  userLimit: Limited | Unlimited;
  storageLimit: StorageLimit;
}

const goodEnterprisePlan: GoodSubscriptionPlan = {
  name: "Enterprise",
  userLimit: { type: "unlimited" }, // Clear meaning
  storageLimit: { type: "unlimited" }, // Clear meaning
};

function checkUserLimitGood(
  plan: GoodSubscriptionPlan,
  currentUsers: number,
): boolean {
  switch (plan.userLimit.type) {
    case "unlimited":
      return true;
    case "limited":
      return currentUsers <= plan.userLimit.max;
  }
}

[fit] Parse

[fit] Don't Validate


// ❌ BAD: Validation approach
function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function sendEmail(email: string, message: string) {
  if (!validateEmail(email)) {
    throw new Error("Invalid email");
  }
  // email is still just a string here!
}```

// ✅ GOOD: Parse approach
type Email = { readonly _tag: "Email"; readonly value: string };

function parseEmail(input: string): Email | null {
  if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
    return { _tag: "Email", value: input };
  }
  return null;
}

function sendEmailSafe(email: Email, message: string) {
  // email is guaranteed to be valid here!
  console.log(`Sending to ${email.value}: ${message}`);
}

// ❌ BAD: Validation approach

function validateNonEmpty<T>(arr: T[]): boolean {
  return arr.length > 0;
}

function getFirstElement<T>(arr: T[]): T {
  if (!validateNonEmpty(arr)) {
    throw new Error("Array is empty!");
  }
  return arr[0]; // TypeScript still thinks this could be undefined!
}

// ✅ GOOD: Parse approach
type NonEmptyArray<T> = readonly [T, ...T[]];

function parseNonEmptyArray<T>(arr: readonly T[]): NonEmptyArray<T> | null {
  if (arr.length === 0) return null;
  return arr as NonEmptyArray<T>;
}

// Now all these functions are completely safe with no defensive checks:

function head<T>(arr: NonEmptyArray<T>): T {
  return arr[0]; // Always defined!
}```

[fit] Make impossible
states impossible


// ❌ BAD: Invalid states are possible

interface BadUserStatus {
  isLoggedIn: boolean;
  username?: string;
  lastLoginTime?: Date;
  isGuest: boolean;
  guestId?: string;
  isPremium: boolean;
  subscriptionExpiry?: Date;
}

// Functions need defensive checks everywhere
function greetUser(user: BadUserStatus): string {
  if (user.isLoggedIn && user.username) {
    if (user.isGuest) {
      // Wait, how can they be logged in with username AND be a guest?
      return "Hello... uh...";
    }
    return `Welcome back, ${user.username}`;
  } else if (user.isGuest && user.guestId) {
    return `Welcome, Guest ${user.guestId}`;
  } else {
    return "Please log in";
  }
}

// ✅ GOOD: Make impossible states impossible
type Guest = {
  type: "guest";
  guestId: string;
};

type FreeUser = {
  type: "free";
  username: string;
  lastLoginTime: Date;
};

type PremiumUser = {
  type: "premium";
  username: string;
  lastLoginTime: Date;
  subscriptionExpiry: Date;
};

type LoggedOut = {
  type: "logged-out";
};

type UserStatus = Guest | FreeUser | PremiumUser | LoggedOut;

// ✅ GOOD: Make impossible states impossible
// Clean, exhaustive handling with no defensive checks
function greetUserGood(user: UserStatus): string {
  switch (user.type) {
    case "guest":
      return `Welcome, Guest ${user.guestId}`;
    case "free":
      return `Welcome back, ${user.username}`;
    case "premium":
      return `Welcome back, ${user.username} ⭐`;
    case "logged-out":
      return "Please log in";
  }
}

[fit] Composing prompts


[fit] Customizing tabs
different clients

theme: Franziska footer: OpenAI Agents JS SDK - Technical Deep Dive slidenumbers: true autoscale: true

OpenAI Agents JS SDK

[fit] Multi-Agent Orchestration at Scale


Start Simple

import { Agent, run } from '@openai/agents';

const agent = new Agent({
  name: 'Assistant',
  instructions: 'You are a helpful agent',
});

const result = await run(agent, 'What is the capital of France?');

console.log(result.finalOutput);
// "The capital of France is Paris."

^ Zero configuration to get started. Just an agent and a prompt.


Add a Tool

import { Agent, run, tool } from '@openai/agents';

const getWeather = tool({
  name: 'get_weather',
  description: 'Get the weather for a city',
  parameters: { city: 'string' }, // Simple schema
  execute: async ({ city }) => {
    return {
      city,
      temperature: '20°C',
      conditions: 'Sunny',
    };
  },
});

const agent = new Agent({
  name: 'Weather Assistant',
  instructions: 'Help users with weather information',
  tools: [getWeather],
});

^ Tools extend agent capabilities. Execute functions based on user needs.


Running the Agent

async function main() {
  const result = await run(agent, "What's the weather in Tokyo?");

  console.log(result.finalOutput);
  // "The weather in Tokyo is sunny with a
  //  temperature of 20°C."

  // Inspect what happened
  console.log(result.toolCalls);
  // [{ name: 'get_weather', arguments: { city: 'Tokyo' } }]

  console.log(result.usage);
  // { inputTokens: 45, outputTokens: 23, totalTokens: 68 }
}

^ The agent automatically calls tools when needed. Full observability included.


[.text: alignment(center)]

Tools & Functions


Tool System Architecture

[.build-lists: true]

  • Function Tools: Local execution with Zod validation
  • Hosted Tools: OpenAI server-side execution
  • MCP Tools: Model Context Protocol servers
  • Agent Tools: Recursive composition

^ Extensible tool system. New tool types can be added.


Forcing Tool Use: tool_choice

import { Agent, run } from '@openai/agents';

const agent = new Agent({
  name: 'DataAnalyst',
  tools: [queryTool, analyzeTool, reportTool],
});

// Option 1: Auto (default) - Model decides
await run(agent, 'Analyze sales', {
  modelSettings: { toolChoice: 'auto' },
});

// Option 2: Required - Must use SOME tool
await run(agent, 'Analyze sales', {
  modelSettings: { toolChoice: 'required' },
});

// Option 3: None - Prevent ALL tool use
await run(agent, 'Just explain the process', {
  modelSettings: { toolChoice: 'none' },
});

// Option 4: Specific tool - Force exact tool
await run(agent, 'Get the data', {
  modelSettings: { toolChoice: 'queryTool' },
});

^ Control exactly when and which tools are used


tool_choice: What It Controls

// tool_choice works with ALL tool types:

const agent = new Agent({
  tools: [
    // Function tools (local execution)
    tool({ name: 'calculate', execute: async () => {...} }),

    // MCP tools from local servers
    mcpServer,

    // Hosted MCP tools (OpenAI-side)
    hostedMcpTool({ serverLabel: 'data-api' }),

    // Agents as tools
    specialistAgent,
  ],
});

// tool_choice applies to ALL of them:
await run(agent, input, {
  modelSettings: {
    toolChoice: 'required',     // Must use SOME tool
    toolChoice: 'calculate',     // Force function tool
    toolChoice: 'transfer_to_specialist', // Force handoff
    // Note: Individual MCP tools can't be forced by name
    // (MCP server decides which of its tools to use)
  },
});

^ tool_choice is universal BUT MCP tools are grouped by server


tool_choice Patterns

// Pattern 1: Force tool on first turn only
const result = await run(agent, input, {
  modelSettings: {
    toolChoice: messages.length === 0 ? 'required' : 'auto'
  },
});

// Pattern 2: Prevent tool loops
const agent = new Agent({
  tools: [recursiveTool],
  maxTurns: 5,
});

await run(agent, input, {
  // Disable tools after 3 turns to prevent infinite loops
  modelSettings: {
    toolChoice: turn > 3 ? 'none' : 'auto'
  },
});

// Pattern 3: Sequential tool enforcement
const steps = ['gather_data', 'analyze', 'generate_report'];
for (const toolName of steps) {
  await run(agent, `Step: ${toolName}`, {
    modelSettings: { toolChoice: toolName },
  });
}

^ Advanced patterns for controlled tool execution


Parallel Tool Calls

// Enable parallel tool execution
const agent = new Agent({
  name: 'EfficientAnalyst',
  tools: [fetchUserData, fetchOrders, fetchInventory, computeMetrics],
});

// Allow model to call multiple tools in one turn
const result = await run(agent, 'Get all customer data and metrics', {
  modelSettings: {
    parallelToolCalls: true, // Default: false
    toolChoice: 'required',
  },
});

// Model might call all four tools simultaneously:
// Turn 1: [fetchUserData, fetchOrders, fetchInventory, computeMetrics]
// All execute in parallel, results returned together

// Without parallel calls (default):
await run(agent, 'Get all customer data and metrics', {
  modelSettings: {
    parallelToolCalls: false,
  },
});
// Turn 1: fetchUserData
// Turn 2: fetchOrders
// Turn 3: fetchInventory
// Turn 4: computeMetrics
// Much slower, but more controlled

^ Parallel tools = faster execution, fewer turns


Function Tool: Zod Schema Validation

const analyticsTool = tool({
  name: 'analyze_metrics',
  parameters: z.object({
    metric: z.enum(['revenue', 'users', 'churn']),
    timeframe: z.string().regex(/^\d{4}-\d{2}$/),
    segments: z.array(z.string()).optional(),
  }),
  strict: true, // Enforce schema compliance
  execute: async (input, context) => {
    // input is fully typed from Zod schema
    // TypeScript knows: input.metric is 'revenue' | 'users' | 'churn'
    return computeMetrics(input, context.database);
  },
});

^ Zod provides runtime validation AND TypeScript types


Tool Error Handling

const resilientTool = tool({
  name: 'external_api',
  parameters: z.object({ query: z.string() }),
  execute: async (input, context) => {
    try {
      return await externalAPI.query(input.query);
    } catch (error) {
      if (error.code === 'RATE_LIMIT') {
        // Return structured error for agent to understand
        return {
          error: 'Rate limited. Please try again in 60 seconds.',
          retryAfter: 60,
        };
      }
      // Re-throw unexpected errors
      throw error;
    }
  },
});

^ Tools should handle expected errors, re-throw unexpected ones


Tool Composition Strategies

// Sequential composition
const pipelineTool = tool({
  name: 'data_pipeline',
  parameters: z.object({
    data: z.any(),
  }),
  execute: async (input, context) => {
    const extracted = await extractTool.execute(input, context);
    const transformed = await transformTool.execute(extracted, context);
    const loaded = await loadTool.execute(transformed, context);
    return loaded;
  },
});

// Parallel composition
const aggregateTool = tool({
  name: 'aggregate_sources',
  parameters: z.object({
    query: z.string(),
  }),
  execute: async (input, context) => {
    const [db, api, cache] = await Promise.all([
      dbTool.execute(input, context),
      apiTool.execute(input, context),
      cacheTool.execute(input, context),
    ]);
    return mergeResults(db, api, cache);
  },
});

^ Tools can orchestrate other tools for complex operations


[fit] Hosted Tools:
Server-Side Execution

[fit] Run on OpenAI infrastructure,
not your code


Run on OpenAI infrastructure, not your code

import {
  webSearchTool,
  fileSearchTool,
  codeInterpreterTool,
} from '@openai/agents';

const agent = new Agent({
  tools: [
    // Web Search - Internet search capabilities
    webSearchTool({
      maxResults: 5,
      locationBias: 'us',
      blockedDomains: ['example.com'],
    }),

    // File Search - Query OpenAI vector stores
    fileSearchTool({
      vectorStoreId: 'vs_abc123',
      maxResults: 10,
    }),

    // Code Interpreter - Sandboxed code execution
    codeInterpreterTool({
      containerOptions: { memory: '512MB' },
    }),
  ],
});

^ No round-trips to your app. Execute directly on OpenAI servers.


[.text: alignment(center)]

Function Calling


Function Calling: Native Tools

Direct code execution in your process

const calculateTax = tool({
  name: 'calculate_tax',
  description: 'Calculate tax for a transaction',
  parameters: z.object({
    amount: z.number(),
    state: z.string(),
    category: z.enum(['sales', 'income', 'property']),
  }),
  execute: async ({ amount, state, category }, context) => {
    // Your code runs directly in the same process
    const rate = await context.taxService.getRate(state, category);
    const tax = amount * rate;
    await context.database.logCalculation({ amount, state, tax });
    return { tax, rate, total: amount + tax };
  },
});

^ Function tools are YOUR code running in YOUR process with full control.


Function Calling: Benefits

Why use function calling:

Full control - Your code, your logic ✅ Direct access - Database, APIs, file system ✅ Type safety - TypeScript + Zod validation ✅ Synchronous - Immediate execution ✅ Debugging - Set breakpoints, inspect state ✅ Performance - No network overhead ✅ Security - Runs in your security context


[.text: alignment(center)]

MCP


MCP: Model Context Protocol

SDK Version & Specification Support

// packages/agents-core/package.json
"@modelcontextprotocol/sdk": "^1.17.2"

// Version Details
 MCP SDK 1.17.2 (Released: August 7, 2025)
 JSON-RPC 2.0 protocol
 Tool discovery & execution
 Multiple transport protocols
 OAuth authentication (HTTP/SSE)
 Tool filtering & caching
 Session management
 Node.js 18+ required

// Transport Support
const transports = {
  stdio: MCPServerStdio,        // Local processes
  sse: MCPServerSSE,            // Server-sent events
  http: MCPServerStreamableHttp, // HTTP streaming
  hosted: HostedMCPTool,        // OpenAI connects to remote MCP servers
};

^ Full MCP 1.17.2 specification support with all transport protocols


MCP: Three Server Types

import {
  MCPServerStdio,
  MCPServerSSE,
  MCPServerStreamableHttp,
} from '@openai/agents';

// 1. Stdio - Local process communication
const stdioServer = new MCPServerStdio({
  name: 'GitHub MCP Server',
  fullCommand: 'npx -y @modelcontextprotocol/server-github',
  env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN },
});
await stdioServer.connect();

// 2. SSE - Server-Sent Events
const sseServer = new MCPServerSSE({
  name: 'Remote MCP',
  url: 'https://api.example.com/mcp/sse',
});
await sseServer.connect();

// 3. Streamable HTTP (Request/Response)
const httpServer = new MCPServerStreamableHttp({
  name: 'HTTP MCP',
  url: 'https://api.example.com/mcp',
});
await httpServer.connect();

^ MCP supports multiple transport protocols for different use cases


MCP: Hosted MCP Tools Explained

// HostedMCPTool doesn't mean "hosted by OpenAI"
// It means "OpenAI connects to YOUR hosted MCP server"

const hostedTool = hostedMcpTool({
  server_url: 'https://your-server.com/mcp', // YOUR server
  server_label: 'My MCP Server',
  authorization: { type: 'bearer', token: 'your-token' },
});

// Flow:
// 1. Agent needs tool → 2. OpenAI calls YOUR server
// 3. YOUR server executes → 4. OpenAI relays result back

// Compare with local MCP:
const localMCP = new MCPServerStdio({...}); // Runs on YOUR machine
const hostedMCP = hostedMcpTool({...});      // Runs on YOUR server
// Both execute on YOUR infrastructure, not OpenAI's

^ HostedMCPTool = OpenAI connects to YOUR remote MCP server, not OpenAI hosting


MCP: Tool Discovery Protocol

// MCP servers expose tools dynamically
const agent = new Agent({
  mcpServers: [githubMCP, filesystemMCP],
  // Tools are discovered at runtime
});

// Get all available MCP tools
const tools = await agent.getMcpTools(context);
console.log(tools);
// [
//   { name: 'github_create_issue', parameters: {...} },
//   { name: 'fs_read_file', parameters: {...} },
//   ...
// ]

^ MCP tools are discovered dynamically, not hardcoded


Function Calling vs MCP

When to use each approach:

[.column] Function Calling Best for: Core application logic

✅ Your business logic ✅ Database operations ✅ Internal APIs ✅ Custom algorithms ✅ Sensitive operations ✅ Need debugging ✅ Performance critical

[.column] MCP Best for: External integrations

✅ Third-party tools ✅ Community tools ✅ Standalone services ✅ Language agnostic ✅ Shared capabilities ✅ Desktop integrations ✅ Standardized tools

^ Function calling for YOUR code. MCP for EXTERNAL tools.


Function Calling vs MCP: Technical

Aspect Function Calling MCP
Execution In-process Separate process/network
Performance Fast (no overhead) Network latency
Debugging Direct (breakpoints) Remote (logs)
Type Safety Full TypeScript Schema-based
Access Everything in app Limited to MCP API
State Shared context Isolated
Deployment With your app Separate service
Updates Redeploy app Update MCP server

^ Function calling is tightly integrated. MCP is loosely coupled.


Combining Both Approaches

const agent = new Agent({
  // Core business logic as function tools
  tools: [
    calculatePricingTool, // Your pricing algorithm
    updateInventoryTool, // Your database operations
    sendNotificationTool, // Your notification system
  ],

  // External capabilities via MCP
  mcpServers: [
    githubMCP, // GitHub operations
    slackMCP, // Slack integration
    filesystemMCP, // File operations
  ],
});

// The agent can use both seamlessly
// "Calculate the price, update inventory, create a GitHub issue,
//  and notify the team on Slack"

^ Use both! Function tools for core logic, MCP for external services.


[.text: alignment(center)]

Context Management


[fit] Context Management Deep Dive

Two-Layer Context System

// Layer 1: Local Context (RunContext) - YOUR application state
class RunContext<TContext> {
  context: TContext; // Mutable shared state
  usage: Usage; // Token/cost tracking
  #approvals: Map<string, ApprovalRecord>; // HITL state
}

// Layer 2: Agent/LLM Context - Conversation history
type AgentContext = ModelItem[]; // Messages, tool calls, responses

^ Two distinct contexts serve different purposes. Local = infrastructure. Agent = conversation.


Local Context: Infrastructure & Services

interface LocalContext {
  // 🔌 External Services
  database: PostgresClient;
  redis: RedisCache;
  elasticsearch: SearchClient;
  stripe: StripeAPI;
  sendgrid: EmailService;
  twilio: SMSService;

  // 🔐 Authentication & Session
  user: { id: string; tier: 'free' | 'pro'; permissions: string[] };
  apiKeys: { openai: string; anthropic: string };
  sessionId: string;
  requestId: string;

  // 🚦 Rate Limiting & Circuit Breakers
  rateLimiter: RateLimiter;
  circuitBreaker: CircuitBreaker;

  // 📊 Observability
  logger: Logger;
  metrics: MetricsCollector;
  tracer: TracingSpan;
}

^ Local context holds everything tools need to do their job. Shared across all tools.


Local Context: Application State

interface LocalContext {
  // 🛒 Transaction State
  cart: ShoppingCart;
  order: Order;
  payment: PaymentTransaction;

  // 🧮 Computation State
  calculationCache: Map<string, Result>;
  tempFiles: TempFileManager;
  batchProcessor: BatchJob;

  // 🎯 Feature Flags & Config
  features: FeatureFlags;
  config: AppConfig;
  experiments: ABTestManager;

  // 🔄 Cross-Agent Shared State
  workflow: WorkflowState;
  conversationSummary: string;
  decisions: Decision[];
  accumulatedData: any[];
}

^ Local context maintains state that persists across tool calls and agent handoffs.


Agent/LLM Context: Conversation History

type AgentContext = Array<
  | { role: 'system'; content: string } // Instructions
  | { role: 'user'; content: string } // User messages
  | { role: 'assistant'; content: string } // Agent responses
  | { role: 'tool'; name: string; result: any } // Tool outputs
  | { role: 'handoff'; from: string; to: string } // Agent transfers
  | { role: 'error'; error: string } // Errors
  | { role: 'reasoning'; thought: string } // Chain-of-thought
>;

^ Agent context is what the LLM "sees" - the conversation that guides its decisions.


Why Two Contexts?

Key Insight

Local Context = Infrastructure (Never sent to LLM)

Agent Context = Conversation (What the LLM sees)

[.column] Local Context ✓ Persists across agents ✓ Mutable by tools ✓ Never sent to LLM ⚠️ ✓ Contains secrets ✓ Holds connections ✓ Tracks metrics

[.column] Agent ContextSent to LLM 🧠 ✓ Can be filtered ✓ Size-limited ✓ No secrets ✓ Natural language ✓ Guides reasoning

^ Critical distinction: Local = your app's plumbing. Agent = the LLM's memory.


Context Usage in Tools

const processPayment = tool({
  name: 'process_payment',
  execute: async (input, context: LocalContext) => {
    // Use local context services
    const user = context.user
    const stripe = context.stripe
    const logger = context.logger
    const metrics = context.metrics

    // Check rate limits
    if (!await context.rateLimiter.check(user.id)) {
      throw new Error('Rate limit exceeded')
    }

    // Process with circuit breaker
    const result = await context.circuitBreaker.run(async () => {
      const charge = await stripe.charges.create({...})
      await logger.info('Payment processed', { charge })
      await metrics.increment('payments.success')
      return charge
    })

    // Update shared state
    context.order.paymentId = result.id
    return result
  }
})

^ Tools leverage local context for all infrastructure needs.


SDK Context Management Recommendations

The SDK's design philosophy for context:

[.column] Local Context (RunContext) ✅ Infrastructure & services ✅ Database connections ✅ User session data ✅ Shared mutable state ✅ Secrets & API keys ❌ Never sent to LLM

[.column] Agent Context ✅ Conversation history ✅ User messages ✅ Tool outputs ✅ Handoff records ❌ No infrastructure ✅ Sent to LLM

^ SDK philosophy: Clear separation of concerns. Infrastructure vs conversation.


Context + Responses API

Triple-Layer Architecture

// With Responses API enabled (default), context becomes triple-layered:

// Layer 1: Local Context (RunContext) - Client-side state
const localContext = {
  database: pgClient,
  user: currentUser,
  // Never sent to API
};

// Layer 2: Server Context - OpenAI's stored conversation state
const serverContext = {
  previousResponseId: 'resp_abc123...', // Links to full history
  conversationId: 'conv_xyz789...', // Thread identifier
  // Stored on OpenAI servers, not transmitted
};

// Layer 3: Request Payload - What's actually sent
const requestPayload = {
  input: newMessages, // Only NEW messages
  previous_response_id: serverContext.previousResponseId,
  // 80%+ smaller than sending full history
};

^ Responses API adds server-side context layer, dramatically reducing payload size


How Responses API Changes
Context Flow

// FIRST TURN - No previous context
const turn1 = await run(agent, 'Explain quantum computing', {
  context: { user: 'alice' }, // Local context
});
// Sends: Full input to API
// Returns: response_id for future reference

// SUBSEQUENT TURNS - References server state
const turn2 = await run(agent, 'How does it differ from classical?', {
  context: { user: 'alice' }, // Same local context
  previousResponseId: turn1.lastResponseId, // Links server conversation
});
// Sends: Only new message + previousResponseId
// Server retrieves and appends to stored conversation

// HANDOFF - Maintains both contexts
const turn3 = await run(agent, 'Let me transfer you to our expert', {
  context: { user: 'alice' }, // Local context persists
  previousResponseId: turn2.lastResponseId, // Server thread continues
});
// Handoff inherits both local context AND server conversation thread

^ Server maintains conversation history, client maintains application state


Context Management Best Practices with Responses API

[.column] What Stays Local

✅ Infrastructure ✅ Secrets & keys ✅ Mutable state ✅ Tool dependencies

// Never sent, always available
context: {
  connections: {...},
  secrets: {...},
  session: {...},
  cache: {...}
}

[.column] What Goes to Server

✅ Message history ✅ Tool results ✅ Agent responses ✅ Automatically managed

// Via previousResponseId
{
  (conversation_history, tool_outputs, handoff_records, reasoning_traces);
}

^ Responses API handles conversation persistence, you handle application state


[.text: alignment(center)]

Voice-Based Agents


Voice Agents: Introduction

import { RealtimeAgent, RealtimeSession } from '@openai/agents-realtime';
import { z } from 'zod';
import { tool } from '@openai/agents';

const weatherTool = tool({
  name: 'get_weather',
  description: 'Get current weather',
  parameters: z.object({ city: z.string() }),
  execute: async ({ city }) => `${city}: 72°F, sunny`,
});

const voiceAgent = new RealtimeAgent({
  name: 'VoiceAssistant',
  instructions:
    'You are a helpful voice assistant. Be conversational and natural.',
  tools: [weatherTool],
  voice: 'alloy', // Options: alloy, echo, shimmer, nova
  model: 'gpt-4o-realtime-preview',
});

^ RealtimeAgent enables natural voice conversations with tool calling


Voice Agents: Browser Implementation

// Browser-side code (React/Next.js example)
import { RealtimeSession } from '@openai/agents-realtime';

export function VoiceChat() {
  const [session, setSession] = useState<RealtimeSession>();

  async function startConversation() {
    // Get ephemeral token from your backend
    const { apiKey } = await fetch('/api/realtime-token').then(r => r.json());

    const session = new RealtimeSession(agent);

    // Connect and auto-configure audio I/O
    await session.connect({ apiKey });

    // Session now handles:
    // ✅ Microphone input
    // ✅ Speaker output
    // ✅ Turn detection
    // ✅ Audio streaming

    setSession(session);
  }

  return <button onClick={startConversation}>Start Voice Chat</button>;
}

^ Browser SDK handles all audio I/O automatically


Voice Agents: Transport Options

// Option 1: WebRTC (Lowest latency, recommended)
const webrtcSession = new RealtimeSession(agent, {
  transport: 'webrtc',
  config: {
    iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
    enableDataChannel: true, // For tool results
  },
});

// Option 2: WebSocket (Simpler, good compatibility)
const wsSession = new RealtimeSession(agent, {
  transport: 'websocket',
  url: 'wss://api.openai.com/v1/realtime',
});

// Performance comparison:
// WebRTC:    ~100ms latency, P2P capable
// WebSocket: ~200ms latency, simpler firewall traversal

^ Choose WebRTC for lowest latency, WebSocket for simplicity


Voice Agents: Audio Configuration

// Agent configuration (simple)
const agent = new RealtimeAgent({
  name: 'AudioOptimized',
  instructions: 'You are a helpful voice assistant',
  voice: 'alloy', // Voice selection
  tools: [weatherTool],
});

// Session configuration (detailed audio settings)
const session = new RealtimeSession(agent, {
  // Turn detection configuration
  turnDetection: {
    type: 'server_vad', // Voice Activity Detection
    threshold: 0.5, // Sensitivity (0-1)
    prefixPaddingMs: 300, // Keep audio before speech
    silenceDurationMs: 500, // Pause = end of turn
  },

  // Audio format configuration
  inputAudioFormat: 'pcm16', // Options: pcm16, g711_ulaw, g711_alaw
  outputAudioFormat: 'pcm16',
  inputAudioTranscription: {
    model: 'whisper-1', // For debugging/logs
  },

  modalities: ['text', 'audio'], // Both supported
});

^ Agent defines behavior, Session handles audio configuration


Voice Agents: Interruption Handling

const agent = new RealtimeAgent({
  // Allow user interruption mid-response
  interruptions: true,

  instructions: `
    You are having a natural conversation.
    If interrupted, gracefully stop and listen.
    Don't repeat yourself after interruptions.
  `,
});

// Handle interruption events
session.on('interruption', (event) => {
  console.log('User interrupted at:', event.timestamp);
  // Agent automatically stops speaking
  // Listening resumes immediately
});

// Manual interruption control
session.interrupt(); // Stop agent mid-speech
session.clearAudioBuffer(); // Clear pending audio

^ Natural conversation flow with interruption support


Voice Agents: Tool Calling in Voice

const agent = new RealtimeAgent({
  tools: [bookingTool, searchTool],
  instructions: 'Explain what you are doing as you use tools',
});

// Tools work identically to text agents
session.on('tool_call', async (event) => {
  // Agent might say: "Let me check availability for you..."

  const result = await event.tool.execute(event.params);

  // Agent continues: "I found 3 available slots..."
  session.sendToolResult({
    toolCallId: event.id,
    result,
  });
});

// Streaming tool updates
session.on('tool_progress', (event) => {
  // Real-time progress for long-running tools
  console.log(`${event.tool}: ${event.progress}%`);
});

^ Tools integrate seamlessly with voice conversations


Voice Agents: Multi-Modal Interactions

// Agent that handles both voice and text
const multiModalAgent = new RealtimeAgent({
  modalities: ['text', 'audio'],
  tools: [screenshotTool, drawingTool],
});

// Voice with visual responses
session.on('tool_result', (event) => {
  if (event.tool === 'generate_chart') {
    // Display chart while agent describes it
    displayChart(event.result.imageUrl);

    // Agent says: "As you can see in the chart..."
  }
});

// Switch modalities mid-conversation
session.sendText('Show me a graph'); // Text input
// Agent responds with voice explanation + visual

session.sendAudio(audioBuffer); // Voice input
// Agent responds appropriately

^ Combine voice, text, and visual elements seamlessly


Voice Agents: State Management

class VoiceAgentManager {
  private sessions = new Map<string, RealtimeSession>();
  private states = new Map<string, ConversationState>();

  async createSession(userId: string): Promise<RealtimeSession> {
    // Restore previous conversation state
    const state = await this.loadState(userId);

    const agent = new RealtimeAgent({
      instructions: this.buildInstructions(state),
      context: state.context,
    });

    const session = new RealtimeSession(agent);

    // Track conversation events
    session.on('message', (msg) => {
      state.messages.push(msg);
      this.saveState(userId, state);
    });

    this.sessions.set(userId, session);
    return session;
  }

  async resumeSession(userId: string): Promise<RealtimeSession> {
    return this.sessions.get(userId) || this.createSession(userId);
  }
}

^ Maintain conversation continuity across sessions


Voice Agents: Handoffs in Voice

const receptionistAgent = new RealtimeAgent({
  name: 'Receptionist',
  instructions: 'Greet users and route to specialists',
  handoffs: [supportAgent, salesAgent],
});

const supportAgent = new RealtimeAgent({
  name: 'Support',
  instructions: 'Provide technical assistance',
  handoffDescription: 'Technical issues and troubleshooting',
});

// Voice handoff with context
session.on('handoff', async (event) => {
  // Receptionist: "Let me transfer you to our support team..."

  // Smooth audio transition
  await session.playTransitionSound();

  // Support: "Hi, I see you're having issues with..."
  await session.switchAgent(event.targetAgent, {
    preserveContext: true,
    announcement: 'support_greeting.mp3',
  });
});

^ Smooth agent transitions in voice conversations


Voice Agents: Production Patterns

// Voice agent with fallback
class ResilientVoiceAgent {
  async connect(apiKey: string) {
    try {
      // Try WebRTC first
      return await this.connectWebRTC(apiKey);
    } catch (error) {
      console.warn('WebRTC failed, falling back to WebSocket');
      return await this.connectWebSocket(apiKey);
    }
  }

  // Reconnection logic
  setupReconnection(session: RealtimeSession) {
    session.on('disconnect', async () => {
      for (let i = 0; i < 3; i++) {
        await sleep(1000 * Math.pow(2, i)); // Exponential backoff

        try {
          await session.reconnect();
          break;
        } catch (e) {
          console.error(`Reconnect attempt ${i + 1} failed`);
        }
      }
    });
  }
}

^ Production-ready patterns for reliability


Voice Agents: Cost Optimization

// Optimize voice agent costs
const costOptimizedAgent = new RealtimeAgent({
  model: 'gpt-4o-realtime-preview',

  // Reduce costs with smart settings
  turnDetection: {
    silenceDurationMs: 300, // Faster turn ending
  },

  // Limit response length
  maxResponseDurationMs: 30000, // 30 second max

  // Use text for simple responses
  responseStrategy: async (input) => {
    if (isSimpleQuery(input)) {
      return { modality: 'text' }; // Cheaper
    }
    return { modality: 'audio' }; // Natural
  },
});

// Monitor usage
session.on('usage', (event) => {
  console.log('Audio seconds:', event.audioSeconds);
  console.log('Token count:', event.tokens);
  console.log('Estimated cost: $', event.estimatedCost);
});

^ Control costs while maintaining quality


Voice Agents: Advanced Features

// Agent with voice selection
const agent = new RealtimeAgent({
  name: 'Professional Assistant',
  voice: 'nova', // Options: alloy, echo, shimmer, nova
  instructions: 'Respond professionally and concisely',
});

// Session with advanced audio processing
const session = new RealtimeSession(agent, {
  // Audio processing (browser-side implementation)
  audioConstraints: {
    noiseSuppression: true,
    echoCancellation: true,
    autoGainControl: true,
  },

  // Custom configuration
  model: 'gpt-4o-realtime-preview',
  temperature: 0.7,
  maxResponseDurationMs: 30000,
});

// Multi-modal capabilities
session.on('function_call', async (fn) => {
  // Voice agent can call functions just like text
  const result = await fn.execute();
  session.sendFunctionResult(fn.id, result);
});

^ Advanced audio processing handled by browser/client implementation


Voice Agents: Testing & Development

// Development mode with transcript
const debugSession = new RealtimeSession(agent, {
  debug: true,
  logTranscripts: true,
});

debugSession.on('transcript', (event) => {
  console.log(`[${event.speaker}]: ${event.text}`);
});

// Simulate voice input for testing
async function testVoiceAgent() {
  const testAudio = await textToSpeech('What is the weather?');

  session.sendAudio(testAudio);

  const response = await session.waitForResponse();
  assert(response.text.includes('weather'));
}

// Load testing voice agents
async function loadTest() {
  const sessions = await Promise.all(
    Array(100)
      .fill(0)
      .map(() => createSession()),
  );

  // Simulate concurrent conversations
}

^ Comprehensive testing strategies for voice agents


Voice Agents: Event Streaming

// Comprehensive event handling for voice agents
session.on('transcript', (event) => {
  // Real-time transcription
  console.log('User said:', event.text);
});

session.on('audio', (event) => {
  // Stream audio chunks to player
  audioPlayer.write(event.data);
});

session.on('tool_call', async (event) => {
  // Tools work same as text agents
  const result = await executeTool(event);
  session.sendToolResult(result);
});

session.on('handoff', (event) => {
  // Handoffs in voice context
  console.log('Transferring to:', event.agent);
});

session.on('interruption', (event) => {
  console.log('User interrupted at:', event.timestamp);
});

session.on('usage', (event) => {
  console.log('Audio seconds:', event.audioSeconds);
  console.log('Tokens used:', event.tokens);
});

^ Event-driven architecture for real-time voice interactions


Voice Agents: Voice/Text Synchronization

// Synchronize state between voice and text agents
const sharedContext = {
  conversation: [],
  userProfile: {},
  sessionData: {},
};

// Voice session with shared context
const voiceSession = new RealtimeSession(voiceAgent, {
  context: sharedContext,
});

// Text agent with same context
const textResult = await run(textAgent, input, {
  context: sharedContext,
});

// Updates from voice reflected in text and vice versa
voiceSession.on('message', (msg) => {
  sharedContext.conversation.push(msg);
});

// Seamless modality switching
async function switchToText() {
  const voiceState = voiceSession.getState();
  const textResult = await run(textAgent, 'continue', {
    messages: voiceState.messages,
    context: sharedContext,
  });
  return textResult;
}

^ Shared context enables seamless voice/text transitions


[.text: alignment(center)]

SDK


SDK Runtime Support

// Fully supported environments
 Node.js 22+     // Full SDK features, complete tracing
 Deno 2.35+      // Native TypeScript, all features
 Bun 1.2.5+      // Fast runtime, full support

// Experimental environments
⚠️ Cloudflare Workers {
  // Special requirements:
  - Enable 'nodejs_compat' flag
  - Manual trace flushing required:
    ctx.waitUntil(getGlobalTraceProvider().forceFlush())
}

⚠️ Browser {
  // Limitations:
  - No tracing capability
  - Limited MCP support
  - Use for Realtime agents only
}

^ SDK explicitly supports these runtimes with specific requirements


SDK State Persistence

import { RunState } from '@openai/agents';

// SDK provides state serialization
const result = await run(agent, input);

// Serialize for storage (SDK feature)
const serialized = JSON.stringify(result.state);
await yourStorage.save(sessionId, serialized);

// Later: Restore state (SDK feature)
const saved = await yourStorage.get(sessionId);
const state = await RunState.fromString(agent, saved);

// Continue conversation with restored state
const nextResult = await run(agent, newInput, {
  state, // SDK handles state restoration
  previousResponseId: result.lastResponseId, // For Responses API
});

^ SDK provides RunState serialization/restoration for any storage backend


SDK Error Types

import {
  MaxTurnsExceededError,
  GuardrailTripwireTriggered,
  ToolCallError,
} from '@openai/agents';

try {
  const result = await run(agent, input);
} catch (error) {
  // SDK-specific error types
  if (error instanceof MaxTurnsExceededError) {
    console.log('Loop detected:', error.maxTurns);
    // SDK provides maxTurns info
  }

  if (error instanceof GuardrailTripwireTriggered) {
    console.log('Guardrail:', error.guardrailName);
    console.log('Result:', error.guardrailResult);
    // SDK provides guardrail details
  }

  if (error instanceof ToolCallError) {
    console.log('Tool failed:', error.toolName);
    // SDK provides tool failure info
  }
}

^ SDK provides typed errors for handling agent-specific failures


SDK Usage Tracking

// SDK provides detailed usage metrics
const result = await run(agent, input);

// Built-in usage tracking
console.log(result.usage);
// {
//   inputTokens: 245,
//   outputTokens: 89,
//   totalTokens: 334
// }

// Track costs across runs
const runner = new Runner({
  modelSettings: {
    maxTokens: 1000, // SDK enforces limit
    temperature: 0.1,
  },
  maxTurns: 10, // SDK prevents infinite loops
});

// Usage accumulates in RunContext
runner.on('turn', (turn) => {
  console.log('Turn usage:', turn.usage);
  console.log('Total so far:', turn.context.usage);
});

^ SDK automatically tracks token usage for cost management


SDK Tracing & Monitoring

import {
  setTracingExportApiKey,
  createCustomSpan,
  addTraceProcessor,
} from '@openai/agents';

// SDK built-in tracing
setTracingExportApiKey(process.env.OPENAI_TRACING_KEY);

// Custom span tracking (SDK feature)
await createCustomSpan('database_query', async () => {
  return await db.query(sql);
});

// SDK debug controls
process.env.DEBUG = 'openai-agents:*'; // Enable debug logs
process.env.OPENAI_AGENTS_DONT_LOG_MODEL_DATA = '1'; // Privacy
process.env.OPENAI_AGENTS_DONT_LOG_TOOL_DATA = '1'; // Security

// Custom trace processor (SDK feature)
addTraceProcessor({
  onSpanStart(span) {
    /* metrics */
  },
  onSpanEnd(span) {
    /* analytics */
  },
});

^ SDK provides comprehensive tracing and debug controls


SDK Configuration by Environment

// SDK supports environment-based configuration
const agent = new Agent({
  model: process.env.NODE_ENV === 'production' ? 'gpt-4o' : 'gpt-4o-mini',

  // SDK's maxTurns prevents infinite loops
  maxTurns: process.env.NODE_ENV === 'production' ? 10 : 5,

  // Dynamic instructions based on environment
  instructions: (context) => {
    if (process.env.NODE_ENV === 'development') {
      return 'Debug mode: Be verbose in responses';
    }
    return productionInstructions;
  },
});

// SDK's Runner configuration
const runner = new Runner({
  tracingDisabled: process.env.NODE_ENV === 'development',
  groupId: process.env.DEPLOYMENT_ID, // For trace grouping
});

^ SDK allows environment-specific agent configuration


SDK Conversation Management

// SDK provides conversation continuity via Responses API
const result1 = await run(agent, 'Hello');

// SDK returns previousResponseId for stateful conversations
const result2 = await run(agent, 'Continue', {
  previousResponseId: result1.lastResponseId,
  // SDK manages conversation history server-side
});

// For interruption handling (SDK feature)
if (result.interruptions.length > 0) {
  // SDK provides interruption state
  const state = JSON.stringify(result.state);

  // Resume after approval (SDK feature)
  const restored = await RunState.fromString(agent, state);
  restored.approve(interruption);
  const final = await run(agent, restored);
}

^ SDK handles conversation state and interruption recovery


SDK Production Features Summary

// What the SDK provides for deployment:

 Runtime support (Node.js, Deno, Bun, Workers)
 State serialization (RunState.fromString)
 Error types (MaxTurnsExceededError, etc.)
 Usage tracking (result.usage)
 Tracing & monitoring (setTracingExportApiKey)
 Debug controls (environment variables)
 Conversation management (previousResponseId)
 Interruption handling (HITL state)
 Context passing (RunContext)
 Max turns protection (prevents loops)
 Dynamic configuration (per environment)

// You provide:
- Infrastructure (servers, databases)
- Storage backend (Redis, PostgreSQL, etc.)
- Security (API keys, rate limiting)
- Deployment (Docker, K8s, serverless)

^ SDK provides agent-specific features, you provide infrastructure


[.text: alignment(center)]

Dynamic Instructions


Dynamic Instructions

// Instructions can be static or dynamic
const agent = new Agent({
  name: 'ContextAware',

  // Static instructions
  instructions: 'You are a helpful assistant',

  // Or dynamic based on context
  instructions: (context) => {
    const time = new Date().getHours();
    const greeting = time < 12 ? 'morning' : 'afternoon';

    return `
      Good ${greeting}! You are helping ${context.user.name}.
      User tier: ${context.user.subscription}
      Available features: ${context.features.join(', ')}
      Current task: ${context.currentTask}
    `;
  },
});

^ Instructions can adapt based on runtime context


[.text: alignment(center)]

Guardrails


[fit] Guardrails: Build Your Own

[fit] The SDK provides the framework

[fit] You implement the detection logic


Your implementation options:

  • Regular expressions for patterns
  • External APIs (OpenAI Moderation, AWS Comprehend)
  • ML models (toxicity, sentiment)
  • Other agents as validators
  • Custom business rules

^ No pre-built guardrails. Complete flexibility to implement your own logic.


Guardrails: Input Protection

// Guardrails are custom functions YOU implement
const piiGuardrail: InputGuardrail = {
  name: 'PII_Check',
  execute: async ({ input, context, agent }) => {
    // You implement detectPII - could use regex, ML, or external API
    const hasPII =
      /\b\d{3}-\d{2}-\d{4}\b/.test(input) || // SSN
      /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/.test(input); // CC

    return {
      tripwireTriggered: hasPII, // Halt if PII detected
      outputInfo: { detected: hasPII ? ['SSN or Credit Card'] : [] },
    };
  },
};

// Or use another agent as a guardrail
const mathHomeworkGuardrail: InputGuardrail = {
  name: 'Math_Homework_Check',
  execute: async ({ input, context }) => {
    const checkAgent = new Agent({
      outputType: z.object({ isMathHomework: z.boolean() }),
    });
    const result = await run(checkAgent, input, { context });
    return {
      tripwireTriggered: result.finalOutput?.isMathHomework ?? false,
      outputInfo: result.finalOutput,
    };
  },
};

^ SDK provides the framework. You implement the logic - regex, ML models, agents, or APIs.


Guardrails: Output Protection

const toneGuardrail = defineOutputGuardrail({
  name: 'Professional_Tone',
  execute: async ({ output, context, agent }) => {
    const analysis = await analyzeTone(output);

    if (analysis.toxicity > 0.3 || !analysis.professional) {
      return {
        tripwireTriggered: true,
        outputInfo: {
          reason: 'Unprofessional tone detected',
          suggestions: analysis.improvements,
        },
      };
    }

    return { tripwireTriggered: false, outputInfo: analysis };
  },
});

^ Output guardrails validate agent responses. Can trigger re-generation.


Guardrails: Async Validation

const agent = new Agent({
  outputGuardrails: [
    {
      name: 'Compliance_Check',
      execute: async ({ output }) => {
        // Async call to compliance service
        const compliant = await complianceAPI.check(output);
        return {
          tripwireTriggered: !compliant.passed,
          outputInfo: compliant.report,
        };
      },
    },
  ],
});

// Guardrails run in parallel by default

^ Async guardrails enable external validation services


[.text: alignment(center)]

Understanding Tripwires

Tripwire = Emergency Stop


When tripwireTriggered: true:

  1. Execution halts immediately
  2. Specific exception thrown
  3. No agent processing occurs (input) or output discarded (output)
  4. You handle the exception
// Tripwire vs Warning Pattern
return {
  tripwireTriggered: true, // STOPS execution, throws exception
  outputInfo: { reason: 'PII detected' },
};

// Alternative: Warning pattern (not built-in, you'd implement)
return {
  tripwireTriggered: false, // Continues with warning
  outputInfo: { warning: 'Potential issue detected' },
};

^ Tripwires are for safety-critical checks. Use them when you MUST stop.


Tripwire Exception Handling

try {
  const result = await run(agent, input);
} catch (error) {
  if (error instanceof InputGuardrailTripwireTriggered) {
    console.log('Input blocked:', error.guardrailResult);
    // Options:
    // - Return error to user
    // - Sanitize input and retry
    // - Log for compliance
    // - Escalate to human
  }

  if (error instanceof OutputGuardrailTripwireTriggered) {
    console.log('Output blocked:', error.guardrailResult);
    // Options:
    // - Retry with different prompt
    // - Use fallback response
    // - Escalate to supervisor agent
    // - Return safe default message
  }
}

^ Specific exception types let you handle input vs output violations differently.


[fit] Human-in-the-Loop


HITL: The User Experience

What happens when approval is needed:

  1. Agent requests approval → Execution pauses
  2. State serialized → Stored in database/Redis
  3. Human notified → Email, Slack, dashboard alert
  4. Human reviews → Sees tool, parameters, context
  5. Human decides → Approve or reject
  6. Agent resumes → From exact pause point

Key insight: The agent doesn't "wait" - it saves state and stops. Resume can happen seconds, hours, or days later.

^ Asynchronous by design. No blocking threads or timeouts.


[fit] HITL: Notification & Review UI

// What the human sees in the approval interface
interface ApprovalRequest {
  // Context
  requestId: string;
  timestamp: Date;
  requestor: { name: string; email: string };
  agent: { name: string; purpose: string };

  // What needs approval
  tool: {
    name: 'delete_user';
    parameters: { userId: 'admin_123' };
    riskLevel: 'HIGH';
  };

  // Decision context
  conversation: Message[]; // Recent history
  reason: string; // Why agent wants to do this

  // Actions
  actions: ['Approve', 'Reject', 'Request More Info'];
}

^ Humans need full context to make informed decisions.


Human-in-the-Loop: Approval Flow

tool({
  name: 'delete_user',
  needsApproval: async (context, input, callId) => {
    // Return true to require approval
    return (
      input.userId.startsWith('admin_') || context.sensitiveOperation === true
    );
  },
  execute: async (input) => {
    // Only runs after approval
    return await database.deleteUser(input.userId);
  },
});

^ needsApproval is evaluated at runtime for each call


HITL: Interruption & Resume

// First run - hits approval requirement
let result = await run(agent, 'Delete admin_user_123');

if (result.interruptions?.length > 0) {
  // Serialize state for persistence
  const serialized = JSON.stringify(result.state);
  await redis.set(`approval:${id}`, serialized);

  // Send to approval queue
  await queue.send({
    id,
    interruptions: result.interruptions,
    requestor: context.user,
  });
}

^ State can be persisted while waiting for approval


HITL: State Serialization

// Later, after approval received
const serialized = await redis.get(`approval:${id}`);
const state = await RunState.fromString(agent, serialized);

// Process approvals
for (const interruption of approvals) {
  if (interruption.approved) {
    state.approve(interruption);
  } else {
    state.reject(interruption);
  }
}

// Resume execution from exact point
const finalResult = await run(agent, state);

^ Complete state preservation enables async approval workflows


HITL: Real-World Implementation

Building the approval system:

[.column] Backend

  • Queue (SQS, RabbitMQ)
  • State store (Redis, DynamoDB)
  • Notification service
  • WebSocket server

[.column] Frontend

  • Admin dashboard
  • Mobile app
  • Slack bot
  • Email with action links

HITL: WebSocket Integration

// Real-time approval flow with WebSocket
ws.on('approval_request', async (data) => {
  const { state, interruptions } = data;

  // Send to UI with full context
  ws.emit('show_approval_ui', {
    tool: interruptions[0].rawItem.name,
    params: interruptions[0].rawItem.arguments,
    risk: calculateRisk(interruptions[0]),
    timeout: '24 hours', // Optional auto-reject
  });
});

ws.on('approval_response', async (response) => {
  const state = await RunState.fromString(agent, response.state);

  if (response.approved) {
    state.approve(response.interruption);
    await audit.log('approval.granted', response);
  } else {
    state.reject(response.interruption);
    await audit.log('approval.denied', response);
  }

  const result = await run(agent, state);
  ws.emit('execution_complete', result);
});

^ WebSockets for immediate response. Audit logging for compliance.


[.text: alignment(center)]

Handoffs & Multi-Agent


Handoff Patterns

Control Transfer

const triageAgent = new Agent({
  name: 'Triage',
  handoffs: [billingAgent, technicalAgent],
  instructions: 'Route to appropriate specialist',
});

^ Agents can transfer control completely to specialists


Context Flow Through Handoffs

const handoff = new Handoff({
  agent: specialistAgent,
  inputFilter: (data) => ({
    ...data,
    // Local context ALWAYS flows through unchanged
    context: data.context, // Database, cache, user, etc.

    // Agent context can be filtered/modified
    inputHistory: data.inputHistory
      .filter(
        (item) =>
          // Remove sensitive conversation history
          !item.content?.includes('password'),
      )
      .slice(-5), // Limit context window

    // Inject new conversation context
    newItems: [
      { role: 'system', content: `Escalated from ${data.agent.name}` },
      { role: 'system', content: `User tier: ${data.context.user.tier}` },
    ],
  }),
});

^ Key insight: Local context flows untouched. Agent context is carefully managed.


Handoffs with Responses API

Server Context Thread Continuity

// With Responses API, handoffs maintain THREE contexts:

const runner = new Runner(agent1, {
  context: { user: 'alice', db: pgClient }, // 1. Local context
  previousResponseId: 'resp_123...', // 2. Server context reference
});

// During handoff to agent2:
// 1. Local Context: Flows through unchanged
// 2. Server Context: Thread continues via previousResponseId
// 3. Input Filter: Can still modify what agent2 "sees"

const handoff = new Handoff({
  agent: agent2,
  inputFilter: (data) => {
    // data.runContext preserves local state
    // previousResponseId maintains server conversation
    // But we can filter what's visible to agent2:
    return {
      ...data,
      inputHistory: [], // Hide previous conversation
      newItems: [{ role: 'system', content: 'Fresh start for specialist' }],
    };
  },
});

// Result: agent2 has local context, server thread, but filtered view

^ Responses API maintains conversation thread even when filtering handoff inputs


Handoff Filtering Strategies

// Strategy 1: Summarization
const summarizeHandoff = new Handoff({
  agent: managerAgent,
  inputFilter: async (data) => {
    const summary = await summarizeConversation(data.inputHistory);
    return {
      ...data,
      inputHistory: [],
      newItems: [
        {
          role: 'system',
          content: `Previous conversation summary: ${summary}`,
        },
      ],
    };
  },
});

// Strategy 2: Selective history
const selectiveHandoff = new Handoff({
  agent: specialistAgent,
  inputFilter: (data) => ({
    ...data,
    inputHistory: data.inputHistory.filter(
      (item) => item.role === 'tool' || item.content?.includes('ERROR'),
    ),
  }),
});

^ Different filtering strategies for different handoff scenarios


Agent as Tool

Agents can be tools for other agents


Agents can be tools for other agents

const translationAgent = new Agent({
  name: 'translator',
  instructions: 'Translate text to the requested language',
});

const orchestratorAgent = new Agent({
  name: 'orchestrator',
  instructions: 'Coordinate translation tasks',
  tools: [
    // Convert agent to tool with asTool()
    translationAgent.asTool({
      toolName: 'translate_text',
      toolDescription: 'Translate text to any language',
    }),
  ],
});

// Orchestrator calls translation agent like any other tool
// Input → Tool call → Agent runs → Returns result → Continue

^ Agent-as-tool: Stateless, isolated execution. Returns to caller.


Agent as Tool: Use Cases

When to use agent-as-tool pattern:

Specialized capabilities - Complex agent for specific tasks ✅ Reusable components - Same agent used by multiple orchestrators ✅ Isolated execution - Keep concerns separated ✅ Parallel processing - Call multiple agent-tools concurrently


Handoff vs Agent-as-Tool

[.column] Handoff

  • Full conversation history
  • Control transfer
  • Stateful continuation
  • Context preservation
  • Agent takes over

[.column] Agent Tool

  • Generated input only
  • Returns to caller
  • Stateless invocation
  • Isolated execution
  • Original agent continues

Handoff Return Pattern

Manual bidirectional configuration required

// Configure agents with mutual handoffs
const triageAgent = new Agent({
  name: 'Triage',
  instructions: 'Route to appropriate specialist based on request type',
  handoffs: [billingAgent, technicalAgent],
});

const billingAgent = new Agent({
  name: 'Billing',
  instructions: `
    Handle billing inquiries.
    If customer asks non-billing questions, transfer back to triage.
  `,
  handoffs: [triageAgent], // ← Explicit return path
});

const technicalAgent = new Agent({
  name: 'Technical',
  instructions: `
    Resolve technical issues.
    If unable to resolve or out of scope, transfer back to triage.
  `,
  handoffs: [triageAgent], // ← Explicit return path
});

// Flow: Triage → Specialist → Triage (if needed)

^ No automatic returns. Each agent must explicitly configure handoff paths.


Multi-Agent Coordination Patterns

Manager/Orchestrator Pattern

const orchestrator = new Agent({
  name: 'Orchestrator',
  instructions: `
    You coordinate multiple specialists:
    1. Analyze request complexity
    2. Delegate to appropriate agents
    3. Synthesize results
    4. Ensure quality
  `,
  handoffs: [
    dataAgent, // For data analysis
    reportAgent, // For report generation
    reviewAgent, // For quality review
  ],
});

^ Orchestrator pattern for complex multi-step workflows


Multi-Agent: Parallel Execution

const parallelAnalysis = async (data: Data) => {
  // Run multiple agents in parallel
  const [sentiment, summary, keywords, entities] = await Promise.all([
    run(sentimentAgent, data),
    run(summaryAgent, data),
    run(keywordAgent, data),
    run(entityAgent, data),
  ]);

  // Aggregate results with consensus agent
  return await run(consensusAgent, {
    sentiment: sentiment.finalOutput,
    summary: summary.finalOutput,
    keywords: keywords.finalOutput,
    entities: entities.finalOutput,
  });
};

^ Parallel patterns for independent analysis tasks


[.text: alignment(center)]

Lifecycle & Control


Lifecycle Hooks

class CustomAgent extends Agent {
  async onAgentStart(context: RunContext) {
    // Initialize resources
    await context.context.cache.warm();
  }

  async onTurnComplete(result: TurnResult, context: RunContext) {
    // After each LLM response
    if (result.toolsUsed.includes('sensitive_tool')) {
      await context.context.audit.log(result);
    }
  }

  async onToolResult(tool: string, result: any, context: RunContext) {
    // After each tool execution
    context.context.metrics.record(tool, result);
  }

  async onAgentEnd(finalOutput: any, context: RunContext) {
    // Cleanup resources
    await context.context.cache.flush();
  }
}

^ Lifecycle hooks enable custom behavior at key points


Forcing Tool Use

const agent = new Agent({
  modelSettings: {
    // Force specific tool on first turn
    toolChoice: {
      type: 'function',
      name: 'analyze_request',
    },
  },
});

// Dynamic tool forcing
class AdaptiveAgent extends Agent {
  async onTurnComplete(result: TurnResult) {
    if (!result.toolsUsed.includes('validate')) {
      // Force validation on next turn
      this.modelSettings.toolChoice = {
        type: 'function',
        name: 'validate',
      };
    }
  }
}

^ Tool choice can be controlled programmatically


[.text: alignment(center)]

Tracing & Monitoring


Tracing Architecture

// OpenTelemetry-based tracing
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('agents-sdk');

await tracer.startActiveSpan('agent_run', async (span) => {
  span.setAttributes({
    'agent.name': agent.name,
    'agent.model': agent.model,
    'input.length': input.length,
  });

  const result = await run(agent, input);

  span.setAttributes({
    'output.length': result.finalOutput.length,
    'tools.used': result.toolsUsed.length,
    'handoffs.count': result.handoffs.length,
  });

  span.end();
  return result;
});

^ Full OpenTelemetry support for production observability


Custom Trace Processors

class CustomTraceProcessor implements SpanProcessor {
  onStart(span: Span): void {
    // Track span start
    metrics.increment('spans.started');
  }

  onEnd(span: ReadableSpan): void {
    // Custom processing
    if (span.name.includes('tool_call')) {
      const duration = span.duration;
      metrics.histogram('tool.duration', duration);

      if (duration > 5000) {
        alerts.send({
          type: 'SLOW_TOOL',
          tool: span.attributes['tool.name'],
          duration,
        });
      }
    }
  }
}

// Register processor
tracerProvider.addSpanProcessor(new CustomTraceProcessor());

^ Custom processors for metrics, alerts, and analysis


Performance Monitoring

// Built-in performance tracking
const result = await run(agent, input);

console.log(result.usage);
// {
//   inputTokens: 1234,
//   outputTokens: 567,
//   totalTokens: 1801,
//   inputCachedTokens: 200,
//   inputAudioTokens: 0,
//   outputAudioTokens: 0
// }

console.log(result.timing);
// {
//   start: 1234567890,
//   firstToken: 1234567891,
//   end: 1234567895,
//   duration: 5000,
//   ttfb: 1000  // Time to first byte
// }

^ Comprehensive usage and timing metrics built-in


Debug Strategies

// Verbose logging
import { setLogLevel } from '@openai/agents';
setLogLevel('debug');

// Step-through debugging
const runner = new Runner({
  pauseOnTool: true,
  pauseOnHandoff: true,
});

runner.on('tool_call', async (event) => {
  console.log('Tool called:', event);
  // Debugger breakpoint here
  debugger;
});

// Trace visualization
const result = await run(agent, input);
console.log(result.trace.visualize());
// Outputs ASCII tree of execution flow

^ Rich debugging capabilities for development


[.text: alignment(center)]

Best Practices


Best Practices

[.build-lists: true]

  • Small, focused agents over monolithic ones
  • Explicit handoffs over implicit routing
  • Typed context over string passing
  • Structured outputs over free text
  • Deterministic flows where possible
  • Guardrails on boundaries
  • Lifecycle hooks for custom behavior

^ Learned from production deployments


Anti-Patterns to Avoid

Circular handoffs without termination ❌ Shared mutable state outside context ❌ Synchronous long-running toolsUnconstrained recursive depthMissing error boundariesIgnoring tripwire guardrails

^ Common pitfalls in multi-agent systems


Production Checklist

  • Guardrails configured
  • Error boundaries in place
  • Tracing enabled
  • State persistence ready
  • Circuit breakers configured
  • Monitoring dashboards
  • Runbook documented
  • Load tested
  • Approval flows tested

^ Essential for production deployments


Advanced: Custom Providers

class CustomProvider implements Model {
  async generate(request: ModelRequest): Promise<ModelResponse> {
    const response = await this.customAPI.complete({
      messages: request.messages,
      tools: request.tools,
      // Custom transformation
    });

    return {
      output: transformResponse(response),
      usage: extractUsage(response),
    };
  }

  async streamGenerate(request: ModelRequest): AsyncIterable<ModelChunk> {
    // Streaming implementation
  }
}

^ Bring any model through provider interface


Advanced: Workflow DSL

const workflow = defineWorkflow({
  nodes: {
    start: triageAgent,
    billing: billingAgent,
    technical: technicalAgent,
    resolve: resolutionAgent,
  },
  edges: {
    start: ['billing', 'technical'],
    billing: ['resolve'],
    technical: ['resolve'],
  },
  guards: {
    'start->billing': (ctx) => ctx.issue.type === 'billing',
    'start->technical': (ctx) => ctx.issue.type === 'technical',
  },
});

const executor = new WorkflowExecutor(workflow);
const result = await executor.run(input);

^ Higher-level abstractions possible on core primitives


Future Roadmap

[.build-lists: true]

  • Long-running functions: Suspend/resume for async operations
  • Voice pipeline: Enhanced speech capabilities
  • Distributed execution: Cross-process agent coordination
  • Model routing: Dynamic model selection
  • Adaptive flows: ML-driven handoff decisions
  • Visual agents: Computer use and UI automation

^ SDK continues to evolve with community needs


Key Takeaways

[.build-lists: true]

  1. Type-safe orchestration from ground up
  2. Composable agents with clear boundaries
  3. Production-ready patterns built-in
  4. Provider-agnostic architecture
  5. Extensible through clean abstractions
  6. Voice-enabled for modern interfaces

^ Not just another wrapper - thoughtful orchestration framework


Your Next Steps

// Start here
const agent = new Agent({
  name: 'MyFirstAgent',
  instructions: 'You are a helpful assistant',
});

// Add capability
agent.tools = [myTool];

// Add intelligence routing
agent.handoffs = [specialist];

// Add safety
agent.inputGuardrails = [piiCheck];
agent.outputGuardrails = [toneCheck];

// Ship it
await run(agent, userInput);

^ Begin simple. Complexity emerges from composition.


Questions?

Let's Build Intelligence Together

^ Thank you for your attention. Ready for deep technical discussion.

autoscale: true

[fit] Practical
Algebraic Thinking

[fit] Making the Type System Work for You


[fit] Previously...

We explored the mathematical foundations

Today: How to use these ideas in production code


[fit] Two Core Principles

  1. Make types more precise
  2. Make impossible states impossible

Result: Fewer bugs, fewer tests, more confidence


[fit] Part 1:
Precise Types


[fit] Branded Types

The Problem

// ❌ Everything is just a string
function transferMoney(
  fromAccountId: string,
  toAccountId: string,
  amount: string
) {
  // Easy to mix up parameters
}

// Oops! Arguments in wrong order
transferMoney("100.00", "acc123", "acc456");

[fit] The Solution

// ✅ Branded types make misuse impossible
type AccountId = string & { readonly __brand: "AccountId" };
type Money = string & { readonly __brand: "Money" };

function AccountId(value: string): AccountId {
  if (!/^acc\d+$/.test(value)) {
    throw new Error(`Invalid account ID: ${value}`);
  }
  return value as AccountId;
}

function Money(value: string): Money {
  if (!/^\d+\.\d{2}$/.test(value)) {
    throw new Error(`Invalid money format: ${value}`);
  }
  return value as Money;
}

[fit] Type Safety in Action

function transferMoney(
  fromAccountId: AccountId,
  toAccountId: AccountId,
  amount: Money
) {
  // Implementation
}

// ✅ This works
transferMoney(
  AccountId("acc123"),
  AccountId("acc456"),
  Money("100.00")
);

// ❌ This won't compile!
transferMoney("100.00", "acc123", "acc456");
//            ^^^^^^^^  Type 'string' is not assignable to type 'Money'

[fit] Tests We Don't
Need to Write

// ❌ Without branded types, we need these tests:
describe('transferMoney', () => {
  it('should validate fromAccountId format', () => {
    expect(() => 
      transferMoney('invalid', 'acc456', '100.00')
    ).toThrow();
  });
  
  it('should validate toAccountId format', () => {
    expect(() => 
      transferMoney('acc123', 'invalid', '100.00')
    ).toThrow();
  });
  
  it('should validate amount format', () => {
    expect(() => 
      transferMoney('acc123', 'acc456', 'not-money')
    ).toThrow();
  });
  
  it('should not accept parameters in wrong order', () => {
    // How do you even test this?
  });
});

[fit] With Branded Types

// ✅ These tests are now impossible to write incorrectly!
describe('transferMoney', () => {
  it('should transfer money', () => {
    // The only test we need - the business logic
    const result = transferMoney(
      AccountId("acc123"),
      AccountId("acc456"),
      Money("100.00")
    );
    expect(result).toEqual({ success: true });
  });
});

// Validation is tested once, at the boundary
describe('AccountId', () => {
  it('should accept valid account IDs', () => {
    expect(() => AccountId("acc123")).not.toThrow();
  });
  
  it('should reject invalid account IDs', () => {
    expect(() => AccountId("invalid")).toThrow();
  });
});

[fit] Real-World Example:
Email Validation

Before: Validation Everywhere

// ❌ Defensive programming nightmare
class UserService {
  async createUser(email: string, name: string) {
    if (!this.isValidEmail(email)) {
      throw new Error("Invalid email");
    }
    // ... create user
  }
  
  async sendWelcomeEmail(email: string) {
    if (!this.isValidEmail(email)) {
      throw new Error("Invalid email");
    }
    // ... send email
  }
  
  async updateEmail(userId: string, newEmail: string) {
    if (!this.isValidEmail(newEmail)) {
      throw new Error("Invalid email");
    }
    // ... update
  }
  
  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

[fit] After: Parse Don't Validate

// ✅ Validation happens once at the boundary
type Email = string & { readonly __brand: "Email" };

function parseEmail(input: string): Email | null {
  if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
    return input as Email;
  }
  return null;
}

class UserService {
  // No validation needed - type system guarantees validity
  async createUser(email: Email, name: string) {
    // Just use it!
  }
  
  async sendWelcomeEmail(email: Email) {
    // Just use it!
  }
  
  async updateEmail(userId: string, newEmail: Email) {
    // Just use it!
  }
}

[fit] Refined Number Types

The Problem

// ❌ Runtime errors waiting to happen
function calculateDiscount(
  price: number,
  discountPercent: number
): number {
  if (price < 0) throw new Error("Price cannot be negative");
  if (discountPercent < 0 || discountPercent > 100) {
    throw new Error("Discount must be between 0 and 100");
  }
  return price * (1 - discountPercent / 100);
}

// These compile but fail at runtime
calculateDiscount(-50, 20);      // Negative price
calculateDiscount(100, 150);     // Invalid discount
calculateDiscount(100, -10);     // Negative discount

[fit] The Solution

// ✅ Make invalid states unrepresentable
type PositiveNumber = number & { readonly __brand: "PositiveNumber" };
type Percentage = number & { readonly __brand: "Percentage" };

function PositiveNumber(n: number): PositiveNumber {
  if (n < 0) throw new Error(`${n} is not positive`);
  return n as PositiveNumber;
}

function Percentage(n: number): Percentage {
  if (n < 0 || n > 100) throw new Error(`${n} is not a valid percentage`);
  return n as Percentage;
}

function calculateDiscount(
  price: PositiveNumber,
  discountPercent: Percentage
): PositiveNumber {
  const discounted = price * (1 - discountPercent / 100);
  return discounted as PositiveNumber; // Safe: math guarantees positive
}

[fit] Part 2:
Parse, Don't Validate


[fit] The Core Insight

Validation = Checking but keeping imprecise types

Parsing = Transforming into precise types


[fit] Replace Option with
Precise Types

The Problem

// ❌ Using Option/null for business logic
interface BadUser {
  id: string;
  email: string;
  subscription?: {
    plan: string;
    expiresAt: Date;
  };
}

function canAccessPremiumFeature(user: BadUser): boolean {
  // Defensive checks everywhere
  if (!user.subscription) return false;
  if (user.subscription.expiresAt < new Date()) return false;
  return user.subscription.plan === "premium";
}

function getPlanName(user: BadUser): string {
  // More defensive programming
  return user.subscription?.plan ?? "free";
}

[fit] The Solution

// ✅ Parse into precise types at the boundary
type FreeUser = {
  type: "free";
  id: UserId;
  email: Email;
};

type PaidUser = {
  type: "paid";
  id: UserId;
  email: Email;
  plan: "basic" | "premium";
  expiresAt: FutureDate; // Parsed to ensure it's in the future
};

type User = FreeUser | PaidUser;

// No defensive checks needed!
function canAccessPremiumFeature(user: User): boolean {
  return user.type === "paid" && user.plan === "premium";
}

function getPlanName(user: User): string {
  return user.type === "paid" ? user.plan : "free";
}

[fit] Replace Arrays with
Precise Types

The Problem

// ❌ Arrays that might be empty
interface BadTeam {
  name: string;
  members: string[];
}

function getTeamLead(team: BadTeam): string {
  if (team.members.length === 0) {
    throw new Error("Team has no members");
  }
  return team.members[0]; // Still might be undefined!
}

function assignTask(team: BadTeam, task: Task) {
  if (team.members.length === 0) {
    throw new Error("Cannot assign task to empty team");
  }
  // Defensive check repeated everywhere
}

[fit] The Solution

// ✅ Parse into NonEmptyArray at creation
type NonEmptyArray<T> = readonly [T, ...T[]];

type Team = {
  name: string;
  members: NonEmptyArray<TeamMember>;
};

// Parse at the boundary
function createTeam(
  name: string, 
  members: TeamMember[]
): Team | null {
  if (members.length === 0) return null;
  return {
    name,
    members: members as NonEmptyArray<TeamMember>
  };
}

// No defensive checks needed!
function getTeamLead(team: Team): TeamMember {
  return team.members[0]; // Always exists!
}

function assignTask(team: Team, task: Task) {
  // Just assign it - team always has members
}

[fit] Replace Booleans with
Precise Types

The Problem

// ❌ Boolean flags lose information
interface BadApiResponse {
  success: boolean;
  data?: any;
  error?: string;
  statusCode?: number;
}

function handleResponse(response: BadApiResponse) {
  if (response.success) {
    if (!response.data) {
      // Success but no data?
      console.error("Invalid state");
    }
    processData(response.data);
  } else {
    if (!response.error) {
      // Failure but no error message?
      console.error("Unknown error");
    }
    logError(response.error);
  }
}

[fit] The Solution

// ✅ Parse into specific result types
type ApiResponse<T> =
  | { type: "success"; data: T; statusCode: 200 | 201 | 204 }
  | { type: "client_error"; error: string; statusCode: 400 | 401 | 403 | 404 }
  | { type: "server_error"; error: string; statusCode: 500 | 502 | 503; retryAfter?: number };

// Parse at the HTTP boundary
async function parseApiResponse<T>(
  response: Response,
  schema: z.ZodSchema<T>
): Promise<ApiResponse<T>> {
  if (response.ok) {
    const data = schema.parse(await response.json());
    return { 
      type: "success", 
      data, 
      statusCode: response.status as 200 | 201 | 204 
    };
  }
  
  const error = await response.text();
  if (response.status < 500) {
    return { 
      type: "client_error", 
      error, 
      statusCode: response.status as 400 | 401 | 403 | 404 
    };
  }
  
  return { 
    type: "server_error", 
    error, 
    statusCode: response.status as 500 | 502 | 503,
    retryAfter: response.headers.get("Retry-After") 
      ? parseInt(response.headers.get("Retry-After")!) 
      : undefined
  };
}

// Clean handling without defensive checks
function handleResponse<T>(response: ApiResponse<T>) {
  switch (response.type) {
    case "success":
      processData(response.data); // Data always exists
      break;
    case "client_error":
      showUserError(response.error); // Error always exists
      break;
    case "server_error":
      logError(response.error); // Error always exists
      if (response.retryAfter) {
        scheduleRetry(response.retryAfter);
      }
      break;
  }
}

[fit] Validation Anti-Pattern:
Repeated Checks

// ❌ Same validation logic everywhere
class OrderService {
  validateOrder(items: Item[], coupon?: string): boolean {
    if (items.length === 0) return false;
    if (items.some(item => item.quantity <= 0)) return false;
    if (items.some(item => item.price < 0)) return false;
    if (coupon && !this.isValidCoupon(coupon)) return false;
    return true;
  }
  
  calculateTotal(items: Item[], coupon?: string): number {
    // Repeat all the checks...
    if (items.length === 0) throw new Error("No items");
    if (items.some(item => item.quantity <= 0)) {
      throw new Error("Invalid quantity");
    }
    // ... calculate
  }
  
  submitOrder(items: Item[], coupon?: string) {
    // And again...
    if (!this.validateOrder(items, coupon)) {
      throw new Error("Invalid order");
    }
    // But we still don't know WHY it's invalid!
  }
}

[fit] Parse Once, Use Everywhere

// ✅ Parse into a valid order type
type ValidItem = {
  product: ProductId;
  quantity: PositiveInteger;
  price: PositiveMoney;
};

type ValidOrder = {
  items: NonEmptyArray<ValidItem>;
  coupon?: ValidCoupon;
};

// Parse at the boundary
function parseOrder(
  items: unknown[],
  coupon?: string
): ValidOrder | { error: string } {
  if (items.length === 0) {
    return { error: "Order must have at least one item" };
  }
  
  const validItems: ValidItem[] = [];
  for (const item of items) {
    const quantity = parsePositiveInteger(item.quantity);
    if (!quantity) return { error: `Invalid quantity: ${item.quantity}` };
    
    const price = parsePositiveMoney(item.price);
    if (!price) return { error: `Invalid price: ${item.price}` };
    
    validItems.push({ product: item.product, quantity, price });
  }
  
  if (coupon) {
    const validCoupon = parseCoupon(coupon);
    if (!validCoupon) return { error: `Invalid coupon: ${coupon}` };
    return { 
      items: validItems as NonEmptyArray<ValidItem>, 
      coupon: validCoupon 
    };
  }
  
  return { items: validItems as NonEmptyArray<ValidItem> };
}

// Now all functions just work with ValidOrder
class OrderService {
  calculateTotal(order: ValidOrder): PositiveMoney {
    // No validation needed - just calculate!
    const subtotal = order.items.reduce(
      (sum, item) => sum + (item.price * item.quantity), 
      0
    );
    return order.coupon 
      ? applyCoupon(subtotal, order.coupon)
      : subtotal as PositiveMoney;
  }
  
  submitOrder(order: ValidOrder): OrderId {
    // No validation needed - just submit!
    return db.createOrder(order);
  }
}

[fit] Part 3:
Impossible States

[fit] Form State Machine

The Problem

// ❌ Too many boolean flags = exponential complexity
interface BadFormState {
  isLoading: boolean;
  isSubmitting: boolean;
  hasError: boolean;
  errorMessage?: string;
  isSuccess: boolean;
  successMessage?: string;
  data?: FormData;
}

// Which combinations are valid?
// isLoading && isSubmitting?
// hasError && isSuccess?
// hasError but no errorMessage?
// isSuccess but no data?

[fit] State Explosion

// ❌ Defensive code everywhere
function FormComponent({ state }: { state: BadFormState }) {
  if (state.isLoading && state.isSubmitting) {
    // Is this possible? What do we show?
    return <div>Loading... Submitting...?</div>;
  }
  
  if (state.hasError && !state.errorMessage) {
    // Defensive programming
    return <div>Unknown error</div>;
  }
  
  if (state.isSuccess && !state.data) {
    // Another edge case
    return <div>Success but no data?</div>;
  }
  
  if (state.hasError && state.isSuccess) {
    // Which one wins?
    return <div>¯\_()_/¯</div>;
  }
  
  // ... more defensive checks
}

[fit] The Solution:
Algebraic Data Types

// ✅ Each state has exactly the data it needs
type FormState =
  | { type: "idle" }
  | { type: "loading" }
  | { type: "submitting"; data: FormData }
  | { type: "error"; message: string; canRetry: boolean }
  | { type: "success"; data: FormData; response: Response };

// Impossible states are now impossible!
// Can't be loading AND submitting
// Can't have error WITHOUT message
// Can't be success WITHOUT data

[fit] Clean State Handling

// ✅ Exhaustive, no defensive programming needed
function FormComponent({ state }: { state: FormState }) {
  switch (state.type) {
    case "idle":
      return <Form onSubmit={handleSubmit} />;
      
    case "loading":
      return <Spinner />;
      
    case "submitting":
      return <ProgressBar data={state.data} />;
      
    case "error":
      return (
        <ErrorMessage 
          message={state.message}
          onRetry={state.canRetry ? handleRetry : undefined}
        />
      );
      
    case "success":
      return <SuccessMessage response={state.response} />;
  }
}

[fit] Tests We Eliminated

// ❌ Without ADTs, we need to test invalid combinations
describe('FormComponent with boolean flags', () => {
  it('handles loading and submitting at same time', () => {});
  it('handles error state without message', () => {});
  it('handles success state without data', () => {});
  it('handles error and success at same time', () => {});
  it('handles all false flags', () => {});
  it('handles all true flags', () => {});
  // ... 2^6 = 64 possible combinations to test!
});

// ✅ With ADTs, only valid states exist
describe('FormComponent with ADT', () => {
  it('renders form in idle state', () => {});
  it('shows spinner when loading', () => {});
  it('shows progress when submitting', () => {});
  it('shows error with retry button', () => {});
  it('shows success with response', () => {});
  // Just 5 tests - one per valid state!
});

[fit] Real-World Example:
API Request State

Before: Boolean Soup

// ❌ Classic loading/error/data pattern
interface BadApiState<T> {
  loading: boolean;
  error: Error | null;
  data: T | null;
  lastFetch?: Date;
  isStale?: boolean;
  isRefetching?: boolean;
}

function useApiData<T>(url: string): BadApiState<T> {
  const [state, setState] = useState<BadApiState<T>>({
    loading: true,
    error: null,
    data: null,
  });
  
  // Complex state updates
  useEffect(() => {
    setState(prev => ({ ...prev, loading: true, error: null }));
    
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setState({
          loading: false,
          error: null,
          data,
          lastFetch: new Date(),
          isStale: false,
        });
      })
      .catch(error => {
        setState(prev => ({
          ...prev,
          loading: false,
          error,
          // Keep old data on error? 🤷
        }));
      });
  }, [url]);
  
  return state;
}

[fit] After: Tagged Union

// ✅ Every state is explicit and complete
type ApiState<T> =
  | { type: "idle" }
  | { type: "loading"; previousData?: T }
  | { type: "success"; data: T; fetchedAt: Date }
  | { type: "error"; error: Error; previousData?: T; canRetry: boolean };

function useApiData<T>(url: string): ApiState<T> {
  const [state, setState] = useState<ApiState<T>>({ type: "idle" });
  
  useEffect(() => {
    // Clear state transitions
    setState(prev => 
      prev.type === "success" 
        ? { type: "loading", previousData: prev.data }
        : { type: "loading" }
    );
    
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setState({ 
          type: "success", 
          data, 
          fetchedAt: new Date() 
        });
      })
      .catch(error => {
        setState(prev => ({
          type: "error",
          error,
          previousData: prev.type === "loading" ? prev.previousData : undefined,
          canRetry: !error.message.includes("401"),
        }));
      });
  }, [url]);
  
  return state;
}

[fit] Parse + State Machines

Parsing Events Before Transitions

// ❌ Validation approach - stringly typed events
type BadEvent = {
  type: string;
  payload?: any;
};

function handleEvent(state: State, event: BadEvent): State {
  if (event.type === "submit") {
    if (!event.payload?.items || event.payload.items.length === 0) {
      throw new Error("Invalid submit event");
    }
    // Still don't know if items are valid...
  }
  // More string checks...
}

[fit] Parse Events into Types

// ✅ Parse events into precise types
type CartEvent =
  | { type: "ADD_ITEM"; item: ValidItem }
  | { type: "REMOVE_ITEM"; itemId: ItemId }
  | { type: "APPLY_COUPON"; coupon: ValidCoupon }
  | { type: "SUBMIT"; shippingAddress: ValidAddress };

// Parse at the event boundary
function parseCartEvent(raw: unknown): CartEvent | { error: string } {
  const { type, ...payload } = raw as any;
  
  switch (type) {
    case "ADD_ITEM": {
      const item = parseValidItem(payload.item);
      if (!item) return { error: "Invalid item data" };
      return { type: "ADD_ITEM", item };
    }
    case "SUBMIT": {
      const address = parseAddress(payload.address);
      if (!address) return { error: "Invalid shipping address" };
      return { type: "SUBMIT", shippingAddress: address };
    }
    // ... other cases
  }
}

// State machine with parsed events
function cartReducer(state: CartState, event: CartEvent): CartState {
  switch (state.type) {
    case "shopping":
      switch (event.type) {
        case "ADD_ITEM":
          // event.item is guaranteed to be valid!
          return {
            ...state,
            items: [...state.items, event.item]
          };
        case "SUBMIT":
          // Can only submit non-empty cart with valid address
          if (state.items.length === 0) return state;
          return {
            type: "checking_out",
            items: state.items as NonEmptyArray<ValidItem>,
            shippingAddress: event.shippingAddress
          };
      }
  }
}

[fit] Part 4:
LLM Agent Types

[fit] Agent Tool Calls

The Problem

// ❌ Stringly-typed tool calls
interface BadToolCall {
  tool: string;
  parameters: Record<string, any>;
}

async function executeToolCall(call: BadToolCall) {
  switch (call.tool) {
    case "search":
      // Hope parameters.query exists and is a string!
      return await search(call.parameters.query);
      
    case "calculate":
      // Hope these exist and are numbers!
      return calculate(
        call.parameters.operation,
        call.parameters.a,
        call.parameters.b
      );
      
    case "send_email":
      // So many ways this can fail
      return await sendEmail(
        call.parameters.to,
        call.parameters.subject,
        call.parameters.body
      );
  }
}

[fit] Type-Safe Tool Calls

// ✅ Each tool has its own parameter type
type ToolCall =
  | { tool: "search"; parameters: { query: string; limit?: number } }
  | { tool: "calculate"; parameters: { 
      operation: "add" | "subtract" | "multiply" | "divide";
      a: number;
      b: number;
    }}
  | { tool: "send_email"; parameters: {
      to: Email;  // Using our branded type!
      subject: string;
      body: string;
      cc?: Email[];
    }};

async function executeToolCall(call: ToolCall) {
  switch (call.tool) {
    case "search":
      // TypeScript knows parameters.query exists
      return await search(call.parameters.query, call.parameters.limit);
      
    case "calculate":
      // All parameters are guaranteed to exist with correct types
      return calculate(
        call.parameters.operation,
        call.parameters.a,
        call.parameters.b
      );
      
    case "send_email":
      // Email type ensures valid email addresses
      return await sendEmail(call.parameters);
  }
}

[fit] Agent State Machine

The Problem

// ❌ Agent with unclear state transitions
interface BadAgent {
  isThinking: boolean;
  isExecutingTool: boolean;
  currentTool?: string;
  hasResponse: boolean;
  response?: string;
  error?: Error;
  toolResults?: any[];
  needsMoreInfo: boolean;
}

// What's the flow? Can it be thinking AND executing?
// When do toolResults get cleared?
// What if there's an error during tool execution?

[fit] Agent State Machine

// ✅ Clear state transitions
type AgentState =
  | { type: "idle" }
  | { type: "thinking"; prompt: string }
  | { type: "requesting_tool"; tool: ToolCall; reason: string }
  | { type: "executing_tool"; tool: ToolCall }
  | { type: "processing_result"; tool: ToolCall; result: unknown }
  | { type: "needs_clarification"; questions: string[] }
  | { type: "complete"; response: string; toolsUsed: ToolCall[] }
  | { type: "failed"; error: Error; partialResponse?: string };

// State transitions are explicit
type AgentEvent =
  | { type: "START"; prompt: string }
  | { type: "REQUEST_TOOL"; tool: ToolCall; reason: string }
  | { type: "TOOL_COMPLETE"; result: unknown }
  | { type: "NEED_CLARIFICATION"; questions: string[] }
  | { type: "COMPLETE"; response: string }
  | { type: "ERROR"; error: Error };

[fit] Type-Safe State Transitions

// ✅ State machine ensures valid transitions
function agentReducer(
  state: AgentState,
  event: AgentEvent
): AgentState {
  switch (state.type) {
    case "idle":
      if (event.type === "START") {
        return { type: "thinking", prompt: event.prompt };
      }
      break;
      
    case "thinking":
      switch (event.type) {
        case "REQUEST_TOOL":
          return { 
            type: "requesting_tool", 
            tool: event.tool, 
            reason: event.reason 
          };
        case "NEED_CLARIFICATION":
          return { 
            type: "needs_clarification", 
            questions: event.questions 
          };
        case "COMPLETE":
          return { 
            type: "complete", 
            response: event.response, 
            toolsUsed: [] 
          };
      }
      break;
      
    case "executing_tool":
      if (event.type === "TOOL_COMPLETE") {
        return { 
          type: "processing_result", 
          tool: state.tool, 
          result: event.result 
        };
      }
      break;
  }
  
  // Invalid transition
  if (event.type === "ERROR") {
    return { 
      type: "failed", 
      error: event.error,
      partialResponse: state.type === "thinking" ? state.prompt : undefined
    };
  }
  
  return state; // No transition
}

[fit] Parse LLM Responses

The Problem

// ❌ Trusting LLM to return correct format
async function callLLM(prompt: string): Promise<any> {
  const response = await llm.complete(prompt);
  try {
    return JSON.parse(response); // Hope it's valid JSON
  } catch {
    return { error: "Failed to parse" };
  }
}

async function getToolCall(): Promise<ToolCall> {
  const response = await callLLM("What tool to use?");
  // Hope it has the right shape...
  return response as ToolCall;
}

[fit] Parse LLM Output

// ✅ Parse LLM responses into precise types
const ToolCallSchema = z.discriminatedUnion("tool", [
  z.object({
    tool: z.literal("search"),
    parameters: z.object({
      query: z.string().min(1).max(200),
      limit: z.number().int().positive().max(10).default(5)
    })
  }),
  z.object({
    tool: z.literal("calculate"),
    parameters: z.object({
      operation: z.enum(["add", "subtract", "multiply", "divide"]),
      a: z.number(),
      b: z.number()
    })
  }),
  z.object({
    tool: z.literal("send_email"),
    parameters: z.object({
      to: z.string().email().transform(e => e as Email),
      subject: z.string().min(1).max(200),
      body: z.string().min(1).max(5000),
      cc: z.array(z.string().email().transform(e => e as Email)).optional()
    })
  })
]);

type ToolCall = z.infer<typeof ToolCallSchema>;

async function getToolCall(context: string): Promise<ToolCall | null> {
  const prompt = `Given context: ${context}
  
  Respond with a JSON tool call in this exact format:
  { "tool": "search", "parameters": { "query": "...", "limit": 5 } }`;
  
  const response = await llm.complete(prompt);
  
  // Parse and validate in one step
  const parsed = ToolCallSchema.safeParse(JSON.parse(response));
  if (!parsed.success) {
    console.error("Invalid tool call:", parsed.error);
    return null;
  }
  
  return parsed.data; // Guaranteed valid with correct types!
}

[fit] Complex Agent Example

// ✅ Multi-step agent with type safety throughout
type SearchResult = { title: string; url: string; snippet: string };
type WebContent = { url: string; text: string; links: string[] };

type AgentTool =
  | { tool: "web_search"; parameters: { query: string } }
  | { tool: "read_webpage"; parameters: { url: string } }
  | { tool: "summarize"; parameters: { text: string; maxWords: number } };

type AgentMemory = {
  searches: Array<{ query: string; results: SearchResult[] }>;
  pagesRead: Array<{ url: string; content: WebContent }>;
  summaries: Array<{ original: string; summary: string }>;
};

type ResearchAgentState =
  | { type: "planning"; goal: string; steps: string[] }
  | { type: "searching"; query: string; previousSearches: string[] }
  | { type: "evaluating_results"; results: SearchResult[]; goal: string }
  | { type: "reading_page"; url: string; reason: string }
  | { type: "synthesizing"; memory: AgentMemory; goal: string }
  | { type: "complete"; report: string; sources: string[] };

[fit] Tests Eliminated

// ❌ Without proper types
describe('Agent without types', () => {
  it('handles thinking while executing', () => {});
  it('handles response without completion', () => {});
  it('handles tool results in wrong state', () => {});
  it('validates tool parameters exist', () => {});
  it('checks tool parameter types', () => {});
  it('ensures email format in send_email', () => {});
  it('handles missing required parameters', () => {});
  it('handles unknown tool names', () => {});
  // ... dozens more defensive tests
});

// ✅ With algebraic types
describe('Agent with ADTs', () => {
  it('transitions from thinking to tool request', () => {});
  it('processes tool results', () => {});
  it('handles clarification needs', () => {});
  it('completes with response', () => {});
  it('handles errors gracefully', () => {});
  // Just test the business logic!
});

[fit] Part 4:
API Validation


[fit] Runtime Validation

The Problem

// ❌ Hope the API returns what we expect
interface User {
  id: string;
  email: string;
  age: number;
  role: "admin" | "user";
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data; // 🤞 Hope it matches User interface!
}

// Runtime error when API returns { id: 123, email: null, role: "superuser" }

[fit] Parse at the Boundary

// ✅ Validate at runtime, type-safe everywhere else
import { z } from "zod";

// Define schema (runtime + compile time)
const UserSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  age: z.number().int().positive(),
  role: z.enum(["admin", "user"]),
});

// Extract the TypeScript type
type User = z.infer<typeof UserSchema>;

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  // Parse and validate in one step
  const user = UserSchema.parse(data);
  return user; // Guaranteed to be valid User!
}

// Or handle errors gracefully
async function fetchUserSafe(id: string): Promise<User | null> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  
  const result = UserSchema.safeParse(data);
  if (result.success) {
    return result.data;
  } else {
    console.error("Invalid user data:", result.error);
    return null;
  }
}

[fit] Complex API Types

// ✅ Nested validation with branded types
const EmailSchema = z.string().email().transform(email => email as Email);
const UserIdSchema = z.string().uuid().transform(id => id as UserId);

const CommentSchema = z.object({
  id: z.string(),
  authorId: UserIdSchema,
  content: z.string().min(1).max(500),
  createdAt: z.string().datetime(),
  edited: z.boolean(),
  editedAt: z.string().datetime().optional(),
});

const PostSchema = z.object({
  id: z.string(),
  authorId: UserIdSchema,
  title: z.string().min(1).max(200),
  content: z.string().min(1),
  tags: z.array(z.string()),
  status: z.enum(["draft", "published", "archived"]),
  comments: z.array(CommentSchema),
  metadata: z.object({
    views: z.number().int().nonnegative(),
    likes: z.number().int().nonnegative(),
    shareCount: z.number().int().nonnegative(),
  }),
});

type Post = z.infer<typeof PostSchema>;
// All nested types are validated and branded!

[fit] Local Reasoning


[fit] What is Local Reasoning?

Understanding code behavior by looking only at:

  1. The function signature
  2. The types involved
  3. The immediate context

Not by:

  • Tracing through the entire codebase
  • Reading implementation details
  • Checking all callers

[fit] Example: Payment Processing

// ❌ Without precise types - need to read everything
async function processPayment(
  customerId: string,
  cardNumber: string,
  amount: number,
  currency: string
): Promise<{ success: boolean; transactionId?: string }> {
  // Need to read implementation to know:
  // - Is customerId validated?
  // - Is cardNumber format checked?
  // - Can amount be negative?
  // - What currencies are valid?
  // - When is transactionId present?
}

// Must trace through callers to understand usage
const result = await processPayment(
  getUserId(),      // Is this the right ID format?
  getCardNumber(),  // Is this validated?
  calculateTotal(), // Can this be negative?
  "EUR"            // Is this supported?
);

[fit] With Precise Types

// ✅ Everything clear from the signature
type CustomerId = string & { readonly __brand: "CustomerId" };
type CardNumber = string & { readonly __brand: "CardNumber" };
type PositiveAmount = number & { readonly __brand: "PositiveAmount" };
type Currency = "USD" | "EUR" | "GBP";

type PaymentResult =
  | { type: "success"; transactionId: TransactionId; amount: PositiveAmount }
  | { type: "declined"; reason: string }
  | { type: "error"; code: string; retryable: boolean };

async function processPayment(
  customerId: CustomerId,
  cardNumber: CardNumber,
  amount: PositiveAmount,
  currency: Currency
): Promise<PaymentResult> {
  // Implementation doesn't matter for understanding!
}

// Usage is self-documenting
const result = await processPayment(
  CustomerId("cus_123"),        // ✅ Must be valid
  CardNumber("4242424242424242"), // ✅ Must be valid
  PositiveAmount(99.99),        // ✅ Must be positive
  "EUR"                         // ✅ IDE shows valid options
);

switch (result.type) {
  case "success":
    // TypeScript knows transactionId exists here
    console.log(`Transaction ${result.transactionId} completed`);
    break;
  case "declined":
    // TypeScript knows reason exists here
    showError(`Payment declined: ${result.reason}`);
    break;
  case "error":
    // TypeScript knows code and retryable exist here
    if (result.retryable) {
      retry();
    }
    break;
}

[fit] Function Signatures
Tell the Whole Story

// ✅ These signatures tell you everything

// Pure transformation - no side effects
function toUpperCase(s: string): string

// Might fail - check the result
function parseDate(s: string): Date | null

// Async operation that might fail
function fetchUser(id: UserId): Promise<User | null>

// Side effect with no return
function logError(error: Error): void

// Infallible parser with proof
function parsePositive(n: number): PositiveNumber | never

// State machine transition
function nextState(current: State, event: Event): State

// Builder pattern - returns new instance
function withTimeout(request: Request, ms: number): Request

[fit] Key Takeaways


[fit] 1. Parse at the Boundaries

// ✅ Validate once, use everywhere
const email = parseEmail(userInput);
if (!email) {
  return { error: "Invalid email" };
}

// No more validation needed!
sendWelcomeEmail(email);
updateUserEmail(userId, email);
addToMailingList(email);

[fit] 2. Make Illegal States
Unrepresentable

// ✅ If it compiles, it works
type LoadingState =
  | { status: "idle" }
  | { status: "loading"; startedAt: Date }
  | { status: "success"; data: Data; loadedAt: Date }
  | { status: "error"; error: Error; canRetry: boolean };

// No defensive programming needed!

[fit] 3. Use the Type System
as Documentation

// ✅ Types explain the business rules
type OrderState =
  | { status: "draft"; items: Item[] }
  | { status: "submitted"; items: NonEmptyArray<Item>; submittedAt: Date }
  | { status: "paid"; items: NonEmptyArray<Item>; paidAt: Date; paymentId: PaymentId }
  | { status: "shipped"; items: NonEmptyArray<Item>; trackingNumber: TrackingNumber }
  | { status: "delivered"; items: NonEmptyArray<Item>; deliveredAt: Date };

// The types show: drafts can be empty, but submitted orders cannot

[fit] The Testing Impact


[fit] Tests Eliminated:
Validation

// ❌ Before: Testing defensive programming
describe('OrderService - validation approach', () => {
  it('rejects empty order in calculateTotal', () => {});
  it('rejects negative quantities in calculateTotal', () => {});
  it('rejects negative prices in calculateTotal', () => {});
  it('rejects invalid coupon in calculateTotal', () => {});
  
  it('rejects empty order in submitOrder', () => {});
  it('rejects negative quantities in submitOrder', () => {});
  it('rejects negative prices in submitOrder', () => {});
  it('rejects invalid coupon in submitOrder', () => {});
  
  it('rejects empty order in validateOrder', () => {});
  // ... 20+ validation tests
});

// ✅ After: Testing business logic only
describe('OrderService - parse approach', () => {
  it('calculates total with items', () => {});
  it('applies coupon discount', () => {});
  it('submits order successfully', () => {});
  // Just 3-4 business logic tests!
});

[fit] Tests Eliminated:
State Machines

// ❌ Before: Testing impossible states
describe('FormComponent - boolean flags', () => {
  // Testing all 2^6 = 64 combinations
  it('handles isLoading=true, isSubmitting=true', () => {});
  it('handles isLoading=true, hasError=true', () => {});
  it('handles isSuccess=true, hasError=true', () => {});
  it('handles hasError=true, errorMessage=undefined', () => {});
  it('handles isSuccess=true, data=undefined', () => {});
  // ... 59 more impossible state tests
});

// ✅ After: Testing valid states only
describe('FormComponent - ADT', () => {
  it('shows form when idle', () => {});
  it('shows spinner when loading', () => {});
  it('shows error with message', () => {});
  it('shows success with data', () => {});
  it('transitions between states correctly', () => {});
  // Just 5 tests for 5 valid states!
});

[fit] Tests Eliminated:
Type Safety

// ❌ Before: Testing type mismatches
describe('Agent - without types', () => {
  it('validates tool exists', () => {});
  it('validates search has query parameter', () => {});
  it('validates calculate has operation parameter', () => {});
  it('validates calculate has numeric a and b', () => {});
  it('validates email has valid to address', () => {});
  it('validates email has subject', () => {});
  it('validates email has body', () => {});
  it('handles missing parameters gracefully', () => {});
  it('handles wrong parameter types', () => {});
  // ... dozens of parameter validation tests
});

// ✅ After: Compiler ensures correctness
describe('Agent - with types', () => {
  it('executes search tool', () => {});
  it('executes calculate tool', () => {});
  it('executes email tool', () => {});
  it('handles tool errors', () => {});
  // Just test the actual functionality!
});

[fit] Total Impact

Parse, Don't Validate

  • Before: 20+ validation tests per service method
  • After: 1 parser test + business logic tests
  • Reduction: 85%

State Machines

  • Before: 2^n tests for n boolean flags
  • After: n tests for n valid states
  • Reduction: Exponential → Linear

Type-Safe APIs

  • Before: Defensive tests for every parameter
  • After: Schema validation at boundary only
  • Reduction: 90%

[fit] Tests You Don't
Need to Write

Before: ~200 defensive tests

After: ~30 business logic tests

85% reduction in test code

100% increase in confidence


[fit] Thank You!

Questions?

Resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment