Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

The agent1 of 15

The two modes

About 7 minutes to read

On this page

Kotoba runs one agentic loop. It runs in one of two modes, and the mode decides three things: which tools she is offered, how many times she may call them, and whether you are waiting for her.

Companion modeWork mode
What it isThe conversation you are inA background job that outlives the turn that started it
Tools offered24 — or 33 inside the Discord bot process29 — or 38 inside the Discord bot process
Tool calls per turn840
Model calls (iterations) per turn941
Failed tool calls in the turn before tools are withdrawn26
Time ceilingnone in the loop — each tool has its own timeout3600 s of compute, or 1500 s when an ElevenLabs agent call is holding the line
Who hears itYou, liveNobody — its text goes to a queue nothing reads; only the final summary is spoken

Every number above was read out of core/loop.py, core/transport.py and the live registry. The work-mode numbers are settings, not constants — see Changing the caps.


Companion mode

This is the default. Every turn you type or speak runs here.

She gets her full companion toolset every turn. There is no intent router — no keyword matching decides "this sounds like a search". The loop offers every tool that passes its own availability check and the model picks. From core/loop.py:

"There is no keyword intent router: she gets her full toolset every turn, gated only by real AVAILABILITY through each tool's check(), and the MODEL decides what to call."

The caps are per turn, not per session. A turn ends when the model answers without calling a tool. The next thing you say starts a fresh budget.

When the budget runs out, the loop does not stop mid-sentence: it withdraws every tool and runs one more iteration so she can answer in words with what she has. That is why the iteration cap is always at least one higher than the tool-call cap — the gate fires at the top of an iteration, so at the model's usual one call per iteration it needed iteration 41 of 40 and never ran. The loop just stopped mid-chain, and the work runner announced "Done." as a success.

What companion mode cannot reach

Seven tools are work-mode only. They are the ones that block on a human, spawn a process, or install third-party code:

delegate · mcp_find · mcp_install · activate_tools · ask_secret · request_credential · get_credential

Tools from a connected MCP server are also work-mode only — their toolset is named mcp:<server>, which is not in the companion set. So the browser lives in work mode, always.

If the model calls one of those names in a conversation anyway, the loop refuses before dispatch and tells it plainly that nothing ran:

"'…' is not part of a conversation — it belongs to a background job. NOTHING ran and there is no result."

What work mode cannot reach

Exactly two, and they are the mode controls themselves: start_work and cancel_work. A job cannot start another job or cancel itself.


Work mode

Starting one

She starts it, not you. There is no "run in background" button. When a request needs the browser, several steps, code execution or a new capability, the model calls start_work(goal=…) — and that call returns immediately, so the conversation keeps going.

Two things travel into the job:

  • The goal, which the model writes.
  • Your own message, verbatim. The goal is a paraphrase and paraphrases lose things — a URL, an exact spelling, the language you asked in. The brief tells the job that your words override the paraphrase above them.

One job per session. Ask for a second while one is running and start_work refuses and tells her to say so.

The tool's own return value is deliberately narrow:

"The job has been LAUNCHED in the background — that is all that has happened so far: nothing has been opened, read, built or run yet."

Following one

The job emits frames on the same event channel the UI already listens to: work_started, working, a step card per action, artifact when a file is written, task_list when she updates her plan, subagent_spawned/subagent_done for helpers, then work_done.

Every frame carries the job's own run_id. That is what lets a client tell a background job's rows from the turn it is currently awaiting — without it, a long job's steps drew inside a new reply.

When the job ends, the client fires a hidden __work_done__ turn — the web app sends it over the voice channel, the terminal runs the same turn in process — and she announces the result in her own words. The sentinel is never shown in the transcript and never reaches memory. It is announced once: the work state marks it, so a second trigger does not repeat it.

Cancelling one

cancel_work is a tool, not a button. You say "stop that" and the model calls it. It cancels the runner task, clears the state, and also cancels any deferred approval that was still waiting — a job stopped without that left an approved-but-detached command running.

The web UI has no cancel button, on purpose. Grepping the frontend for cancel_work returns nothing.

The terminal has /stop, which calls the same tool's execute() directly. It names what would die and asks first:

stop the long job? 4m 12s in 12 steps so far, on: <goal>. None of it is kept if you stop it now

/stop takes no argument. Typed with one (/stop 2) it answers the word instead of acting, because the terminal numbers a session's past jobs and a misread number on a rail that kills something must not go unanswered.

Cancelling is not failing. A requested stop clears the state and announces nothing; the bracket is still closed so the UI stops showing work in progress. A CancelledError nobody requested is told apart from a real stop (Task.cancelling() == 0) and recorded as the failure it is.


What else changes in work mode

  • A system message is injected describing how to behave: act rather than describe, verify before claiming, keep the workspace organised. It is built from the tools actually offered, so she is never told how to drive a browser she does not have.
  • Step zero is a skill. If one of her skills matches the job, her first call must be skill_view on that one skill. Everything after it acts. See Skills.
  • MCP servers are deferred. A connected server's tools are not offered until the model calls activate_tools('<name>'), because tool-selection accuracy falls off past roughly 30–50 tools at once. browser is the exception and is always active.
  • clarify and ask_user block. In a conversation a question is the answer — she asks it and your reply is the next turn. A background job has no next turn, so both put a card on screen and wait (up to 300 s for ask_user, 180 s for clarify). A question that cannot be drawn anywhere is refused before it is asked, not lost after.
  • Approval windows widen. The job runs on channel="text", which buys the roomy 180-second approval window instead of a voice turn's 25 or 60 seconds.
  • Human waiting time is not charged to the budget. The compute ceiling is wall time minus the seconds spent blocked on you. A slow password plus 2FA must not fail as "took too long".

Changing the caps

Work-mode limits are read at runtime from ~/.kotoba/settings.yaml, so a change in Settings applies to the next job with no restart. Companion caps are fixed at import from the environment.

SettingDefaultEnvironment variableWhere
work_timeout3600 (s)KOTOBA_WORK_TIMEOUTSettings → Advanced (work), CLI --settings
work_max_iter40KOTOBA_WORK_MAX_ITERsame
work_max_tool_calls40KOTOBA_WORK_MAX_TOOL_CALLSsame
work_fail_limit6KOTOBA_WORK_FAIL_LIMITsame
companion tool calls8KOTOBA_MAX_TOOL_CALLSenvironment only
companion fail limit2KOTOBA_FAIL_LIMITenvironment only

The four work fields appear in both the web Settings panel (under "Advanced (work)") and the terminal's --settings view. The two companion caps are environment-only and are in neither.

Per-tool caps ride on top of the per-turn total and are environment-only:

LimitDefaultVariable
Any one tool, per turn3KOTOBA_PER_TOOL_LIMIT
todo and cronjob, per turn10KOTOBA_TODO_TOOL_LIMIT
delegate, per turn3KOTOBA_DELEGATE_LIMIT
delegate running at once2KOTOBA_DELEGATE_CONCURRENCY
browser__* in work mode40KOTOBA_BROWSER_TOOL_LIMIT
web_search, per run8KOTOBA_WEB_SEARCH_LIMIT

An exact repeat of a call she already made this turn is not executed at all. It still counts against the per-tool cap, and the model is handed the earlier result back with an instruction not to retry it.