Shahathir (•◡•)

24 · Batu Caves, Selangor, Malaysia · 🇲🇾

professionally distracted

My Career Journey

  1. Financial Risk Group logo

    Financial Risk Group

    1yrs 3mos

    Assistant Software Developer

    Jun 2025 – Present

  2. Estee Lauder Companies logo

    Estee Lauder Companies

    6mos

    Software Engineer Intern

    Sep 2024 – Mar 2025

Tools & Platforms

TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman

Words I Live By

Shahathir is currently not listening to anything
Shahathir is currently not listening to anything

2026 © shahathir.me

Changelogs · Old site

  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
July 5, 2026

Giving an agent a tool to ask *me* a question

A human-in-the-loop tool that pauses the turn to offer a few choices — and the surprise that the tool re-executes from the top when the user answers.

Sometimes the agent hits a fork that's genuinely mine to decide — which of two approaches to take, which source to pull from. Guessing is bad; so is dumping a wall of text that asks. The nice move is to pause and offer a few buttons.

So I gave it a tool: ask_user. The model calls it with one to four questions, each a short header, a prompt, and two to four options. The UI renders a little card, I click, and the turn continues with my answer folded into the tool's result.

The interesting part is how it pauses, because it changed how I think about tools.

The tool raises an interrupt from inside its own body. The first time it runs, interrupt() throws and the whole stream stops with a "waiting on the user" reason. When I answer, the agent doesn't resume at some saved instruction pointer — it re-executes the entire tool from the top, and this time that same interrupt() call returns my answer instead of throwing.

That "re-runs from the top" detail is the whole thing. The interrupt() line is effectively a checkpoint, which means everything before it has to be idempotent — if the tool did real work before asking, that work happens twice. In practice you validate the questions, then interrupt, and do nothing else before it.

ASK_USER = "ask-user"
 
@tool(context="tool_context")
def ask_user(questions: list[dict], tool_context) -> dict:
    if problem := validate(questions):
        return {"ok": False, "error": problem}   # never pause on a bad payload
 
    # 1st call: raises and stops the stream.  On resume: returns the answer.
    reply = tool_context.interrupt(ASK_USER, reason={"questions": questions})
    if reply.get("skipped"):
        return {"ok": True, "skipped": True}      # user declined -> model uses judgment
 
    raw = reply.get("answers", [])
    return {"ok": True, "answers": [
        {"question": q["question"],
         "selected": (raw[i] if i < len(raw) else {}).get("selected", [])}
        for i, q in enumerate(questions)          # index-aligned to the questions
    ]}

Here's the felt experience — press run and the turn streams until the tool fires, then the whole thing blocks on your click before resuming:

idle

Press run to start a turn.

A couple of contract choices I liked: answers are index-aligned to the questions and carry option labels (strings), not indices, so nothing breaks if options get reordered; the client always appends an implicit free-text "Other"; and there's a skip path that resolves with {skipped: true} and a note telling the model to proceed with its best judgment and not re-ask.

To the model it looks like the most ordinary tool imaginable — ask a question, get an answer. Underneath it's a coroutine that suspends a whole streaming turn on a human. It's the same shape as the multiple-choice prompts coding agents now use to check in with you mid-task, and building it made those feel a lot less like magic.