AGENTS.md
Research crates carry reserved symbols; read
RESERVED.mdbefore touching them. It also holds the issue-label and auto-ship rules. Ordinary chatty2 work is unaffected.
Quick-start guide for AI coding agents working in this repository. Optimized for limited context windows: read this first, then dive deeper via the links below.
For human-oriented documentation, see the user guides
(published at https://boersmamarcel.github.io/chatty2/). The repo
README.md is a short landing page (download, links, dev
quick-start). For coding patterns see CLAUDE.md. For
architecture, see docs/INDEX.md.
What is this repo?
Chatty — a desktop and terminal AI agent built in Rust. Two binaries (GPUI desktop app, Ratatui terminal app) backed by a shared, UI-agnostic core crate.
Workspace map
crates/
├── chatty-core/ # UI-agnostic: models, services, tools, settings,
│ # repositories, factories, exporters
├── chatty-gpui/ # Desktop binary `chatty` (GPUI)
├── chatty-tui/ # Terminal binary `chatty-tui` (Ratatui)
│ # also: --headless, --pipe, and acp (stdio) modes
├── chatty-terminal/ # Embedded terminal model: PTY + alacritty Term (no UI)
├── chatty-wasm-runtime/ # Wasmtime runtime for WASM agent modules
├── chatty-module-registry/ # Module discovery, manifest, lifecycle
├── chatty-protocol-gateway/ # HTTP gateway: OpenAI / MCP / A2A protocols
├── chatty-fabric/ # Broker's pure state: Directory, TaskTable, EdgeLog, Transport trait,
│ # EndpointBudget/RunPermit, swarm.rs (nested-run event forwarding)
├── chatty-module-sdk/ # SDK for `wasm32-wasip2` modules (standalone)
├── chatty-trace/ # Research: traces / ATIF / FeedbackFn (AGE-5, ships)
├── chatty-playbook/ # Research: ACE playbook (AGE-17, ships)
├── chatty-flow/ # Research: AFlow WorkflowRepr + IR (AGE-13, ships)
├── chatty-optimize/ # Research: GEPA/AFlow + paired stats + QA loaders
├── hive-client/ # Hive registry client
├── hive-e2e/ # Nightly chatty × Hive contract/trust suite (ignored, run by plugin-e2e.yml)
└── hive-billing-sdk/ # Billing SDK (separate Cargo.lock)
modules/ # Reference WASM agent modules (echo, benford)
docs/ # Deep-dive architecture and design docs
scripts/ # Packaging + setup scripts
Dependency direction: chatty-gpui and chatty-tui depend on
chatty-core. chatty-core never depends on a UI crate. GPUI’s Global
trait impls live behind the gpui-globals feature.
Where things live (cheat sheet)
| You want to… | Look here |
|---|---|
| Add an LLM provider | crates/chatty-core/src/factories/agent_factory/ + ProviderType |
| Add an LLM tool | crates/chatty-core/src/tools/, register in agent_factory.rs |
| Add a service | UI-agnostic → crates/chatty-core/src/services/. UI-specific → crates/chatty-gpui/src/chatty/services/ |
| Add a setting | model in chatty-core/src/settings/models/, repo in chatty-core/src/settings/repositories/, UI in chatty-gpui/src/settings/views/ |
| Add a desktop view | crates/chatty-gpui/src/chatty/views/ — emit events to ChattyApp |
| Add a terminal view | crates/chatty-tui/src/ui/ |
| Add a workspace-global singleton | Define in chatty-core, add impl Global in chatty-core/src/gpui_globals.rs |
| Find all process-wide statics | Singleton inventory at top of crates/chatty-core/src/lib.rs |
| Persist data | JSON repo in chatty-core/src/settings/repositories/, or SQLite in chatty-core/src/repositories/ |
| Add a slash command | crates/chatty-gpui/src/chatty/controllers/app_controller/slash_commands.rs |
Build / test / lint — single source of truth
The CI workflow (.github/workflows/ci.yml)
is the ground truth. If a command is not here, it is not what CI runs.
Docs-only PRs (and other diffs that do not match the Rust path filter in
ci.yml) skip compile, test, and clippy. The required test check still
goes green. Stale CI runs on the same PR are cancelled on the next push.
make setup # one-time: install Linux deps + wasm32-wasip2 target
make build # cargo build (debug)
make test # cargo test --all-features -- --test-threads=1 (matches CI)
make test-fast # cargo test -p chatty-core --lib (quick inner loop)
make test-tui # cargo test -p chatty-tui (TUI changes only)
make test-gpui # cargo test -p chatty-gpui (GPUI changes only)
make test-gateway # cargo test -p chatty-protocol-gateway (gateway changes only)
make lint # cargo clippy --all-features --all-targets -- -D warnings (matches CI)
make fmt # cargo fmt
make fmt-check # cargo fmt --check
make wasm-modules # build every WASM module and test fixture (needed by tests)
make wasm-template # cargo-generate a module from templates/module and build it (needs cargo-generate)
make test-benford # the benford plugin's own unit tests, on the host target
make lint-module-sdk # clippy chatty-module-sdk for wasm32-wasip2
make test-billing-sdk # hive-billing-sdk's tests, on the host target
make docs-gen # regenerate docs/generated reference pages
make docs # sync + build mdBook site (docs-site/book/)
make docs-serve # local preview at http://localhost:3000
make docs-check-links # lychee link check, sources + built site (AGE-117)
make docs-check-nav # INDEX.md + SUMMARY.md drift check (AGE-116)
make docs-check-frontmatter # optional YAML frontmatter schema (AGE-115)
make docs-check-leakage # user guides must not carry contributor material
make docs-check-reference # reference tables match tool_registry.rs and friends
make docs-check # all of the docs checks above
make animations # re-record README/docs GIFs (scripts/animations/README.md)
make ci # everything the Rust CI path runs, locally, in order
Or use cargo directly:
cargo build
cargo test --all-features
cargo fmt --check
cargo clippy --all-features --all-targets -- -D warnings
Test-thread footgun (pdfium fixed, AGE-176; CI/make test still serialize, AGE-600)
chatty-core tests used to intermittently die with SIGTRAP because pdfium,
which is not thread-safe, was driven from several test threads at once
(concurrent FPDF_InitLibrary hits a Chromium CHECK = int3 = SIGTRAP;
concurrent rendering corrupts its heap). create_pdfium() now returns a
PdfiumHandle that holds a process-wide lock for as long as the caller uses
pdfium (crates/chatty-core/src/services/pdfium_utils.rs), so never take a
second handle while holding one on the same thread — that SIGTRAP is fixed.
Despite that fix, ci.yml’s “Run tests” step still passes --test-threads=1
(GitHub-hosted runners have shown other, unrelated intermittent SIGTRAPs
under parallel cargo test), and make test was brought back in line with
it for the same reason (AGE-600) — don’t remove --test-threads=1 from
either as if it were a leftover pdfium workaround. If a SIGTRAP, SIGSEGV or
SIGABRT comes back, look for a new pdfium call that bypasses
create_pdfium() first.
Windows footgun
ci.yml never compiles for Windows on a PR — windows-latest only runs in
warm-release-cache, which is gated if: push && ref == main (a cache
warmer, not a required check). A Windows-only compile error (e.g. code that
should be #[cfg(unix)], like the broker/participant modules, AGE-339) rides
straight to main and isn’t caught until build-windows fails during a
release, shipping an assetless tag. If you touch platform-conditional code,
don’t trust a green PR as proof Windows still builds.
Disk footgun
A full cargo test --all-features needs about 16 GiB of target/ even with
the workspace’s trimmed dependency debuginfo ([profile.dev.package.*] in
the root Cargo.toml); at Cargo’s defaults it needs 28+ GiB and can run a
small disk out of space mid-link. See
docs/build-disk-usage.md before building on a
constrained sandbox or CI runner.
WASM module prebuild
Tests that load real plugins (echo_plugin_e2e, the sandbox, registry and
conformance suites, plugins_headless) read them from target/wasm-fixtures/,
which is build output. Build them (once per checkout) before running the full
test suite; without them those tests fail at once with the path they looked
at and this command:
make wasm-modules # scripts/build-wasm-fixtures.sh; needs the wasm32-wasip2 target
Running
cargo run -p chatty-gpui # desktop app
cargo run -p chatty-tui # terminal app
cargo run -p chatty-tui -- --help # CLI options (headless / pipe / direct providers)
Conventions to follow
These are the patterns the codebase already uses. Follow them; don’t
invent new ones. See CLAUDE.md for full rationale and
examples.
- Event-driven entity communication — Use
EventEmitter+cx.subscribe(). NoArc<dyn Fn>callbacks between entities. Seedocs/entity-communication.md. - Optimistic updates — Update the in-memory global immediately, then persist asynchronously with logged errors.
- Entity refs in globals — Default to
GlobalWeakEntity<T>(avoids circular ownership); useGlobalStrongEntity<T>only when the global must keep the entity alive itself (e.g.StreamManager,ModelsNotifier). - Stream lifecycle — All LLM streams go through
StreamManagerwith cancellation tokens; the stream loop never updates UI directly, it emits events. Streams carry a monotonic epoch so a staleStreamEndedcan’t tear down a newer turn, and a shared stall watchdog (chatty-core/src/services/stream_processor.rs) ends a turn after 180s of silence;--headlessauto-resumes a stalled turn itself, up toHEADLESS_STALL_RESUME_ATTEMPTS(2) times, before failing the run. Seedocs/stream-manager.md. - Turn budget —
TurnBudget(chatty-core/src/services/turn_budget.rs) is a rig hook everystream_promptcall registers: it tells the model how many tool turns are left once most of the budget is spent, and gives it one final tool-free call to answer instead of letting rig’sMaxTurnsErrorswallow an in-progress reply.max_agent_turnsdefaults to0(no cap, not the old 10); an unattended run with no cap (--headless,--pipe, a delegated worker) instead runs under a wall-clock budget,chatty-tui --max-duration <secs|30m|2h>(default 30m). - Error handling — Don’t
.ok()away errors silently. Log aswarn!()for non-critical paths; propagate with?for critical I/O. - Tool errors — Every
impl Tool’s error path must go throughmap_tool_error(tool_name, error)(chatty-core/src/tools/mod.rs), not rig’s defaultTool::map_error, or the model/transcript only ever see the redacted string"the tool failed". See “Tool Error Reporting Pattern” in CLAUDE.md. - MCP API keys — When sending MCP config to the LLM, report
has_api_key(), never theapi_keyfield; a"****"(MASKED_API_KEY_SENTINEL) sent back by the model means “keep the stored value”. See “Security Practices” in CLAUDE.md. - Rust edition — 2024. Use
LazyLock/OnceLock(std) rather thanlazy_static/once_cell. - GPUI / gpui-component skills — When changing desktop UI, load
.claude/skills/gpuiand.claude/skills/gpui-component(vendored fromnpx skills add longbridge/gpui-component; lockfileskills-lock.json). - Desktop screenshot harness — A desktop UI change is not done until it
has been seen running.
.claude/skills/desktop-screenshotstarts the app under Xvfb on an isolated config, drives it with XTest (x.py), and screenshots it; read the PNGs and attach them to the Linear issue. It also lists the gpui traps that only a live run shows. - Transcript blocks — Typed block/turn types in
chatty-gpui/src/chatty/views/transcript/render the transcript; persistence stays untyped (MessageEntry+system_traceJSON) in chatty-core. Don’t leak transcript block types into chatty-core. chatty-tui has its own parallelMessageBlockenum (chatty-tui/src/engine/mod.rs) matching the desktop’s behaviour (activity folds, inline approvals, plan, error, diff) without sharing widgets or a chatty-gpui dependency — see CLAUDE.md. The gpui transcript list uses gpui’slist/ListState(measured heights), notv_virtual_list(predicted heights) — see CLAUDE.md. - Tool failure signal is text, not a flag —
map_tool_error()’sError: {tool_name}: {message}prefix is the only thing that tellsllm_service::tool_result_looks_like_errora tool call failed. Don’t drop or reword that prefix. See CLAUDE.md. - Stale docs — If a change alters a fact a page claims, update that
page in the same PR.
update-agent-docs.ymlonly safety-netsAGENTS.md/CLAUDE.md. See the Documentation section ofCONTRIBUTING.md. - Experiments live in harbor-chatty, not here — pre-registrations, run
reports, spikes, benchmark harnesses and their task fixtures go in the
private
harbor-chattyrepo underexperiments/. chatty2 keeps the research crates’ design notes (docs/research/modules/), process docs, baselines its own benches check against, and a short summary in the user guide for any result a shipped feature relies on. Never commit one-off helper scripts or agent work notes (implementation summaries, plans).
Known gotchas
-
No
chatty-gpui::chatty::*re-exports.crates/chatty-gpui/src/chatty/mod.rsused to re-exportauth,exporters,factories,repositories,toolsfromchatty_core; those re-exports were removed so call sites importchatty_core::…directly. If grep finds no definition underchatty-gpui/, look inchatty-core/.scripts/check-no-core-reexports.sh(in CI) fails the build if they come back. -
Test parallelism. See “Test-thread footgun” above: the pdfium SIGTRAP is fixed, but CI and
make testintentionally still pass--test-threads=1for an unrelated runner flake — that isn’t a regression to “fix”. -
WASM plugin prebuild. The tests that load plugins fail immediately, naming the missing path and
make wasm-modules, if you haven’t built them on this checkout. -
Linux system packages. GPUI needs a long list of
lib*-devpackages. Runmake setup(orscripts/setup-linux.sh) on a fresh machine. -
Standalone crates, own lockfiles, wasm32 by default.
crates/hive-billing-sdk/,modules/benford/andcrates/chatty-module-sdk/each declare their own[workspace]andCargo.lock(intentional) — bump their deps in that lockfile, not the root one.hive-billing-sdkandbenfordalso default towasm32-wasip2via their own.cargo/config.toml, which has no libtest runner, socargo testthere needs an explicit host target:cargo test --manifest-path <crate>/Cargo.toml --target x86_64-unknown-linux-gnu(make test-billing-sdk/make test-benforddo this; CI runs the same, AGE-600). -
The
gpui-globalsfeature. chatty-core types implementgpui::Globalonly when this feature is enabled. chatty-gpui enables it; chatty-tui does not. If aGlobalimpl is missing, add it incrates/chatty-core/src/gpui_globals.rs. -
Sub-agent worktrees. Delegated workers (
invoke_agent→local-agentor a named virtual agent;sub_agentwas retired in AGE-303) each get their owngit worktreeunder<workspace>/.chatty/worktrees/<name>on asub-agent/<name>branch (AGE-314), passed to the child via chatty-tui’s--workspace <DIR>flag.<name>is a wish, not a guarantee: if that branch or directory already exists in the repository (another root process’s worker, or a tree left from an earlier run), the first free<name>-N(from 2) is used instead, and the evidence envelope names the branch actually created (AGE-402). Worktrees are left in place after a worker exits (never auto-removed) and are excluded via.git/info/exclude, not.gitignore— they won’t show ingit statusbut can still accumulate on disk. In a git repository, a worktree that cannot be created now fails the delegation instead of silently sharing the tree; only a workspace that isn’t a git repo falls back to the old shared-tree behavior. See CLAUDE.md. -
Large module directories. Several complex areas have been split into sub-module directories (
chat_view/,chat_input/,auto_updater/,trace_components/,transcript/, etc.). Start with themod.rsand its module-level docstring to scope what you need before loading sibling files. The largest single files aremessage_ops.rs(~1260 lines) andmain.rs(~1225 lines). -
rpptxis pinned to a fork. The rootCargo.tomlpatchesrpptx,oxml-layoutandoxml-pdftoboersmamarcel/rdocxvia[patch.crates-io](AGE-343/AGE-474) — the publishedrpptx = "0.12.1"doesn’t compile alongsideusvgand mishandles real decks.cargo updatewill not move it off the fork; don’t “fix” the patch block without checking whether upstream has actually shipped a release with those fixes. See CLAUDE.md. -
The agent shell loads the login profile, but still starts
--norc --noprofile.shell_serviceruns bash on a PTY (chatty-terminal) and sources/etc/profileand the first of~/.bash_profile/~/.bash_login/~/.profilein its init, before the first prompt (output discarded,PS1unset while it runs), so a project’s conda env orPATHadditions reach the model’s commands. Command completion is read from OSC 133 / OSC 6973 marks in the byte stream, not printed marker lines. If you’re debugging a shell tool that behaves differently interactively vs. under the agent, check what the login profile does, not.bashrc(never sourced). See CLAUDE.md.
Deeper reading
| Topic | File |
|---|---|
| System overview (diagrams) | docs/system-overview.md |
| Component map (diagrams) | docs/component-map.md |
| Crate split rationale | docs/workspace-crate-split.md |
| Stream lifecycle | docs/stream-manager.md |
| Entity communication | docs/entity-communication.md |
| Rendering pipeline | docs/rendering-system.md |
| Token budget | docs/token-tracking.md |
| Agent memory | docs/agent-memory.md |
| Agents, specs & A2A | docs/agents-and-specs.md |
| WASM plugins | docs/plugins.md |
| WIT reference | docs/wit-reference.md |
| Debugging the UI and streams | docs-site/src/dev/guides/debug.md |
| Release process | docs/RELEASE_PROCESS.md |
| Coding patterns & behavior | CLAUDE.md |
Cloud VM notes
Build/test/lint/run commands are unchanged — use the make targets above.
These are only the non-obvious caveats of a headless cloud build VM.
-
Toolchain. The workspace is edition 2024 and declares
rust-version = "1.94"in the rootCargo.toml(the research crates inherit it). If a build fails withfeature edition2024 is required, the VM’s default toolchain is too old: runrustup default stable. CI usesdtolnay/rust-toolchain@stable, so it always runs ahead of the declared MSRV; if a dependency raises the floor, bumprust-versionand name the dependency in the commit. -
Use GNU
cc/c++, not clang. The bundled DuckDB C++ build and the desktop binary link fail under the system clang (fatal error: 'memory' file not found,unable to find library -lstdc++). If you see either, runsudo update-alternatives --set cc /usr/bin/gccandsudo update-alternatives --set c++ /usr/bin/g++. -
The WASM fixtures under
target/wasm-fixtures/are build output and required by some integration tests; runmake wasm-modulesafter a fresh checkout. -
Disk. A full
--all-featurestest build needs about 16 GiB oftarget/; seedocs/build-disk-usage.mdbefore running it on a small VM. -
Running the desktop app headlessly. The VM has no GPU but software Vulkan (Mesa
llvmpipe/lavapipe) works on the existing X11 display (DISPLAY=:1). ExportXDG_RUNTIME_DIRfirst, e.g.export XDG_RUNTIME_DIR=/tmp/xdg-runtime-$(id -u) && mkdir -p "$XDG_RUNTIME_DIR" && chmod 700 "$XDG_RUNTIME_DIR", otherwise X11 window creation fails withBadMatch.chatty-tuineeds none of this. -
No LLM API key is configured. To chat from either frontend, use a local Ollama:
ollama serve(systemd is not running, so launch it in a background/tmux session),ollama pull qwen2.5:0.5b, then./target/debug/chatty-tui --ollama http://localhost:11434 --model qwen2.5:0.5b --headless -m "...". The desktop app auto-detects a running local Ollama and lists its models with no configuration. -
Clippy is clean workspace-wide.
cargo clippy --workspace --all-features --all-targets -- -D warningspasses (AGE-174). CI added--all-targetsso tests/benches/examples are linted too — without it, lints insidetests/go unreported (a finding sat unnoticed inchatty-protocol-gateway’s e2e test until this was added).make lint/make cinow pass--all-targetstoo (AGE-600), so a cleanmake lintmatches CI’s clippy invocation exactly. -
Local rustc lints strictly less than CI’s. This VM’s default toolchain (1.94.1) can be behind the
stableCI uses (e.g. 1.98.1) — clippy findings visible only on newer stable (such as redundant glob imports) can pass locally and fail CI. Runrustup run stable cargo clippy ...before trusting a green local clippy if CI still fails.