# PicoBot ## Build & Run - `cargo build` — build the binary - `cargo run -- gateway` — start gateway server (binds `127.0.0.1:19876` by default) - `cargo run -- chat` — connect to gateway as CLI client (default `ws://127.0.0.1:19876/ws`) ## Config - Config load order: `~/.picobot/config.json` then fallback to `./config.json` (`src/config/mod.rs:237-267`) - `.env` (cwd) is loaded with a custom parser, not via dotenv crate; env var placeholders `` in config JSON are substituted - Config example: `config.example.json` - `session_ttl_hours` defaults to 4 in code when absent (`src/gateway/mod.rs:44`); config.example.json shows 168 as a suggestion ## Tests - `cargo test --lib` — run unit tests (runs all `#[test]` in `src/`) - `cargo test --test test_integration -- --ignored` — run integration tests (also `test_tool_calling`, `test_request_format`) - **All** integration tests require `tests/test.env` with real API keys; copy from `tests/test.env.example` and fill in keys - Integration tests are `#[ignore]` by default; use `-- --ignored` to run them ## Reference - `reference/` — third-party reference implementations (nanobot, Mini-Agent, zeroclaw); not part of this project; do not modify ## Architecture ### Modes - **Gateway mode** (`cargo run -- gateway`): HTTP/WebSocket server; owns `GatewayState` which holds all services - **Client mode** (`cargo run -- chat`): TUI chat client; connects to gateway via WebSocket, purely for user interaction ### Core Data Flow ``` Channel → MessageBus → SessionManager → AgentLoop → (tools) → SessionManager → MessageBus → OutboundDispatcher → Channel ↑ ControlChannel ──→ SessionManager (dialog ops: create/switch/archive/delete) ``` ### Modules | Module | Responsibility | Key Types | |--------|---------------|-----------| | `gateway` | Server lifecycle, HTTP/WS endpoints, owns `GatewayState` | `GatewayState`, `run()` | | `client` | TUI rendering, WebSocket client for CLI chat | `App`, `run()` | | `channels` | External integrations (Feishu, CLI chat) | `ChannelManager`, `Channel` trait | | `bus` | Async message queue (inbound/outbound/control channels) | `MessageBus`, `InboundMessage`, `OutboundMessage`, `ControlMessage` | | `session` | Conversation session lifecycle, dialog operations | `SessionManager`, `Session` | | `agent` | LLM call loop, tool execution, context compression | `AgentLoop` | | `providers` | LLM API clients (OpenAI-compatible, Anthropic) | `LLMProvider` trait, factory `create_provider()` | | `tools` | Agent tools (bash, file ops, http, web, get_skill) | `ToolRegistry`, `Tool` trait | | `skills` | Skills loading, management, and prompt building | `SkillsLoader`, `Skill` | | `storage` | SQLite persistence for sessions and messages | `Storage`, `SessionMeta`, `MessageMeta` | | `scheduler` | Cron-based job scheduling, next-run computation | `Scheduler`, `Schedule`, `next_run_for_schedule()` | | `observability` | Observer pattern for agent/tool telemetry events | `Observer` trait, `ObserverEvent`, `MultiObserver` | | `protocol` | WebSocket protocol message types | `WsInbound`, `WsOutbound`, `SessionSummary` | | `config` | Config loading, env substitution, path resolution | `Config`, `LLMProviderConfig` | | `logging` | Tracing initialization with file rotation | `init_logging()`, `init_logging_console_only()` | ### Functional Boundaries - **Channels** only send/receive messages via `MessageBus`; they know nothing about sessions or LLM - **MessageBus** is a pure async queue; it routes nothing, just passes messages - **SessionManager** owns session state and dialog operations; it does NOT call LLM directly - SessionManager is responsible for injecting skills prompt into conversation history - **AgentLoop** receives dialog events from `SessionManager`, calls LLM via `providers`, executes tools, returns text responses - AgentLoop is stateless; all state is managed by Session/SessionManager - **Providers** are pure HTTP clients; no bus/session/channel awareness - **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results ### Key Constraints - Gateway **changes working directory** to workspace on startup (`src/gateway/mod.rs:31`) - Session/message persistence uses SQLite via `sqlx`; DB stored in workspace as `.picobot_sessions.db` by default - `ChannelManager` owns the `MessageBus` and all channel instances - `OutboundDispatcher` routes outbound messages to the correct channel via `ChannelManager` - Config `.env` loading uses `unsafe { env::set_var(...) }` — don't refactor to safer patterns without understanding side effects