ai-hats-client
Drive an ai-hats headless session from a program: send turns, wait for each
one to end, and read everything the session did.
ai-hats headless runs a role's interactive session with no terminal. Its stdin
takes commands, one JSON object per line (commands/v1). Its stdout opens with a
header line (headless/v1) and then copies the session's event log byte for
byte (events/v1). Its exit code says how the session ended. This package speaks
that contract and nothing else. It uses only the standard library and imports
nothing from ai-hats.
from ai_hats_client import HeadlessSession
with HeadlessSession.start(["ai-hats", "headless", "-r", "maintainer"]) as s:
first = s.turn("remember the word KIWI")
second = s.turn("which word?")
end = s.close()
assert "KIWI" in second.text and end.code == 0
HeadlessSession.start(argv, cwd=, env=)launches the holder and reads the header. If the holder refuses before the start, it raisesSessionEnded, which carries the exit code.prompt(text, id=None)sends a turn and returns its id.turn_for(id)waits for theturn_endedevent that lists that id.turn(text)does both.close()closes stdin, which tells the holder to finish its turns and exit. It then reads to the end and returns anExitwith the code, every event and the holder's stderr.terminate()signals the holder instead.- Leaving a
withblock closes the session. If the session does not end withinstart(..., close_timeout=60), the holder is terminated and theHeadlessTimeoutis raised, so no session keeps spending turns.
Every wait has a bound. Every error carries what was read so far and the tail of the holder's stderr.
- A command the holder would refuse raises
ValueErrorbefore it is sent: empty text, anidthat is not a lowercase canonical UUID, adecisionother thanallowordeny. - A command the holder does not run raises
HeadlessError, and so does any command after the session has ended. The holder lists what it runs inheader.commands. prompt(),answer()andinterrupt()may be called from another thread, for instance a watchdog that interrupts a turn while the main thread waits on it. The waits themselves belong to one thread.
Questions and interrupts
A session asks its stdin owner what it would ask a person at the terminal: a
gate's ask, a consent point, claude's own permission prompt, leaving plan mode,
the model's own question. Each question arrives as a person_asked event with a
call_id, and the call waits for your answer.
def decide(question):
s.answer(question["call_id"], "allow") # or "deny", message="why"
turn = s.turn("push the branch", on_question=decide)
- A turn wait hands each question to
on_question. Without a handler it raisesQuestionPendinginstead of waiting out its bound. Answerpending.question["call_id"]and wait again: nothing read so far is lost. answer(call_id, "allow", answers={"<question>": "<answer>"})answers a question the model asked withAskUserQuestion.tool_call(call_id)returns the call the question is about, with the questions and their options in itsinput.- A question stays offered until you answer it, its handler returns, or its call gets a result: a handler that raised sees it again on the next wait.
- The first answer on a call id wins. The holder refuses the rest and logs a
command_rejectedsignal for each, whosecall_idnames the call. Match a refusal to your answer by it, never by thedetailtext; it may land in a later turn than the question, andeventsholds every one read so far. - A question you are offered may already be closed: the session withdrew it, or
its call's result had not been read yet. Answering it is then not a mistake —
the answer comes back refused, and whether the call ran is the
okof itstool_result_received. interrupt()stops the running turn and keeps the session. The turn still ends with itsturn_ended, and a question it had open is closed.
The holder never times out a question. If you will not wait, answer deny.
The client is synchronous. A caller that needs asyncio runs it in a thread.
Testing without a model
ai_hats_client.testing has a stand-in claude binary that speaks the same wire.
Tests that use it need no model and no login:
import os
from ai_hats_client.testing import install
stub = install(tmp_path) # writes tmp_path/stub-bin/claude
env = {**os.environ, **stub.env(os.environ["PATH"])}
Each prompt's text picks what the stub does. The directives are listed in the
module docstring of ai_hats_client.testing.stub_claude.
Metadata
Release files for ai-hats-client 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ai_hats_client-0.4.0.tar.gz | 24.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ai_hats_client-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.9 kB
Release files / ai_hats_client-0.4.0.tar.gz
| Download URL | ai_hats_client-0.4.0.tar.gz |
|---|---|
| Size | 24.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
750fda5484349bc666bd39de2b0ff2093ffc06c5beb27444f3dec6348c9179c3
|
|
BLAKE2b-256 checksum How to use checksums |
6c334a7d4e7dfca7a9503ff582ef8e789a5804c937f8c67123b57ee91240fdd6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logRelease files / ai_hats_client-0.4.0-py3-none-any.whl
| Download URL | ai_hats_client-0.4.0-py3-none-any.whl |
|---|---|
| Size | 21.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f69614a71033c4c4a6dc4d4d0436fefbcca19b31bd4375a250a26481dc81f9fb
|
|
BLAKE2b-256 checksum How to use checksums |
e6d5cb810454ba9a00cfee3ebe36ad0b3339d5ebfffaa2f4e726d2856bd1bf34
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logSecurity for ai-hats-client 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ai_hats_client-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.