Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AGENTS.md

Research crates carry reserved symbols; read RESERVED.md before 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 providercrates/chatty-core/src/factories/agent_factory/ + ProviderType
Add an LLM toolcrates/chatty-core/src/tools/, register in agent_factory.rs
Add a serviceUI-agnostic → crates/chatty-core/src/services/. UI-specific → crates/chatty-gpui/src/chatty/services/
Add a settingmodel in chatty-core/src/settings/models/, repo in chatty-core/src/settings/repositories/, UI in chatty-gpui/src/settings/views/
Add a desktop viewcrates/chatty-gpui/src/chatty/views/ — emit events to ChattyApp
Add a terminal viewcrates/chatty-tui/src/ui/
Add a workspace-global singletonDefine in chatty-core, add impl Global in chatty-core/src/gpui_globals.rs
Find all process-wide staticsSingleton inventory at top of crates/chatty-core/src/lib.rs
Persist dataJSON repo in chatty-core/src/settings/repositories/, or SQLite in chatty-core/src/repositories/
Add a slash commandcrates/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(). No Arc<dyn Fn> callbacks between entities. See docs/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); use GlobalStrongEntity<T> only when the global must keep the entity alive itself (e.g. StreamManager, ModelsNotifier).
  • Stream lifecycle — All LLM streams go through StreamManager with cancellation tokens; the stream loop never updates UI directly, it emits events. Streams carry a monotonic epoch so a stale StreamEnded can’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; --headless auto-resumes a stalled turn itself, up to HEADLESS_STALL_RESUME_ATTEMPTS (2) times, before failing the run. See docs/stream-manager.md.
  • Turn budget — TurnBudget (chatty-core/src/services/turn_budget.rs) is a rig hook every stream_prompt call 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’s MaxTurnsError swallow an in-progress reply. max_agent_turns defaults to 0 (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 as warn!() for non-critical paths; propagate with ? for critical I/O.
  • Tool errors — Every impl Tool’s error path must go through map_tool_error(tool_name, error) (chatty-core/src/tools/mod.rs), not rig’s default Tool::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 the api_key field; 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 than lazy_static/once_cell.
  • GPUI / gpui-component skills — When changing desktop UI, load .claude/skills/gpui and .claude/skills/gpui-component (vendored from npx skills add longbridge/gpui-component; lockfile skills-lock.json).
  • Desktop screenshot harness — A desktop UI change is not done until it has been seen running. .claude/skills/desktop-screenshot starts 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_trace JSON) in chatty-core. Don’t leak transcript block types into chatty-core. chatty-tui has its own parallel MessageBlock enum (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’s list/ListState (measured heights), not v_virtual_list (predicted heights) — see CLAUDE.md.
  • Tool failure signal is text, not a flag — map_tool_error()’s Error: {tool_name}: {message} prefix is the only thing that tells llm_service::tool_result_looks_like_error a 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.yml only safety-nets AGENTS.md / CLAUDE.md. See the Documentation section of CONTRIBUTING.md.
  • Experiments live in harbor-chatty, not here — pre-registrations, run reports, spikes, benchmark harnesses and their task fixtures go in the private harbor-chatty repo under experiments/. 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

  1. No chatty-gpui::chatty::* re-exports. crates/chatty-gpui/src/chatty/mod.rs used to re-export auth, exporters, factories, repositories, tools from chatty_core; those re-exports were removed so call sites import chatty_core::… directly. If grep finds no definition under chatty-gpui/, look in chatty-core/. scripts/check-no-core-reexports.sh (in CI) fails the build if they come back.

  2. Test parallelism. See “Test-thread footgun” above: the pdfium SIGTRAP is fixed, but CI and make test intentionally still pass --test-threads=1 for an unrelated runner flake — that isn’t a regression to “fix”.

  3. 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.

  4. Linux system packages. GPUI needs a long list of lib*-dev packages. Run make setup (or scripts/setup-linux.sh) on a fresh machine.

  5. Standalone crates, own lockfiles, wasm32 by default. crates/hive-billing-sdk/, modules/benford/ and crates/chatty-module-sdk/ each declare their own [workspace] and Cargo.lock (intentional) — bump their deps in that lockfile, not the root one. hive-billing-sdk and benford also default to wasm32-wasip2 via their own .cargo/config.toml, which has no libtest runner, so cargo test there needs an explicit host target: cargo test --manifest-path <crate>/Cargo.toml --target x86_64-unknown-linux-gnu (make test-billing-sdk / make test-benford do this; CI runs the same, AGE-600).

  6. The gpui-globals feature. chatty-core types implement gpui::Global only when this feature is enabled. chatty-gpui enables it; chatty-tui does not. If a Global impl is missing, add it in crates/chatty-core/src/gpui_globals.rs.

  7. Sub-agent worktrees. Delegated workers (invoke_agent → local-agent or a named virtual agent; sub_agent was retired in AGE-303) each get their own git worktree under <workspace>/.chatty/worktrees/<name> on a sub-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 in git status but 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.

  8. 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 the mod.rs and its module-level docstring to scope what you need before loading sibling files. The largest single files are message_ops.rs (~1260 lines) and main.rs (~1225 lines).

  9. rpptx is pinned to a fork. The root Cargo.toml patches rpptx, oxml-layout and oxml-pdf to boersmamarcel/rdocx via [patch.crates-io] (AGE-343/AGE-474) — the published rpptx = "0.12.1" doesn’t compile alongside usvg and mishandles real decks. cargo update will 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.

  10. The agent shell loads the login profile, but still starts --norc --noprofile. shell_service runs bash on a PTY (chatty-terminal) and sources /etc/profile and the first of ~/.bash_profile/~/.bash_login/~/.profile in its init, before the first prompt (output discarded, PS1 unset while it runs), so a project’s conda env or PATH additions 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

TopicFile
System overview (diagrams)docs/system-overview.md
Component map (diagrams)docs/component-map.md
Crate split rationaledocs/workspace-crate-split.md
Stream lifecycledocs/stream-manager.md
Entity communicationdocs/entity-communication.md
Rendering pipelinedocs/rendering-system.md
Token budgetdocs/token-tracking.md
Agent memorydocs/agent-memory.md
Agents, specs & A2Adocs/agents-and-specs.md
WASM pluginsdocs/plugins.md
WIT referencedocs/wit-reference.md
Debugging the UI and streamsdocs-site/src/dev/guides/debug.md
Release processdocs/RELEASE_PROCESS.md
Coding patterns & behaviorCLAUDE.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 root Cargo.toml (the research crates inherit it). If a build fails with feature edition2024 is required, the VM’s default toolchain is too old: run rustup default stable. CI uses dtolnay/rust-toolchain@stable, so it always runs ahead of the declared MSRV; if a dependency raises the floor, bump rust-version and 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, run sudo update-alternatives --set cc /usr/bin/gcc and sudo update-alternatives --set c++ /usr/bin/g++.

  • The WASM fixtures under target/wasm-fixtures/ are build output and required by some integration tests; run make wasm-modules after a fresh checkout.

  • Disk. A full --all-features test build needs about 16 GiB of target/; see docs/build-disk-usage.md before 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). Export XDG_RUNTIME_DIR first, 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 with BadMatch. chatty-tui needs 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 warnings passes (AGE-174). CI added --all-targets so tests/benches/examples are linted too — without it, lints inside tests/ go unreported (a finding sat unnoticed in chatty-protocol-gateway’s e2e test until this was added). make lint/make ci now pass --all-targets too (AGE-600), so a clean make lint matches 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 stable CI uses (e.g. 1.98.1) — clippy findings visible only on newer stable (such as redundant glob imports) can pass locally and fail CI. Run rustup run stable cargo clippy ... before trusting a green local clippy if CI still fails.