Skip to content

Questions

An agent can stop mid-turn to ask you a question: which approach to take, which file to change, whether it understood you. A flow can ask you the other way, treating the person at the prompt as an agent. Use questions whenever a run needs an answer only a person should give.

Try it

A HumanAgent is the person at the prompt, declared among the agents. See the person as an agent.

Step 1. Write a flow that pairs an assistant with one. It runs the assistant, then asks you what to say next, and repeats.

python
# .humanize/flows/pairing/__init__.py
"""An agent and you, taking turns."""

from typing import NamedTuple

from hmz.agents import AgentBase, HumanAgent
from hmz.flows import flow


class Agents(NamedTuple):
    """The agent, and whoever is at the prompt."""

    assistant: AgentBase
    human: HumanAgent


@flow
def run(agents: Agents, task: str) -> None:
    conversation = agents.assistant.new()
    said = task
    while said:
        answered = conversation(said, suppress=True)
        said = agents.human(answered)

Saying something to a HumanAgent is asking what to say next; what it answers with is what you typed. This flow is chat, the flow the interface opens on.

Step 2. Run it from a command line.

sh
hmz exec -f pairing -a claude/claude-opus-5:high "Read README.md and tell me what this is."

One -a, and the flow drives two agents. Nobody is asked what the person runs, so a HumanAgent is not one of the agents -a names. On a command line nobody is at a prompt, so agents.human(...) answers with nothing. said is falsy, so the loop ends and the flow does the one thing it was given.

Step 3. Run it in the interface to see the asking.

/flow pairing

Now it is a conversation: you and the agent take turns.

At the prompt

The agent shows the question and whatever it offered. The next line you type is the answer, not a word put into the turn. The status line reads enter answer while that is so.

You are not held to the options. Every backend that offers options also takes something else. The options are what the agent expects, and what an interface shows so the question reads as one.

If the flow ends or is stopped while a question is still up, the question ends with it. Stopping a flow is never blocked on a question.

When nobody is there

hmz exec has nobody at a prompt. /afk says the same thing on purpose:

/afk on

In both cases the backend is told nobody answered, and the agent carries on.

A turn waiting on an answer that is not coming is a flow that has stopped. So this is the default everywhere except an interface with /afk off.

This is why every questionnaire wants suppress=True and a None branch. A flow that assumed somebody was there would hang forever exactly when nobody was. Asking starts allowed: an agent that really needs a person gets one unless it has been said that none is there.

From Python

From Python, the hook is agent.ask:

python
agent.ask = lambda question: input(f"{question.text} {question.options} ")

The question it receives is:

python
@dataclass(frozen=True)
class Question:
    text: str
    options: tuple[str, ...]

Return a string to answer, or None for "nobody answered". Leave ask unset and it is None every time.

Whatever happens, the question also reaches anything watching the agent, as an asks event. A flow can record that its agent wanted to ask without answering it:

python
def looking(agent, session, event):
    if event.kind == "asks":
        Path("questions.log").open("a").write(f"{agent.id}: {event.text}\n")

agent.watch(looking)

An unattended run can collect everything its agents wanted to ask, and you can read it in the morning.

The session on an asks event is None, whichever backend asked. A question belongs to the agent, not to one conversation. ask is set on the agent, and the agent is what a stopped turn reaches. So a watcher can say which agent wanted to ask, as the one above does, but not which of its conversations did.

The other direction: a flow asking you

The flow asking you is the person as an agent:

python
said = agents.human("Here is what I did. What next?")

With a schema, the same call is a questionnaire. The person is not shown a JSON Schema. They are asked a question per field, and the model is built out of what they typed:

python
from typing import Literal

from pydantic import BaseModel, Field


class Settled(BaseModel):
    """What has to be agreed before anything is built."""

    approach: Literal["fast", "careful"] = Field(description="Which way should this be built?")
    tests: bool = Field(description="Write tests for it?")
    rounds: int = Field(default=3, description="How many rounds may it take?")


@flow
def run(agents: Agents, task: str) -> None:
    settled = agents.human("How should I do this?", schema=Settled, suppress=True)
    if settled is None:
        return                                    # nobody was there
    working = agents.assistant.new()
    for _ in range(settled.rounds):
        working(f"{task}\n\nBuild this the {settled.approach} way."
                f"{' Write tests.' if settled.tests else ''}", suppress=True)

Each field becomes a question, and what the person types is built into the model:

In the modelWhat they are asked
description=the question itself, or the field's name where it has none
Literal[…]those words, as the answers it offers
boolyes and no
a default"or - for 3" — and a dash takes it
list[str]one line, separated by commas

So a flow settles what only a person can settle in the model it is going to run on, once, rather than by parsing a sentence. What the model refuses is put back on the field it was refused for, in the model's own words, a bounded number of times.

Each of those goes the same road a coding agent's own question takes, so /afk answers it the same way: nobody is there.

The person is not:

  • a coding agent — it runs no model and spends nothing;
  • in /status's handover graph or the cost readout — its turns are not bracketed by the events that say whose turn it is;
  • one of the conversations tab steps between;
  • able to run moments — a moment is a point in a turn of a model.

The moment, for a hook

NOTIFICATION is the moment that fires when the agent stops to ask you something. A hook on it cannot answer, because a verdict does nothing there. It can log, notify or wake something up instead:

python
agent.hooks.on(Moment.NOTIFICATION, lambda occasion: ring_a_bell(occasion.said))

See also

Released under the Apache-2.0 licence.