ai-agent-gateway-cli
Python client for the ai-agent-gateway HTTP/SSE protocol, shipped as the hank
entrypoint.
Usage
python -m agent_gateway_cli login
python -m agent_gateway_cli chat "hello"
python -m agent_gateway_cli --config-namespace cashnerd login
python -m agent_gateway_cli --config-namespace cashnerd chat --session default "hello"
hank login
hank chat
hank chat "hello"
hank chat --model-key anthropic.claude-sonnet-5 --effort high \
--model-key-for plan.author=anthropic.claude-opus-5-5 \
--effort-for plan.author=high "research this company"
hank chat --model-key anthropic.claude-opus-5-5 --save-preference "remember this choice"
hank preset
hank chat --model-preset <published-name> "run this under a published preset"
hank chat --show-thinking "walk me through the margin bridge"
hank replay ~/.local/state/hank/evidence/isolated-gateways/<key>/ --session sess_<id>
hank control health
The default config namespace is agent-gateway for python -m agent_gateway_cli
and hank for the hank entrypoint, which writes:
~/.cache/hank/cli_config.json~/.cache/hank/sessions/<name>.json
--config-namespace cashnerd writes under ~/.cache/cashnerd/ instead. The
namespace directory and its sessions/ directory are created mode 0700; the
files inside are 0600.
Each named session file holds a local transcript, which is display and
persistence only, and the gateway session identity: the live session id and
token, and the conversation's durable id (log_session_id, the first session's
id), which is set when the first chat POST is accepted — init alone does not
establish a durable conversation, so a session that never sent one re-inits
plainly. Each turn sends only the new user message; the gateway replays the
conversation from its session log. When the stored token is no longer usable
(the session's roughly one-hour TTL has passed, or the gateway restarted), the
CLI opens a new gateway session with resume_session_id set to the durable id,
and the conversation continues for a session the caller owns. If the gateway
refuses the resume, the CLI prints a notice and starts a new conversation.
--new clears both the transcript and the identity.
GatewayTransport and AsyncGatewayTransport (agent_gateway_cli/transport.py,
with session_map.SessionMap for per-conversation identity) are the one Python
owner of the gateway chat client contract — init and resume, token reuse, the 401
rule, SSE frame parsing, re-attaching a dropped stream via /chat/subscribe from
the last delivered seq, cancel, tool approvals and results — consumed by
hank chat, api/dev/chat_cli.py and, for frame parsing only, the control
client; the contract they mirror is the consolidation design record.
Every StreamEnvelope carries received_at, and stream_turn yields the
gateway's :keepalive as a seq-less transport_keepalive envelope that never
moves the reconnect cursor. A stream_error is yielded and changes neither when
the turn ends nor how it re-attaches, but a turn that then fails carries the last
stream_error's detail in its error message. A chat stream that sends no byte
for STREAM_IDLE_SECONDS (60, four missed keepalives; the transports'
stream_idle_seconds option) is a dead transport: the read times out and the
turn re-attaches through /chat/subscribe like a dropped connection; the server
turn is never cancelled.
No client wait is a minted cap. /chat/init and the non-stream posts have no
request timeout, and a turn re-attaches for as long as the server still holds
it, repeating the last reconnect delay. The turn-busy 409 loop keeps retrying.
The cancel post is bounded only by a caller's own timeout. The returned session
carries elapsed_s, conflict_retries and reconnects, the cancel returns
cancel_outcome and cancel_elapsed_s, and a transport failure names its cause
and the seconds it waited.
Async callers implement AsyncSessionHolder; MemorySessionHolder and
SessionMap.holder(key) supply it. Same-key holders share live identity and
coordinate init with POST acceptance, releasing the lock before SSE reading.
The streaming holder still pins its own session for approvals.
cancel_chat, submit_approval and submit_tool_result take the holder and
post on the in-flight session the caller passes; a dead-bearer 401 refreshes the
holder for the next turn and the request is never replayed on the new token.
SessionMap.invalidate(key) retires that conversation for every old holder;
delayed init or POST completion cannot save it over the key's new identity.
hank chat with no message on an interactive terminal replaces the process with
the agent-gateway-tui binary (installed with
npm install -g @henrychien/agent-gateway-tui, whose engines field declares
Node >= 24), passing the resolved namespace, session and any
--research-file-id as arguments and the resolved connection as
GATEWAY_BASE_URL, GATEWAY_API_KEY,
GATEWAY_USER_ID, and AGENT_GATEWAY_ROUTE_PREFIX. The terminal reads no
per-turn selector, so --skill, --mode, --model-key, --effort,
--model-key-for, --effort-for, --model-preset, --save-preference,
--clear-preference, --raw, --drain-trailing and --new given there are named in one stderr note
and the terminal launches; they apply to the message form. hank chat "<message>" and
any non-interactive stdout run the Python client in this package instead.
hank replay <path> opens a retained chat transcript in the same terminal binary
without connecting to a gateway: <path> is a sess_*.jsonl file, a directory
holding them, an isolated instance's records root (state/logs/chat/) or a
checkout (api/chat_logs/), searched in that order. --session <sess_id> picks
one transcript, otherwise the newest by mtime; the chosen file is printed on
stderr and passed as agent-gateway-tui --config-namespace <ns> --replay <file>.
The Python client prints streamed thinking only with --show-thinking: the
model's summarized thinking appears dimmed on a [thinking] line ahead of the
[text] reply. Without the flag, thinking deltas go to the debug log only. The
TUI shows thinking by default and needs no flag.
When the provider refuses a turn (a refusal event), the Python client keeps the
text already delivered, prints [refusal] <category>: <explanation> and
[refusal] <guidance> after it, and exits 0; nothing is reset, and the guidance
is to start a fresh conversation before continuing.
When the gateway reports a frame it could not deliver (a stream_error event),
both the Python message CLI and api/dev/chat_cli.py print
[stream_error] <error> where it arrived and keep reading; the turn ends on its
own terminal, and the exit code follows that terminal.
Both the Python message CLI and api/dev/chat_cli.py stop cleanly (exit 0) on
stream_complete with terminal_disposition: "awaiting_approval". They show a
neutral parked notice with approval_id, job_id, and root_turn_id when
present, plus usage, without re-prompting or continuing automatically. The
attempt remains open in the gateway's durable log; like an interrupted turn,
the packaged CLI does not append it to local history. Completed turns alone
commit local history; unknown terminal dispositions remain protocol errors.
Contract
The CLI sends context.channel = "cli" on every chat request. When user_id
is configured, it is sent as a top-level field on both /api/chat/init and
/api/chat. Tool approvals post tool_call_id, nonce, approved, and
allow_tool_type.
The CLI consumes authenticated capability_choices from /api/chat/init.
--model-key and --effort are sent as given; the gateway judges them against
those choices and refuses an ineligible key or unsupported effort. When both are
omitted, the chat request omits all selection fields and the server resolves the
capability default. Repeatable --model-key-for CAPABILITY=MODEL_KEY and
optional --effort-for CAPABILITY=LEVEL flags populate init-time run overrides;
the capability id is sent as given and the gateway owns which capabilities are
selectable. Provider-qualified selectors, upstream model names, display-label
aliases, and local fallback catalogs are not accepted.
--save-preference and --clear-preference use the authenticated server
preference contract for account-wide session.driver state; the server refuses
a save with no --model-key. Local transcript files do not store or resolve
model preferences.
hank preset lists the names the model-selection policy publishes and your
selection, hank preset <name> selects one of those names account-wide, and
hank preset --clear clears it, through the gateway's /model-presets/self
door, which owns which names exist. The shipped policy publishes none today,
so the list is empty and any name is refused. Concrete --model-key/--effort
selection is unchanged.
hank chat --model-preset NAME sends model_preset on /api/chat/init (only
when given): the session runs under that published preset, above your saved
preset and your firm's, for every turn on it; roles the preset omits come from
policy. The gateway refuses a name its policy does not publish before any turn
runs. A stored session token is not reused when the flag is given; the CLI
re-inits, resuming the stored conversation.
--research-file-id <n> attaches context.research_file_id to every chat
turn (interactive and message form) and binds a control dispatch run; the
gateway's research preflight judges the id.
Every gateway request is composed as base_url + route_prefix + path, so the
hosted deployment is reached as https://hank.investments/gateway/api/chat/init.
The default BaseUrlPolicy accepts any hostname over https, allows http
only for a loopback host, and rejects a query or fragment. A base URL may carry
the deployment path: it is hoisted out of the base URL into the route prefix, so
--base-url https://hank.investments/gateway resolves to base URL
https://hank.investments with route prefix /gateway/api, identical to
--base-url https://hank.investments --route-prefix /gateway/api. An explicit
--route-prefix (or AGENT_GATEWAY_ROUTE_PREFIX) that neither equals nor
extends the base URL path is refused, naming both values. TLS
verification is on by default; --insecure and HANK_INSECURE=1 are honored
only for a loopback gateway, and HANK_CA_BUNDLE selects a custom CA bundle.
Products that need a narrower policy can inject
BaseUrlPolicy(allowed_schemes=..., allowed_hostnames=...).
Control plane
Chat and control use separate credentials and separate channels. hank login --control-api-key <key> [--control-channel <channel>] stores the control key in
the same namespace config file. Chat requests always send
context.channel = "cli"; control sessions default to --control-channel tui
(cron/scripts convention sets AGENT_CONTROL_CHANNEL=cli instead). Whichever
channel is used MUST match the channel of the GATEWAY_USER_KEYS entry the
control key belongs to, or session mint fails with ChannelMismatch.
hank control <subcommand> drives /api/control/*: dispatch starts an
autonomous run (--profile analyst --mode once|skill … [--follow]); runs,
run, logs, watch, cancel observe and steer it; approvals/approve/
deny, batches, artifacts, and schedules cover the rest. Run listings are
owner-and-channel scoped: hank control runs shows only runs started on the
control key's channel (documented in docs/reference/agent-run-evaluation.md).
--follow follows a run until it settles; nothing cancels it on a clock.
--timeout-seconds is an optional caller bound: reaching it stops following
and prints elapsed_s with the hank control watch <run> next call, and the
run keeps going. Ctrl-C cancels that exact run (or batch) and waits for its
terminal receipt; a second Ctrl-C exits at once with cancel_outcome: unknown.
Receipts and batch results carry elapsed_s, reconnects, cancel_elapsed_s
and cancel_outcome, and control object results carry elapsed_s.
hank control approvals lists pending approvals and approve/deny resolve
them; there is no standing approval policy to read or set from the CLI.
Metadata
Release files for ai-agent-gateway-cli 0.8.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ai_agent_gateway_cli-0.8.7.tar.gz | 171.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ai_agent_gateway_cli-0.8.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 264.5 kB
Release files / ai_agent_gateway_cli-0.8.7.tar.gz
| Download URL | ai_agent_gateway_cli-0.8.7.tar.gz |
|---|---|
| Size | 171.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
06d6580378dda22ae6cbf2bd3b4c840cf32957af787475801a9ef1171f6cc6ec
|
|
BLAKE2b-256 checksum How to use checksums |
58aea9d496278aa21b9ba9f5f687446e50174c0c3edca53b552f21e02eb3b0f4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|
Release files / ai_agent_gateway_cli-0.8.7-py3-none-any.whl
| Download URL | ai_agent_gateway_cli-0.8.7-py3-none-any.whl |
|---|---|
| Size | 93.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
abbf042d975a12e39e1085243345eb2d031d1b587118a41fc27d83d7e3a5a409
|
|
BLAKE2b-256 checksum How to use checksums |
781a7fd083e187e3d0071426f3a43ef3251bddc9a8205514e4ee0dd6c065b610
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.2
|