- AgentCatalog/definitions with strict Markdown frontmatter, delegation graph, fail-closed tool scoping, and signal contracts - structured cancellation (AgentError::Cancelled/TimedOut) across provider streams, tool batches, and sleep; /stop drives the same terminal state - schema v6 run/group/inbox persistence with execution-ID conditional transitions and completion-slot reservations - ExecutionGate separating run quota from provider/tool step permits - background completion inbox with hidden-trigger continuation turns, fairness scheduling, lease release, dead-lettering, and activation recovery - typed TurnMailbox with two-phase steer admission and atomic consumption at turn commit; /stop releases admitted steer events back to pending - emit_signal tool with contract-enforced rate/dedupe/severity/size limits - WS run/event projection (GetAgentRuns, AgentRunUpdated, AgentEventUpdated), /api/agent-runs* management endpoints, /api/tasks union, WebUI run tree and signal cards - ChannelContext.durable_private persisted for continuation delivery reuse Version 1.7.0
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; when orchestration is enabled, candidate preparation strictly validates trusted Markdown definitions, Provider profiles, delegated tools, Skill allowlists, and delegation edges before activation. Named Agents currently support foreground execution; durable named background delivery is not available until the run/inbox phase lands
- 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 fails closed: new tools default to
RootOnly; only code-reviewedDelegatabletools may appear in Agent Markdown, whiledelegateand scopedget_skillare runtime-injected. Call parameters cannot expand a named Agent's tool set - Sleep tool is a cancellable foreground wait bounded to 24 hours; longer or durable delays belong to Scheduler/background work, and cancelling a Turn must normalize active tool blocks to
Cancelled - 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修改增加末端数字。 注意版本号变更是提交时和仓库中的版本比较,不要在长时间的工程中,不断变化版本号。