XO Ruby Toronto · 2026

Stop being the boss
you hate

How to build AI functions, not chatbots

Space Quest III — a cubicle farm of identical drone workers, a janitor vacuuming the aisle, and a manager overseeing from a raised office nook

Space Quest III (1989) and You Using AI (2026)

QR code — follow along with the slides ↑ follow along
Michael Carroll, styled as a Space Quest III character card: 'the Speaker,' 15 years in Ruby

Show of hands

Which would you rather work in?

Work environment A
  • A clearly defined task
  • A list of tools you need for each part of the task
  • Help when you need it
  • Feedback on outputs & outcomes
Your chatbot
Work environment B
  • A somewhat defined task
  • Tools discovered as you go
  • A manager popping in to check on your work
  • Often no feedback at all

Environment B is the chatbot paradigm.

Good thing your AI chatbot doesn't have
access to Glassdoor.

Yet.

Space Quest III — a robot grabbing the protagonist by the collar

Back to the two environments

Which would you rather work in?

An AI function
Work environment A
  • A clearly defined task
  • A list of tools you need for each part of the task
  • Help when you need it
  • Feedback on outputs & outcomes

Environment A is the functional AI paradigm.

Your chatbot
Work environment B
  • A somewhat defined task
  • Tools discovered as you go
  • A manager popping in to check on your work
  • Often no feedback at all

Environment A, in engineering terms

What is an AI function?

  1. A defined set of inputs.
  2. A defined purpose (prompt).
  3. Can call other functions.
  4. An output that can be evaluated.
Space Quest III — a towering robot looming over two characters

Two functions.

Traditional software function

add_numbers(1, 4)
# => 5

You don't read its internals to trust it. You test it, and you add checks so a future change can't quietly break it.

An AI function

help_center(codebase)
# => [Article, Article, ...]
#   each describing what
#   the product does today

Mostly retrieval — shot through with interpretation, nuance and judgement. Same three properties.

One key difference? How you know it's working.

One key difference

Software is deterministic.
AI is probabilistic.

Software function
  • Testing: binary & reliable
  • Business logic: abstractable
  • Separable from code language? Yes
AI function
  • Testing: non-binary & flakey
  • Business logic: context-based
  • Separable from LLM model? Somewhat

Let's build an AI function!

Real World Example Help Center Documentation Agent

Why build a documentation agent?

  • Help users — and, more importantly, their agents — understand product features.
  • A source of truth on the product for our own coding agents.
  • Better discovery by search engines.

First pass: Have the coding agent do it

AGENTS.md
**Keeping the help center current is part of shipping a
user-facing change, not a follow-up.** Before you finish any
change that alters what a user sees or does — an API field, a
score calculation, a setting, a workflow, a limit — check whether
an article already documents it, and update that article in the
same piece of work.

It works! (Sometimes. sort of.)

  • Getting it right bloats the prompt and confuses the agent.
  • Productivity hit: a spike in merge conflicts.
  • Frequent double-checking: "Did it run?"
  • What is it costing me?

v1 · make it a function

Rethinking this as a function

Trigger
PR merged to main
→
Inputs
PR diff & current help center articles
→
AI
Reviews & suggests changes
→
Human Review
I look, edit, accept/reject
60 article suggestions piled up here after two weeks
→
Publish
Goes live to the world

It works! Like magic! Except...

A hard review gate is just
micromanaging the agent again.

Space Quest III death screen: "Just Like Mom Used to Make — As your life sputters to a close, you decide to cut down on desserts," with Restore, Restart and Quit buttons

So... how do we fix this?

Decompose the agent.

Trigger
→
Inputs
→
AI
→
Human Review
→
Publish

Decomposing our agent

Step 0
Improving our trigger

Space Quest III — wreckage scattered across a junkyard

Step 0 · before any model runs

What changed since last time? Answer it without an LLM.

app/services/help_center/source_index_service.rb
def index_path(relative_path)
  digest = Digest::SHA256.hexdigest(Rails.root.join(relative_path).read)
  source = HelpCenterSource.find_or_initialize_by(kind:, path: relative_path)

  return false if source.persisted? &&
                 source.content_digest == digest &&
                 source.deleted_at.nil?

  source.update!(content_digest: digest, digest_changed_at: Time.current, deleted_at: nil)
  true
end
Trigger
→
Inputs
→
AI
→
Human Review
→
Publish

Decomposing our agent

Step 1
Clean up the inputs

Space Quest III — a wrecked escape pod amid junkyard debris

Step 1 · what we feed it

With context, less is more.

Before
  • The entire PR diff
  • The entire help center corpus
  • “Figure out what's relevant”
After
  • Which articles cite the changed files
  • Help center search tool
  • PR description

Step 1 · Supplements

Opinionated tools for further context

  • changed_sources — files whose digest moved
  • articles_with_changed_sources — prose older than its source
  • stale_articles — untouched past the window
  • uncovered_sources — exists, nothing explains it
  • shipped_announcements — what we told users we shipped
  • open_ideas — context, so it doesn't propose a duplicate
Trigger
→
Inputs
→
AI
→
Human Review
→
Publish

Decomposing our agent

Step 2
Break out agentic subfunctions

Space Quest III — a junkyard of wrecked spacecraft and scattered debris

Step 3 · break out subagents

Same function, smaller more specialized agents

Trigger
PR merged to main
→
Inputs
PR diff & current help center articles
→
AI
Reviews & suggests changes
→
Human Review
I look, edit, accept/reject
→
Publish
Goes live to the world
Revision Agent
Revises existing articles
Features Agent
Finds & suggests new articles
Review Agent
Reviews & scores outputs of other agents for tone & accuracy
Specialized agents QA & ops AI (“Harness”)

Step 3 · rightsizing

Specialized AI is easier to rightsize and evaluate

Revision Agent
Revises existing articles
Midsize
Features Agent
Finds & suggests new articles
Midsize
Review Agent
Reviews & scores outputs of other agents for tone & accuracy
Midsize
Symlink Agent
Links articles to code files
Mini

Midsize → Sonnet, Terra, etc.  ·  Mini → Haiku, Luna, etc.

Decomposing our agent

Step 2: Bonus!
Wildcard: A magic wand for your AI

Space Quest III — wreckage beside an escape pod hatch

Bonus · the escape button

Your agent already knows best what it needs

coolhand-ruby gem — Coolhand::FeedbackService
# "A magical catch-all tool that can fulfill almost any wish…
#  logged for human review only — no data returned, no action taken."
def self.call(description:, thinking:, **)
  Coolhand::FeedbackService.new.create_feedback(
    explanation: description,
    original_output: thinking
  )
  { result: ACTIVE_VARIANT[:result] } # no-op
end
  • Tool promises a fix, but is secretly a no-op.
  • Realtime reports on where the agent is struggling.
  • Agents can't get enough of it!
Trigger
→
Inputs
→
AI
→
Human Review
→
Publish

Decomposing our agent

Step 3
Revisit the review gates

Space Quest III — climbing a catwalk past red warning stairs

Step 3 · prework

First, how much tolerance do we have?

  • Most jobs don't require you to be right all the time.
  • How much error can you tolerate?
  • Your confidence needs to be calibrated too!

Step 3 · still tuning

Review gates levers to consider:

  • Default-accept with a risk flag.
  • Sampling.
  • Non-binary feedback scoring (e.g. revisions & explanations).
  • A single pane of glass.

What a review surface actually needs.

  • Serialized.
  • Deduplicated.
  • Time-boxed.
  • Captures high-signal feedback.
QR code to github.com/Coolhand-Labs github.com/Coolhand-Labs
captainslog's Captain's Daily Brief review screen — an AI-generated daily brief open for direct edit, with Good / Neutral / Bad feedback buttons below
captainslog

Making the help center public increased our Google traffic 3×.

Daily impressions, Aug 2 to Sep 2, 2026, trending upward for both regular Google search results and AI answer engines Aug 2 Sep 2 Google search (SEO) AI answer engines (AEO)
Impressions, Google Search Console, Aug 2 – Sep 2, 2026 (each line scaled to its own range)

And costs have dropped from ~$0.26 per PR to $0.11 per PR.

A little shameless promotion

Coolhand Labs logo Coolhand Labs

A supervisor for your prod agents that doesn't just observe.

We do the AI drudge work, so you can focus on building:

  1. Catches silent failures and reasoning errors.
  2. Turns human feedback into code fixes — no black boxes.
  3. Right-sizes models and cuts token waste.

Thank You

(From me and your AI.)

QR code — these slides michael.carroll.io

Michael Carroll · Founder, Coolhand Labs

Stop being the boss you hate: build AI functions, not chatbots

A talk by Michael Carroll, founder of Coolhand Labs, given at XO Ruby Toronto in October 2026.

About the speaker

Michael Carroll has 15 years in Ruby and has founded three companies. He was on the founding team at RubiconMD, a Ruby health-tech company acquired and now part of CVS Health, then VP of Engineering at Teladoc Health, overseeing 500+ engineers through the pandemic. As he puts it: “One unifying theme of my career? Saving myself and others from the drudge work of engineering, so we could focus on building products.” He now runs Coolhand Labs, which removes that same drudge work from running AI agents — catching silent failures, turning human feedback into code fixes, and right-sizing models — and writes The Everything Engineer.

Two work environments

Consider two work environments. In environment A, the task is clearly defined, you have a list of tools for each part of it, there's help when you need it, and the feedback is about outputs and outcomes. In environment B, the task is somewhat defined, you discover the tools as you go, a manager pops in now and then to check on your work, and often there's no feedback at all. Ask a room which they'd rather work in and the answer is close to unanimous: A.

Environment B is how most of us work with our chatbots. The task is fuzzy, the context is re-supplied by hand every conversation, and we hover over every turn. Good thing your AI chatbot doesn't have access to Glassdoor — yet. The way out isn't a better model or more patience. Environment A is the functional AI paradigm.

Software is deterministic. AI is probabilistic.

Two functions can share the same three properties — inputs, rubrics, an evaluable output — and still be nothing alike to build. add_numbers(1, 4) is deterministic: testing it is binary and reliable, its business logic abstracts cleanly out of the surrounding code, and it's fully separable from the language it's written in. An AI function like help_center is probabilistic: testing it is non-binary and flakey, its business logic is often the context itself rather than something you can pull out into a spec, and it's only somewhat separable from the LLM model running it. That's the one key difference this talk is really about — not the syntax, but how you know it's working.

An agent, then, is a function whose steps aren't all pre-defined: it gets triggered by a schedule, an event or another agent; it loads its baseline context and decides what data and tools it needs; and it produces an output. Notice what's missing from all three steps. A human should never talk to the agent. The only human involvement is evaluating the output.

A case study: keeping our help center current

The rest of the talk is one real function at Coolhand Labs, told in seven beats. The job is unglamorous and never urgent, which is exactly why it rots: keeping public help center articles describing what the product actually does today. It's worth having at all for three reasons: it helps users — and, increasingly, their agents — understand product features; it's a source of truth our own coding agents cite; and it's better discovery by search engines. The number that made the case internally was one I didn't expect: making the help center public increased our Google traffic 3×.

First, the problem. Our AGENTS.md says, in bold, that keeping the help center current is part of shipping a user-facing change and not a follow-up — check whether an article documents the thing you changed, and update it in the same piece of work. Every coding agent in the repo reads it. It works — sometimes, sort of: getting the voice and the goals right bloats the prompt and confuses the agent; merge conflicts spike as agents step on each other's edits; I end up manually double-checking whether it even ran; and there's no way to know what any of it costs, so there's no way to know whether it's worthwhile. An instruction is not a function — it fails all three tests above.

Second, turn it into a function. Take it out of the chatbot and break it into three pieces: propose an idea, write the update, review it. A nightly coordinator fans out one job per source kind, and each one digests every allowlisted file with SHA256 into a source index. That's deliberately not git log --since: from a Sidekiq worker, git depends on the deploy image's clone depth, ref layout and detached-HEAD state, none of which the build guarantees, while a digest per path answers the same question from data we control and survives a rebuild that rewrites mtimes. On top of that index, a signal collector builds the agent's brief out of six deterministic queries — sources whose digest changed, articles whose prose predates a change to a source they cite, articles past their freshness window, sources no article has ever been derived from, features we announced since the last run, and the currently open ideas so it doesn't propose a duplicate. Because the brief is deterministic, the coordinator can ask whether tonight's run is worth paying for before paying for it: if the report is empty, the model never runs and the help center costs nothing that week.

The propose step gets twelve tools, counting the shared ones. All of them read; the only one that creates anything is the tool that proposes an idea — it cannot write an article at all. The capability boundary is the tool list rather than a sentence in the prompt asking it politely not to publish, because an agent can ignore an instruction but it cannot call a tool it doesn't have. Its output is a database row with a status, a kind, a source and a deterministic dedupe key, not a paragraph: reviewable, queryable, and carrying provenance. One input surprised me: our agents have a "wildcard" tool that promises to fix whatever is blocking them; across 489 records it was a placebo that never did anything, and they used it anyway. What they wrote turned out to be a documentation backlog, so agent escalations are now a first-class signal in the brief. Accepting an idea is two lines of code that dispatch the write step — the interesting part was never the dispatch, it's everything that decided the dispatch was worth doing.

Third: it's still wrong sometimes. Probabilistic means it will be, no matter how good the pipeline is. How do we get to trusting it?

Fourth, add review gates on each function, and set a baseline. There are two approval gates. Agent-proposed ideas land pending and need human triage, because acceptance is what authorizes paid LLM spend on a writer run. And a rewrite of an already-published article stages a revision and waits, because these pages are public, indexed, and quoted verbatim by answer engines. Both gates are correct, and I don't want to remove either — but correct isn't the same as permanent. The baseline they're earning is the same one a good CFO works to: they don't audit every invoice, they true up the numbers periodically, and when the numbers don't true up they fix the process so it can't happen twice.

Fifth: once the baseline is high enough, suspend the gates — or sample them at a much lower rate. Everything gets reviewed early, while there's no baseline yet. A fraction gets reviewed once accuracy holds up. And review goes back to everything the moment something changes that needs it back: a new source, a new prompt, a spike in rejections. I can't defend the exact sampling number we picked — that's on the open-questions list — but I can defend the shape: review load should track how much you actually trust the function that week, not stay fixed forever.

Sixth: but what about when people forget to review? Sampled or not, the ideas still sat in a Rails admin table, and I opened it when I remembered to — the same manager from the start of this talk, wearing a different hat. A well-calibrated sample rate still assumes someone opens the queue, and a CRUD table has no cadence of its own; it waits. So we changed the review surface, not the gate. When approvals become the bottleneck, the instinct is to remove the approval; almost always the right move is to make reviewing cheap enough that the approval stops costing anything. A review surface needs three things a CRUD table doesn't have: serialized, so you see one item at a time in order rather than grazing a list for disasters; deduplicated, so it never hands back something you already judged; and time-boxed, with a queue sized so finishing is possible. (Sampling by maturity, from the previous beat, is what decides how much lands in the queue in the first place.) So the review queue became its own endpoint under a /captain namespace — captainslog — authenticated with a bearer token from a "Sign in with Coolhand" OAuth flow rather than an admin session cookie, with the dedup filter applied server-side so two devices can't disagree about what's left.

Whichever direction the feedback comes from, it only counts if it becomes a change. Rejecting the same proposal three times is a rubric the agent never got. An edit that doesn't turn into a prompt fix, a tool fix or a change to what the agent is allowed to read improved exactly one output.

Seventh: where this goes next

Plenty is still unsolved. What's the right sampling rate — everyone picks a number that feels right and nobody can defend it. How do you detect drift when the output is prose and there's no diff to review? What does a good rubric look like for a judgement-heavy function, and who writes it — the engineer or the domain expert? And when is a function mature enough to have no gate at all? We are not there. Saying so plainly is what keeps this a report from the middle of the work, not a victory lap.

So the next time working with AI feels like a slog, look at what you actually built. If you're sitting there typing yes, yes, yes, you didn't build an AI function. You hired a worker to micromanage.

Related reading: The Captain Strategy: stop using AI to become the boss you hate and Captains don't steer every AI agent turn — they read the logs, in The Everything Engineer. The agent-feedback material comes from Your AI Agent Has Notes.