aitalkback
v0.1.0
Published
Hear what your AI agent is doing — speaks an agent's reasoning, tool calls, file access, commands, and token usage out loud, in real time.
Maintainers
Readme
AiTalkBack
Hear what your AI agent is actually doing.
AiTalkBack narrates an agent's work out loud, in real time — the reasoning, the tool calls, the token spend, which files it is touching, and which commands it is about to run. It sits between the prompt and the goal and tells you what is happening in between.
▸ You asked: Refactor the billing module
▸ Reading billing slash invoice dot ts.
▸ Editing billing slash invoice dot ts, to split out the proration helper.
▸ Careful — running git reset --hard HEAD~2, to drop the bad commits.
▸ Done. Claude finished and is waiting on you.Install
npx aitalkback demo # hear a scripted agent run, no install
npm install -g aitalkback # or install the CLI properlyaitalkback demo # hear a scripted agent run
aitalkback install # wire up Claude Code hooks
aitalkback listen # run the narrator daemon
aitalkback proxy # narrate any LLM API through a proxy
open http://127.0.0.1:4737/ # browser overlay (needs `listen` running)Then use Claude Code as normal in another terminal. Remove the hooks again
with aitalkback uninstall.
Requires Node 20.11+. No runtime dependencies — the whole thing is Node
builtins and fetch.
As a library
npm install aitalkbackimport { TalkBack, loadConfig } from 'aitalkback';
import { fromClaudeCodeHook } from 'aitalkback/adapters';
import { createHandler } from 'aitalkback/serverless';
const talkback = await TalkBack.create(loadConfig());
await talkback.ingest(fromClaudeCodeHook(payload));This package is ESM only. From CommonJS, use await import('aitalkback').
How it works
agent → adapter → AgentEvent → thinking buffer → narrator → speech queue → TTSEach stage is replaceable:
| Stage | Job |
| --- | --- |
| Adapter | Turns one agent's output (hooks, SSE chunks, JSON lines) into a canonical AgentEvent. Nothing downstream knows which agent it came from. Ships with Claude Code, Anthropic, OpenAI, DeepSeek, Gemini, and a generic fallback. |
| Thinking buffer | Reassembles token-by-token reasoning into whole thoughts, then extracts the one sentence worth hearing. |
| Narrator | Event → one short spoken sentence. Deterministic templates by default; an optional LLM pass only rewrites what the templates already produced. |
| Speech queue | Priority ordering, de-duplication, load shedding, and barge-in. |
| Provider | macOS say, ElevenLabs, any OpenAI-compatible /audio/speech, or stdout. |
Staying live
An agent emits events far faster than speech can play them. An unbounded queue would drift minutes behind and narrate the past, so the queue:
- orders by priority — errors and destructive commands jump ahead of chatter;
- de-duplicates — the same line inside an 8s window is dropped;
- sheds — past
maxQueuethe lowest-priority lines are discarded, never the alarming ones; - barges in — an alarming event cuts off a routine sentence mid-word, because hearing "deleting your database" after the queue drains defeats the point.
Narrating the thinking
A streaming agent emits one thinking event per token. Narrating those directly gives you fragments; there is no fixing it downstream, because by the time an utterance reaches the queue the thought has already been shredded. So reasoning is buffered into whole thoughts and flushed when the agent goes quiet (1.2s), when the block runs long (700 chars), or the moment the agent stops reasoning and acts — whichever comes first.
Each thought is then reduced to a single sentence extractively, using the agent's own words. Reasoning wanders, backtracks, and only commits near the end, so sentences are scored for decision markers ("I'll…", "the problem is…"), position, and length, while self-directed questions and hedging are discarded. A block that is all throat-clearing is silently dropped.
79 token deltas in:
"Okay, let me look at this. The user says the invoice totals are off by a
cent sometimes. Hmm. Could be rounding. ... Wait, is that actually the
problem? Yes — the rounding happens after summing, so errors accumulate.
I'll switch the proration helper to integer cents and round per line item."
one sentence out:
💭 I'll switch the proration helper to integer cents and round per line item.Extractive, not LLM-summarised, for two reasons: a network round-trip has no place in the hot path, and a summariser can invent a rationale the agent never had. Speaking the agent's literal words means the narration cannot lie about why it did something.
A thought is always spoken before the action it led to. Hearing the rationale after the edit it explains is worse than not hearing it at all.
Not saying secrets out loud
Anything spoken can be overheard, and with cloud TTS it leaves the machine.
Every line is redacted before it reaches a speaker — provider key formats, JWTs,
private keys, SECRET=-shaped assignments, and credentials embedded in URLs.
Naming a credential file is still narrated; its contents never are.
Reading like speech, not like a terminal
Paths are shortened (/Users/you/app/src/auth/session.ts → "auth slash session
dot ts"). Flags are dropped from commands — except on a destructive one,
where the flag is the danger, so rm -rf dist and rm dist never sound alike.
Verbosity
| Level | You hear |
| --- | --- |
| quiet | Session start/end, errors, deletions — the things you would want interrupted for. |
| normal | The above, plus prompts, plans, writes, edits, and commands. |
| verbose | Plus reads, network calls, tool calls, thinking, and token usage. |
| firehose | Everything, including every tool completion. |
Alarming events are spoken at every level, including quiet.
Narrating any LLM: the proxy
aitalkback proxy forwards requests to the real provider and narrates the
response stream on the way past. Point your SDK's base URL at it — no SDK
patching, no code changes:
aitalkback proxy # listens on 4738
export OPENAI_BASE_URL=http://127.0.0.1:4738/openai/v1
export ANTHROPIC_BASE_URL=http://127.0.0.1:4738/anthropic| Provider | Route | Reasoning source |
| --- | --- | --- |
| Anthropic | /anthropic | thinking_delta blocks |
| OpenAI | /openai | reasoning / reasoning_content deltas |
| DeepSeek | /deepseek | reasoning_content deltas |
| Gemini | /gemini | parts flagged thought: true |
Tool calls, token usage, truncated responses, and upstream errors are narrated
for all four. Send a route somewhere else — a gateway, a local model, a mock —
with AITALKBACK_<PROVIDER>_UPSTREAM:
AITALKBACK_OPENAI_UPSTREAM=http://localhost:1234 aitalkback proxyIt stays out of the way
The proxy sits in your request path, so the overriding rule is that narration must never affect the call:
- Nothing is buffered. Each chunk is forwarded before it is parsed, so tokens reach your app at the same moment they would without the proxy. There is a test that fails if a response is ever held back.
- Narration cannot fail the request. Malformed frames, unknown shapes, and parser errors are swallowed; the bytes still reach the caller untouched.
- Speech is never awaited. Events are queued and forgotten.
- Credentials pass straight through and are never read, stored, or logged.
- Cancellation propagates — hang up on the proxy and it hangs up upstream.
Status codes, headers, and error bodies are relayed as-is, so an app cannot tell it is talking to a proxy.
Connecting other agents
Anything that can POST JSON can talk to the daemon:
curl -X POST http://127.0.0.1:4737/v1/events \
-H 'content-type: application/json' \
-d '{"kind":"command","title":"Bash","command":"npm test","rationale":"to check the refactor"}'Or pipe JSON lines straight in:
my-agent --json | aitalkback tailBuilt-in adapters: /v1/events/claude-code (hooks),
/v1/events/anthropic (Messages API stream events),
/v1/events/openai (chat completion chunks), and /v1/events (generic).
Endpoints
| Method | Path | Purpose |
| --- | --- | --- |
| POST | /v1/events[/<source>] | Submit events. Returns 202 immediately — hooks block the agent, so narration happens on our clock, not theirs. |
| GET | / | The browser overlay: live transcript and in-browser speech. |
| GET | /v1/stream | Server-sent events feed of everything being spoken. |
| GET | /v1/recent?n=50 | The last N utterances. |
| POST | /v1/flush | Shut up right now. |
| GET | /health | Liveness, active provider, queue depth. |
Three query parameters on /v1/events let a client narrate for itself:
respond=utterances returns the spoken text instead of a bare 202,
speak=0 suppresses desktop playback, and verbosity=… overrides the
configured level for that request alone.
The daemon binds to 127.0.0.1 only. It accepts anything that reaches it, so
do not expose it to a network.
In the browser
Two clients, both speaking through the browser's own voice — no native TTS needed, so this works anywhere.
The overlay
Run aitalkback listen and open http://127.0.0.1:4737/. It subscribes to
the live feed and gives you a running transcript plus voice, rate, and
reasoning controls. Because only one sentence of reasoning is ever spoken,
each thought keeps a full reasoning disclosure you can expand and read.
The extension
extension/ is an unpacked MV3 extension. Load it via
chrome://extensions → Developer mode → Load unpacked.
It patches fetch and XMLHttpRequest in the page, tees any streaming
response, and recognises Anthropic, OpenAI, DeepSeek, and Gemini frames by
their shape rather than by URL — so it narrates any app calling those APIs
from the browser, including your own.
The popup chooses where narration is spoken:
- This browser — the daemon returns the words and
chrome.ttsspeaks them. - Desktop — the daemon speaks them itself, as usual.
Two properties it is built around, both covered by tests:
- It never disturbs the page. Only a
clone()of the response is read; the original is handed back untouched, so streaming, cancellation, and error handling all behave exactly as they would without the extension. - It cannot exfiltrate anything. Host permissions are limited to
127.0.0.1andlocalhost, so frames can only ever reach a narrator you are running yourself.
Reasoning is buffered in the service worker — per tab, so two concurrent streams cannot interleave into one garbled thought — and sent as whole blocks.
Serverless
createHandler() is a Web-standard fetch handler with no Node dependencies —
it runs on Vercel Functions, Workers, Deno, or Bun. There are no speakers in a
datacenter, so it returns the narration text for the caller to play, which suits
browser extensions and mobile clients.
// api/narrate.ts
import { createHandler } from 'aitalkback/serverless';
export const POST = createHandler({ verbosity: 'normal' });Configuration
~/.aitalkback/config.json (create it with aitalkback config --init):
{
"provider": "auto", // auto | say | elevenlabs | openai | stdout
"voice": "Samantha",
"rate": 210, // words per minute
"verbosity": "normal",
"port": 4737,
"mute": ["file.read"], // event kinds to never speak
"dedupeWindowMs": 8000,
"thinking": { "idleMs": 1200, "maxChars": 700 },
"maxQueue": 24,
"llmNarration": { "enabled": false, "model": "anthropic/claude-haiku-4.5" },
"redactPatterns": [] // extra regexes to strip before speaking
}Flags override the file (--provider, --voice, --rate, --verbosity,
--port, --llm, --dry-run), and AITALKBACK_* environment variables sit
in between.
Optional LLM narration
With --llm, a small model rewrites each already-correct template line into
something less robotic. It is capped at 2.5s and falls back to the template on
any error, so narration never blocks on the network. Off by default: the
templates are instant, free, and cannot hallucinate.
Development
npm install
npm test # 62 tests, no build step (Node type-stripping)
npm run typecheck
npm run buildRunning the tests from source needs Node 22.6+ for --experimental-strip-types,
though the published package runs on 20.11+.
The source avoids TypeScript syntax that requires a transform (no parameter
properties, no enums), so node --experimental-strip-types runs it directly.
License
MIT
