starpod-agent
The orchestrator crate that wires all subsystems together. Provides StarpodAgent — the central type for all chat interactions.
API
let config = load_agent_config(&paths)?;
let agent = StarpodAgent::new(config).await?;
// Non-streaming chat
let response = agent.chat(ChatMessage {
text: "Hello!".into(),
user_id: None,
channel_id: Some("main".into()),
channel_session_key: Some("session-uuid".into()),
attachments: vec![],
}).await?;
// Streaming chat
let (stream, session_id, followup_tx, out_attachments) = agent.chat_stream(&message).await?;
// stream is a Query (tokio Stream of Message)
// followup_tx can inject messages into the running agent loop
// out_attachments: Arc<Mutex<Vec<Attachment>>> — files queued by the Attach tool
// Finalize after streaming
agent.finalize_chat(&session_id, &user_text, &result_text, &result, None).await;
// Deliver accumulated attachments to the user's channel
let attachments = out_attachments.lock().await.drain(..).collect::<Vec<_>>();Chat Pipeline
- Snapshot config — take a cheap clone of the current config (supports hot reload)
- Resolve channel — map
ChatMessage→(Channel, key) - Resolve session — find or create session via
SessionManager; export closed session transcript to memory if applicable - Bootstrap context — memory files + daily logs
- Build system prompt — identity + context + skills + tools + time
- Build provider — construct
LlmProviderfromconfig.provider(anthropic, openai, gemini, groq, deepseek, openrouter, ollama) - Run agent-sdk query — agentic loop with custom tools via the selected provider + automatic conversation compaction
- Drain followup messages — at each iteration boundary, inject any queued user messages (when
followup_mode = "inject") - Self-improve reflection — if
self_improveis enabled and conditions are met (skill failure or 5+ tool calls), run a follow-up query to create/update skills - Record usage — tokens and cost to session database
- Append daily log — conversation summary
- Background nudge — if message count hits the nudge interval, spawn a background review (memory + skills when self-improve is on). Un-nudged sessions are also flushed when the user switches to a different session or when a session closes, so short conversations are never lost
Followup Message Handling
When a user sends a message while a stream is active, behavior depends on followup_mode:
inject(default) — Messages are sent through a channel and drained at the next agent loop iteration boundary (before the next API call). Multiple rapid messages are batched into a single user message.queue— Messages are buffered. After the current stream finishes, all queued messages are combined and dispatched as a new agent loop.
Conversation compaction is enabled by default with a 160k token context budget. The compaction model is configurable via compaction_model in agent.toml (defaults to the primary model).
Custom Tools (21)
| Category | Tools |
|---|---|
| Memory | MemorySearch, MemoryWrite, MemoryAppendDaily |
| Environment | EnvGet |
| Files | FileRead, FileWrite, FileList, FileDelete, Attach |
| Skills | SkillActivate, SkillCreate, SkillUpdate, SkillDelete, SkillList |
| Cron | CronAdd, CronList, CronRemove, CronRuns, CronRun, CronUpdate |
| Heartbeat | HeartbeatWake |
Scheduler Integration
let agent = Arc::new(agent);
let handle = agent.start_scheduler(Some(notifier));
// Runs in background, executing due cron jobs through agent.chat()Config Hot Reload
The agent's config is wrapped in RwLock for hot reload support. Each request snapshots the config at the start, so config changes take effect on the next request.
// Reload config (called by the gateway's file watcher)
agent.reload_config(new_config);
// Get current config snapshot
let config = agent.config(); // returns owned StarpodConfigComponent Accessors
agent.memory() // &Arc<MemoryStore>
agent.session_mgr() // &Arc<SessionManager>
agent.skills() // &Arc<SkillStore>
agent.cron() // &Arc<CronStore>
agent.config() // StarpodConfig (owned snapshot)Memory Nudging
Memory persistence operates at two levels:
System prompt guidance — The system prompt includes always-on instructions that guide the agent to proactively persist knowledge (user corrections, preferences, environment facts) via MemoryWrite and MemoryAppendDaily tools during normal conversation. This is core agent behavior, not gated behind self_improve.
Background nudge — Every memory.nudge_interval user messages (default: 10), a background tokio::spawn task reviews the conversation and persists important information automatically. This catches details the agent may have missed during inline conversation.
The nudge pipeline:
StarpodAgenttracks a per-session message counter (nudge_counters)- When
count % nudge_interval == 0,maybe_nudge_memory()loads the session transcript - A background task calls
nudge::run_nudge()which:- Converts
SessionMessagerecords into a transcript (truncated to 30K chars) - Makes a single non-streaming LLM call with
MemoryWrite/MemoryAppendDailytools - Executes any tool calls from the response (reuses
flush::execute_flush_tool_calls) - Discards the LLM's text output
- Converts
Model resolution: memory.nudge_model → compaction.flush_model → compaction_model → primary model. The nudge is fail-open — provider errors are logged and the chat flow is never interrupted.
Counters are evicted when a session closes (alongside the bootstrap cache).
Self-Improve Mode
When self_improve = true, a SelfImproveTracker is attached to the tool handler. It tracks:
- Which skill was activated (via
SkillActivate) - How many tool calls returned errors
- Total tool call count
- Whether
SkillCreate/SkillUpdatewas already called
After the main agent loop, run_self_improve_reflection() checks these metrics and fires a follow-up query if needed. See the skills concept doc for details.
Tests
25+ unit tests covering agent construction, custom tools, attachments (inbound and outbound via Attach), config reload, session export, self-improve tracking, and memory nudging.