Drop the model-callable sleep tool and its TurnWakeup publisher/handle state. Async background completions and user input already inject through steer-at-safe-boundary or the queued continuation Turn, so the sleep path only misled agents into busy-waiting on non-actionable queue wakes. Cancellation still normalizes running tool blocks to Cancelled.
18 KiB
18 KiB
PicoBot
This file is the operational contract for coding agents working in this repository. Read docs/ARCHITECTURE.md before making architectural or lifecycle changes. Code and tests are the final source of truth; update the document when an architectural invariant changes.
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)cargo run -- run "prompt"— send one prompt through Gateway, print the terminal Turn, and exit; stdin, JSON, verbose progress, and timeout modes are availablecargo run -- reload— validate and gracefully reload a running Gateway's configurationcargo run -- health [--json]— check core, configuration-dependent, and optional runtime dependencies without starting Gatewaydocker compose up -d— start the container with Gateway bound/published on0.0.0.0:19876; overridePICOBOT_GATEWAY_HOST,PICOBOT_PUBLISH_HOST, orPICOBOT_GATEWAY_PORTas needed- WebUI — start Gateway, then open
http://127.0.0.1:19876/; no separate frontend build is required cd webui && npm ci && npm run check && npm run build— validate the Svelte WebUI independently (Node.js 20+); its localdist/is ignoredcargo buildautomatically runs an incremental WebUI production build into CargoOUT_DIR; it runsnpm cionly whenpackage-lock.jsonis not represented by the installed dependency stamppicobot service install|start|stop|status|restart|uninstall— manage the Linux systemd user service (picobot.service)
Config
- Config load order:
~/.picobot/config.jsonthen fallback to./config.json(Config::load_defaultinsrc/config/mod.rs) .envfiles use a custom parser, not dotenv: load<config-dir>/.env, then<workspace_dir>/.env, while pre-existing process variables remain highest priority; config placeholders<VAR_NAME>use the merged values- Config example:
resources/templates/config.example.json(released to~/.picobot/on first run) - CLI TUI identity is stored in
~/.picobot/tui_client_id; it is a non-secret stable chat scope used to restore dialogs across reconnects - One-shot
runuses a unique chat scope per invocation; for loopback Gateway URLs it authenticates/wswith~/.picobot/web_admin_token, while remote URLs use the existing paired CLI token
Tests
cargo test --lib— run unit tests (runs all#[test]insrc/)cargo clippy --all-targets --all-features -- -D warnings— required for Rust changescargo test --test test_schedulerandcargo test --test test_request_format— offline integration/protocol testscargo test --test test_integration -- --ignoredandcargo test --test test_tool_calling -- --ignored— model API integration tests- API-backed tests require
tests/test.envwith real API keys; copy fromtests/test.env.exampleand fill in keys - API-backed tests are
#[ignore]by default; use-- --ignoredto run them
Reference
reference/— third-party reference implementations; 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 - One-shot client mode (
cargo run -- run "prompt"): isolated CLI chat scope; connects to Gateway, waits for a terminal Turn, prints it, and exits
Core Data Flow
Channel → MessageBus.inbound → Gateway inbound router/lane → SessionManager → per-session worker → AgentLoop
↑ │
└── Channel ← OutboundDispatcher ← per-conversation lane ← MessageBus.outbound
AgentLoop → TurnEvent → Session TurnController → latest TurnSnapshot → DeliveryCoordinator → per-turn TurnSink → Channel
WebSocket/Channel → MessageBus.control → Gateway control router → SessionManager (dialog operations)
Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler delivery policy → SessionManager/MessageBus
Modules
| Module | Responsibility | Key Types |
|---|---|---|
gateway |
Server lifecycle, HTTP/WS/WebUI 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 |
Bounded async queues and ordered outbound delivery lanes | MessageBus, OutboundDispatcher, InboundMessage, OutboundMessage, ControlMessage |
session |
Conversation lifecycle, dialog operations, per-session serialization, Turn state, persistence coordination | SessionManager, Session, TurnController |
agent |
LLM call loop, tool execution, context compression, semantic Turn events | AgentLoop, TurnEvent |
providers |
Native LLM streams normalized into text/reasoning/tool/usage chunks | LLMProvider, ProviderChunk, create_provider() |
delivery |
Snapshot projection, latest-wins throttling, terminal retry, per-turn sink lifecycle | DeliveryCoordinator, TurnDeliveryService, PresentationPolicy |
tools |
Agent tools and external adapters (bash, files, HTTP, browser, health) | ToolRegistry, Tool, ToolExecutionContext |
health |
Shared read-only dependency diagnostics for CLI/tool/slash entry points | HealthService, HealthReport |
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() |
work |
Session-scoped active plan and concurrently executable checklist items | WorkManager, TaskPlan, TaskItem |
observability |
Observer pattern for agent/tool telemetry events | Observer trait, ObserverEvent, MultiObserver |
protocol |
WebSocket protocol message types | WsInbound, WsOutbound, SessionSummary, HistoryMessage |
config |
Config loading, env substitution, path resolution | Config, LLMProviderConfig |
logging |
Tracing initialization with file rotation | init_logging(), init_logging_console_only() |
task_supervisor |
Owns, cancels, and boundedly joins gateway background tasks | TaskSupervisor |
Functional Boundaries
- Channels publish inbound messages through
MessageBus; outbound writes arrive throughOutboundDispatcheror a per-turnTurnSink. They know nothing about sessions or LLM - Inbound contract carries normalized sender/time/media plus
ChannelContext; core routing may interpretreply_tobut must treat platform-private context as opaque reply data - MessageBus owns bounded inbound/outbound/control queues; outbound routing and retries belong to
OutboundDispatcher, not the queue - SessionManager owns session state, dialog operations, context construction, per-session work queues, active-Turn steering admission, and persistence coordination
- TurnController is the only owner of active Turn state; snapshots are complete latest-wins values, not token queues, and
Completedis published only after atomic persistence succeeds - DeliveryCoordinator projects active Turn snapshots without mutating history; it owns
TurnSinklifecycle but no platform message IDs, which remain private to each sink - WorkManager owns the single active plan per session, item state transitions, plan versions, and plan-change events; plans are optional and absent from ordinary chat context
- Scheduler supports legacy direct-delivery jobs and managed
task/monitorjobs; managed agents cannot callsend_message, andon_alertsuppresses only healthy informational results - AgentLoop is stateless across turns; it receives prepared history, drains same-Turn steering only at safe model boundaries, calls LLM providers, executes tools, and returns one result
- AgentCatalog is immutable per runtime generation; candidate preparation strictly validates trusted Markdown definitions, Provider profiles, tool/Skill allowlists, and delegation edges before activation. Sub-Agent orchestration is an intrinsic, always-on mechanism (no feature switch). Named Agents support foreground (single/batch) and Root-initiated single background execution with durable run/inbox delivery; a built-in general-purpose definition is released to
~/.picobot/agents/on first run. Background batches and nested background remain restricted - WebUI management APIs only expose allowlisted config/profile/log/storage operations; keep response limits, secret redaction, atomic config writes, and profile path allowlists intact
- WebUI styling uses the local Fluent 2 semantic tokens in
webui/src/styles.css; page and component styles should consume the aliases instead of introducing independent hard-coded palettes, and must preserve selectable light/dark and brand-color themes, keyboard focus, responsive layout, and reduced-motion behavior. Browser-only appearance settings belong inlib/theme.js/localStorage, andpublic/theme-init.jsmust restore them before Svelte mounts - Gateway config reload validates and prepares a complete next runtime generation before retiring the old one; close runtime admission before draining inbound lanes, Sessions, Scheduler jobs, and background sub-agents; MCP connects only during activation; host/port, workspace, and effective storage path remain restart-only, and runtime reload must never mutate process environment variables
- WebUI slash completion must consume the existing
get_slash_commandsWebSocket response; do not duplicate the backend command list in frontend source - Session token statistics persist Provider-reported usage atomically with each completed Turn; WebUI
session_statsand/info [--json]must consume the same SessionStats projection, and context occupancy must use the final request's prompt usage rather than accumulated Turn totals - WebUI chat rendering sanitizes Markdown before inserting HTML; durable
turn_committeddeltas calibrate normal terminal Turns without a full history reload, and history must preserve structured tool-call metadata so calls and results remain independently collapsible - WebUI/TUI file transfer streams bytes over authenticated HTTP and sends only short-lived upload IDs/attachment metadata over WebSocket; messages persist local media paths without guaranteeing later availability, and client responses must never expose those paths
- WebUI/TUI same-turn media delivery stages same-session
send_message(files=...)media on the active Turn and commits it on the final assistant message, after durable tool-call history; safe raster formats should render as an inline preview with download fallback - WebUI authentication protects every management API and
/ws; only static pairing assets, public health/status, and pairing submission may bypass device auth. Pair-code issuance requires both a real loopback peer and the filesystem-held admin token. The same conjunction may authenticate only/wsfor local one-shotrun; it must never authorize management APIs. Never put bearer or admin tokens in URLs or logs - Providers are pure HTTP clients; no bus/session/channel awareness
- Provider reasoning state is private replay data: persist it, replay it only to the matching provider, and never expose it to clients, channels, or logs
- Tools are executed by
AgentLoop; every invocation is normalized toToolOutputand passes throughToolOutputProcessor. PlainToolResultimplementations use the default conversion, while artifact-producing tools declare model/user audience explicitly; model capability checks, final-reply attachment, channel delivery, and Provider serialization stay outside tools - Delegated tool access: a named Agent's tool set is decided solely by its definition file (admin-authored).
delegate,emit_signal,get_skillandagent_taskare runtime-injected and must never be declared intools(get_skillis the scoped-skill switch);allowed_toolscan only narrow the definition, never expand it - No foreground wait tool: Agents wait for asynchronous work by ending the Turn and letting queued completions/signals open a continuation Turn, or by polling status tools; there is no model-callable
sleep/wait tool. Cancelling a Turn must still normalize active tool blocks toCancelled - Stateful tools receive
ToolExecutionContext; browser calls withoutpersistent_idmap each PicoBot dialog to an opaque transient agent-browser session. For long-running work an Agent may autonomously create a persistent identity and must pass its validated ID on every related action; the same ID shares one agent-browser session and serialization gate across dialogs, while different IDs have independent sessions/gates.browser_profilesmay create IDs, persist bounded semantic labels, list, or delete only validated IDs beneath the configured profile root. Do not introduce a global persistence mode switch, a default persistent ID, automatic per-dialog persistent Profiles, Fantoccini, ChromeDriver, WebDriver, model-controlled raw agent-browser session IDs, or arbitrary Profile paths - Health diagnostics are read-only and share
HealthServiceacrosspicobot health, thehealthtool, and/health; checks must not install/fix dependencies, call Provider APIs, or expose secrets
Concurrency and Lifecycle Invariants
- One session runs at most one Turn; ordinary input steers its active Turn by default,
/queueexplicitly waits for the next Turn, and different sessions may run concurrently - Steering admission, final close, fallback, and
/stopmust be lossless and mutually exclusive: an input belongs to exactly the active Turn or the next-Turn FIFO, while/stopintentionally discards both - Outbound messages are ordered per
(channel, chat_id); a slow destination must not block unrelated destinations - Active Turn delivery and ordinary outbound delivery share the same per-
(channel, chat_id)write lock; never enqueue token deltas into MessageBus - Slow Turn consumers may skip intermediate snapshots but must receive an explicit bounded terminal delivery; shutdown must call sink abort so platform cleanup remains possible
- Never hold a Session mutex across model, network, or database I/O unless a documented invariant requires it
- Slow work derived from session state must validate
worker_generation/state_versionbefore committing results - A same-session
send_messagewrite may avoid advancingstate_versiononly when its task-local Turn ID still owns that session's active Turn; all other writes remain versioned - Related durable mutations use Storage transaction APIs; persistence failure must not leave silent memory/database divergence
- Long-lived gateway tasks must be owned by
TaskSupervisor; connection-local tasks must be explicitly joined or aborted by their owner - Connection, retry sleep, queue wait, and shutdown join paths must observe cancellation and have hard time bounds
- Retry only errors explicitly classified as transient; permanent failures must surface immediately
- Never log secrets, authorization headers, or full connection URLs containing temporary credentials
- The workspace cwd is not a filesystem sandbox: default file tools accept absolute paths and Bash inherits process permissions; add explicit canonical-path/process isolation when a hard boundary is required
Key Constraints
- Gateway changes working directory to workspace in
GatewayState::new(src/gateway/mod.rs) - 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- Layered config/workspace
.envloading usesunsafe { env::set_var(...) }during single-threaded startup — don't move it after Gateway tasks are spawned or refactor it without understanding process-wide side effects
Change Workflow
- Inspect the relevant implementation, tests, and
docs/ARCHITECTURE.md; do not rely on names or old reports alone. - Search
reference/only for comparison. Never edit it or copy behavior without checking PicoBot's boundaries. - Preserve unrelated user changes in a dirty worktree. Use
rgfor search andapply_patchfor edits. - Add regression tests for bugs, especially cancellation, timeout, queue saturation, stale state, persistence failure, and retry classification.
- For WebUI changes run
npm run checkandnpm run buildinwebui/, then runcargo buildto verify theOUT_DIRembedding path. Do not commit generateddist/; verify there is no external runtime dependency and keep browser chat on the existing/wsprotocol. - For Rust changes run targeted tests,
cargo test --lib, Clippy with warnings denied, andcargo build. Integration tests require real credentials. - For documentation-only changes verify links, commands, paths, and
git diff --check. - Update README, this file, and the architecture document together when public behavior or an architectural invariant changes.
Documentation Roles
README.md— user-facing overview, setup, capabilities, and navigationdocs/ARCHITECTURE.md— maintainer-facing runtime design, invariants, lifecycle, and extension guidanceAGENTS.md— concise operational rules for repository agentsresources/skills/about-picobot/references/— runtime knowledge shipped to PicoBot; update it only when the assistant's built-in product knowledge must change
Version Management
- 在每次功能变化、架构变化后,适当地更新整个产品的版本号。功能变化增加中段数字,ui变化、bug修改增加末端数字。 注意版本号变更是提交时和仓库中的版本比较,不要在长时间的工程中,不断变化版本号。