4.6 KiB
4.6 KiB
PicoBot
Build & Run
cargo build— build the binarycargo run -- gateway— start gateway server (binds127.0.0.1:19876by default)cargo run -- chat— connect to gateway as CLI client (defaultws://127.0.0.1:19876/ws)
Config
- Config load order:
~/.picobot/config.jsonthen 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<VAR_NAME>in config JSON are substituted- Config example:
config.example.json session_ttl_hoursdefaults 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]insrc/)cargo test --test test_integration -- --ignored— run integration tests (alsotest_tool_calling,test_request_format)- All integration tests require
tests/test.envwith real API keys; copy fromtests/test.env.exampleand fill in keys - Integration tests are
#[ignore]by default; use-- --ignoredto 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; ownsGatewayStatewhich 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 viaproviders, 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 aspicobot.dbby default ChannelManagerowns theMessageBusand all channel instancesOutboundDispatcherroutes outbound messages to the correct channel viaChannelManager- Config
.envloading usesunsafe { env::set_var(...) }— don't refactor to safer patterns without understanding side effects