Chibi is a minimal, composable building block for LLM interactions — not an agent framework. It provides persistent context storage, extensible behavior via plugins/hooks, and communication primitives. Everything else (coordination patterns, workflows, domain behaviors) lives in plugins. This separation keeps chibi small and enables unlimited experimentation at the plugin layer.
- Establish patterns now that scale well, refactor liberally when beneficial.
- Backwards compatibility not a priority, legacy code unwanted. (Pre-alpha.)
- Focused, secure core. Protect file operations from corruption and race conditions.
- Modular designs, not monolithic. Less code is better code; one pattern is better than two.
- Self-documenting code; keep symbols, comments, and docs consistent.
- Missing or incorrect documentation including code comments are critical bugs.
- Iterate over structures to prevent code duplication.
- Comprehensive tests including edge cases.
cargo build # Debug build
cargo test # Run tests
cargo install --path . # Install to ~/.cargo/binGit dependencies: ratatoskr (LLM API client), streamdown-rs (markdown renderer).
Cargo workspace with four crates:
chibi-core (library)
↑ ↑
chibi-cli (binary) chibi-json (binary)
chibi-mcp-bridge (binary, async daemon)
communicates with chibi-core via JSON-over-TCP
LLM communication is delegated to ratatoskr; gateway.rs bridges chibi's types to ratatoskr's ModelGateway interface. See docs/architecture.md for per-file details, storage layout, and data flow.
- docs/architecture.md — Crate structure, file listings, storage layout, data flow
- docs/plugins.md — Plugin authoring, hooks registration, language plugins
- docs/hooks.md — Hook reference with payloads and examples
- docs/configuration.md — Config options
- docs/contexts.md — Context management
- docs/agentic.md — Agentic workflows, sub-agents, tool output caching
- docs/vfs.md — Virtual file system
- docs/cli-reference.md — CLI flags and usage
ContextEntry.cwdisNonefor contexts created before this field was added;config_resolutionfalls back tostd::env::current_dir()in that case.call_agentis not exposed to the LLM as a callable tool (not inFLOW_TOOL_DEFS). Its constant, metadata, andHandoffTarget::Agentare retained for the fallback tool mechanism, hook overrides, and future inter-agent control transfer.TranscriptEntrynow hasrole: Option<String>andflow_control: bool. Old entries withoutroleuse theto == "user"heuristic inentries_to_messagesfor backwards compat. Prefer builder pattern over struct literals to avoid missing new fields.AppState.stateisArc<RwLock<ContextState>>— use.read().unwrap()/.write().unwrap()guards. Panics only on lock poison (indicates a prior panic, not normal flow).ContextsBackendreads the flock registry directly from disk (not via VFS) to avoid a circular dependency — it is itself a VFS backend.PartitionMeta.prompt_countusesserde(default)— pre-existing manifests without this field deserialise to 0; no backfill of old manifests.ContextsBackendusesPartitionManager::load_with_configforprompt_counton eachstate.jsonread (full active-partition scan). If performance becomes a concern, pass a cachedActiveStateviaload_with_cached_state.- Synthesised tools:
(harness tools)module providescall-toolanddefine-tool.(harness hooks)module providesregister-hook.HARNESS_PREAMBLEdefines%tool-registry%,%hook-registry%,define-tool, andregister-hookat top level (not inside the library) soset!can mutate them and rust can read them post-eval. ToolImpl::Synthesisedhasexec_bindingfield:"tool-execute"for convention format,"%tool-execute-{name}%"fordefine-toolmulti-tool files.reload_tool_from_contentandscan_and_registerrequire&ToolsConfigfor tier resolution. Pass&ToolsConfig::default()when no tier overrides needed.load_tools_from_sourcetakes&ToolsConfig(not a tier param). Tests use&ToolsConfig::default()for sandboxed, orconfig_with_tier(path, 2)for unsandboxed.load_tool_from_source(singular) still takes no config param — uses default internally.ToolCategory::from_category_strmaps category strings from scheme tools to variants. Unknown strings →Synthesised.HttpAllowResult::NeedDeclaredtriggers two-phase context build: phase 1 evaluates source without HTTP to readtool-http-allow, phase 2 rebuilds with trusted prefixes.PreFetchUrlhook fires withsafety: "no_url"andsummaryfield (nourl/reason) for network-category tools without a URL parameter.call-toolbridge uses one global mutex:BRIDGE_CALL_CTX(set/cleared per execute viaCallContextGuard). Registry is embedded inToolImpl::Synthesisedand passed throughexecute_synthesised— no longer a separate global. Reason: tein runs scheme on a dedicated worker thread; thread-locals set on the caller thread would be invisible there.ToolImpl::Synthesisedcarriesregistry: Arc<RwLock<ToolRegistry>>socall-toolcan dispatch to any registered tool from the tein worker thread without thread-local state.- Harness also exposes
%context-name%(mutable binding, injected per call),(generate-id)(8 hex chars, uuid v4), and(current-timestamp)(YYYYMMDD-HHMMzUTC). - Structured tasks replace
todos.md..taskfiles under/home/<ctx>/tasks/and/flocks/<name>/tasks/. Parsed at each prompt bystate::tasks::collect_tasks, ephemeral table injected before last user message.tasks.scmplugin inplugins/provides CRUD tools. /sys/contexts/<name>/task-dirsis a virtual file returning a Scheme list datum (viatein_sexp::Sexp) of all task directories visible to the context. It is the single source of truth for task directory enumeration —tasks.scmreads it viafile_head+read; Rust-sidecollect_tasksusestask_dirs_for()on the sameContextsBackend.Modules::Safeallowlist (tein) includes(scheme base),(scheme write),(scheme read),(scheme char), and other pure modules. Modules withdefault_safe: false(e.g.(scheme regex),(tein modules)) are blocked in the sandboxed tier.execute_hookdispatches to both subprocess plugins and synthesised tein callbacks. Tein dispatch uses a thread-localTEIN_HOOK_GUARD(HashSet<HookPoint>) for re-entrancy prevention — if a tein hook callback triggers the same hook point, tein callbacks are skipped on the recursive call.ToolImpl::Synthesisedcarrieshook_bindings: HashMap<HookPoint, String>mapping hook points to named scheme bindings thatexecute_hookcalls.execute_hookaccepts an optionalTeinHookContext(4th parameter, always present regardless of feature flags). WhenSome, setsCallContextGuardper tein tool during dispatch, enablingcall-tooland(harness io)from hook callbacks. Call sites without full async context (sync lifecycle hooks, indexer, compact) passNone.(harness io)is only available atSandboxTier::Unsandboxed. UsesBRIDGE_CALL_CTX(same mechanism ascall-tool) for runtime context — IO functions only work during active tool execution or hook dispatch withTeinHookContext. VFS operations useVfsCaller::System(bypasses zone permissions). Path dispatch:"vfs://..."→ VFS, bare absolute path →tokio::fs. IO bypasses the hook layer entirely — no hook callbacks fire fromio-write/io-read. Exports:io-read,io-write,io-append,io-list,io-exists?,io-delete.execute_hookdeduplicates tein dispatch by(worker_thread_id, binding)pair. Multi-tool plugins share bindings across all their tools; without dedup, each hook event would fire N times (once per tool). The dedup set is local to eachexecute_hookcall.with_vfs_shadows()is required on the unsandboxedContext::builder()inbuild_tein_context. Without it,(scheme process-context)is missing — blocking(chibi term ansi)→(chibi diff). Added insynthesised.rs.BUILTIN_UNSANDBOXEDinconfig.rs: compile-time list of VFS paths thatresolve_tierdefaults toUnsandboxedwithout user config. Currently:["/tools/shared/history.scm"]. User[tools.tiers]overrides take precedence.PreVfsWrite/PostVfsWritehooks fire only for context-initiated writes going throughsend.rstool dispatch. Writes viaVfsCaller::System/(harness io)bypass the hook dispatch layer entirely.history.scmstores snapshots under<file-dir>/.chibi/history/<filename>/<N>. The.chibi/prefix hides them fromvfs_list(dotfile filter).io-listand direct addressing still reach them. Prunes to 10 revisions by default.scheme_evaltool (eval.rs): persistent sandboxed tein environments keyed by context name inEVAL_CONTEXTS(process-globalLazyLock<Mutex<HashMap>>). Evicted on context clear/destroy/rename viaevict_eval_context()(called fromcontext_ops.rs); next eval lazily recreates.parallel: false— concurrent calls for the same context would collide onBRIDGE_CALL_CTX. Registered viaregister_eval_tools(&Arc<RwLock<ToolRegistry>>)after the registry Arc is created (not with otherregister_*_tools(&mut reg)calls).(tein json)exportsjson-parseandjson-stringify(notjson-read-string).(tein safe-regexp)exportsregexp,regexp-search,regexp-matches?,regexp-replace,regexp-replace-all,regexp-split,regexp-extract,regexp-fold,regexp-match-submatch,regexp-match->list. Requiresregexcargo feature on tein dep.build_sandboxed_harness_context()insynthesised.rsis thepub(crate)bridge foreval.rs— wrapsbuild_tein_context("", Sandboxed)so eval can add its own prelude without duplicating FFI setup.scheme_evalandexecute_synthesisedreturn structured output:"result: <value>\nstdout: <output>\nstderr: <output>". Theresultfield contains the expression's return value (or"error: ..."). stdout/stderr show"(empty)"when nothing was captured.format_eval()stringifies viato_string();format_tool()unwraps scheme strings viaas_string().TeinSession::with_captureuses flush-then-drain-run-flush to isolate each call's output (flush-output-port, R7RS, works in sandboxed contexts). Test helpers: useextract_result_field(output)(splits on"\nstdout: ") to isolate the result value.(harness docs)is the canonical import for harness API discovery:(import (harness docs))then(describe hooks-docs)to list all hook points with payload/return contracts,(module-doc hooks-docs 'pre_message)for a specific hook, or(describe harness-tools-docs)for the harness tool API (define-tool,call-tool,register-hook, etc.). Bothhooks-docsandharness-tools-docsare also available as top-level bindings (pre-imported inEVAL_PRELUDE) but(harness docs)is the documented access path.describetakes an alist directly — NOT a symbol.hooks-docsis generated at startup fromHOOK_METADATA(hooks.rs) — the single source of truth for all hook contracts.docs/hooks.mdhook reference is also generated from it viajust generate-docs. Adding aHookPointvariant without aHOOK_METADATAentry failstest_hook_metadata_completeness.(module-exports '(harness docs))(and'(harness tools),'(harness hooks)) errors — runtime-registered modules are absent from tein's build-timeMODULE_EXPORTStable. Useharness-tools-docsandhooks-docsfor API discovery instead.insert_symbols(indexer.rs) now does a two-pass insert for parent resolution: first pass inserts all symbols withparent_id = NULL, second pass resolvesparentnames via line-range containment (smallest enclosing range wins). Plugins that don't emitparentare unaffected.- Language plugins (e.g.
lang_rust):tree-sitter-rustexposes visibility as avisibility_modifierchild kind, not a named field —child_by_field_name("visibility")returnsNone. Usenode.children().find(|n| n.kind() == "visibility_modifier")instead. Also,use_wildcardnodes contain the full path text (e.g."std::collections::*"), not just"*"— take the full node text rather than constructingprefix + "::*".