autumn-plugin-agent
Provider-agnostic LLM agent harness with tool calling for Autumn apps.
Requires autumn-web 0.10.
One trait, two providers, zero heavyweight LLM SDKs. The plugin speaks HTTP directly:
- OpenAI-compatible chat-completions (
POST /chat/completions) — works with OpenAI, Ollama, vLLM, LM Studio, and any proxy that exposes the standard OpenAI protocol. - Anthropic Messages API (
POST /v1/messages).
Quickstart
Add the plugin and a tool:
use ;
use ;
use *;
use Arc;
async
async
async
Configure in autumn.toml:
[]
= "openai-compatible" # or "anthropic"
= "gpt-4o-mini"
# base_url = "http://localhost:11434/v1" # Ollama, vLLM, ...
= 10
= 32000
# max_run_secs = 300 # wall-clock limit per run
# prompt_caching = true # Anthropic cache breakpoints (default on)
The API key comes only from the environment — never from the config file:
Always-on agents
The crate has the primitives that today's always-on agents (Hermes,
OpenClaw, Dots, Muse) share. See
docs/research/always-on-agents.md.
use Arc;
use Duration;
use ;
new
.tool // declares ToolEffect::External
.memory_store
.policy // pause before writes
.heartbeat
.followups // agent books its own wake-ups
.delivery; // where reports go
| Feature | API |
|---|---|
| Multi-turn and sessions | Agent::run_turn, Agent::run_in_session, AgentHandle::chat, SessionStore |
| Compaction | Compaction: summarizes old turns once a session passes a token threshold |
| Memory | MemoryStore + the built-in memory tool; bounded blocks, frozen snapshot per run |
| Skills | Skill::parse (SKILL.md front matter) + the load_skill tool |
| Approvals | ToolPolicy / ToolRules → AgentOutcome::AwaitingApproval { state } → Agent::resume or enqueue_agent_resume |
| Hooks | AgentHooks: before_model, before_tool (Block/Modify), after_tool, on_outcome |
| Loop guard | LoopGuard → AgentOutcome::LoopDetected |
| Deadline | Agent::max_duration / max_run_secs → BudgetKind::Deadline |
| Heartbeat | Heartbeat::every / Heartbeat::cron; HEARTBEAT_OK stays silent; read-only by default; precheck |
| Follow-ups | the schedule_followup tool (delayed agent_run), capped chain depth |
| Subagents | AgentTool wraps a child Agent as a tool |
| Tracked runs | enqueue_agent_run_tracked → poll Autumn's job-status route; the result is the outcome JSON |
| Prompt caching | Anthropic cache_control breakpoints; TokenUsage::cache_read_tokens |
Run cargo run --example always_on_agent to see memory, an approval round
trip, and two heartbeat ticks against a scripted model.
What the plugin installs
[agent]config section (declared strict-config-safe).agent_runandagent_resumebackground jobs — enqueue withenqueue_agent_run(AgentRunArgs::new(prompt)),enqueue_agent_run_tracked, orenqueue_agent_resume(re-exported at the crate root).agent_heartbeatscheduled task, when aHeartbeatis set.agenthealth indicator on/actuator/health— a cheaplist_modelsping against the same provider client the running app uses (shared with the startup hook; DOWN until startup completes). It sits in the health-only group, so a sick provider never blocks deploys.AgentHandleextractor for handlers — a ready-to-run agent from app state.
The agent loop
Agent::run iterates: call model → execute tool calls → append results →
repeat, until the model answers, a budget runs out, the loop guard fires,
or the policy pauses for approval. max_steps counts
tool-execution rounds (the final answer is free) and max_tokens covers
estimated plus provider-reported usage; the per-call output cap reserves the
estimated history cost first, and a final answer that overshoots the token
budget still returns AgentOutcome::BudgetExhausted, not an error. Unknown
tools, tool failures, denials, and rejections reach the model as error
payloads so it can recover. Calls in one round run concurrently
(parallel_tool_calls(false) runs them one by one). When the history must
shrink, the loop keeps the system prompt and the task message and never
leaves a tool result without its call.
Background runs
use ;
let handle = enqueue_agent_run_tracked
.await?;
// Poll handle.status_path(); the result is the AgentOutcome as JSON.
The run id is fixed at enqueue time, so a retried job keeps it. Tools read
it from ToolContext to build idempotency keys.
Crate layout
| Module | Contents |
|---|---|
config |
Layered [agent] config; secrets stay in the env |
error |
AgentError + ErrorKind + HTTP status mapping |
client |
LlmClient trait, both provider implementations, prompt caching |
tools |
Tool trait, ToolContext, ToolEffect, FnTool adapter |
agent |
Agent loop, budgets, approvals (RunState), AgentRuntime |
hooks |
AgentHooks lifecycle hooks |
policy |
ToolPolicy, ToolRules, Strictest |
loop_guard |
Repeated-call detection |
session |
SessionStore, compaction |
memory |
MemoryStore, bounded blocks, the memory tool |
skills |
Skill (SKILL.md), the load_skill tool |
delegate |
AgentTool subagents |
proactive |
Heartbeat, Delivery, schedule_followup |
jobs |
agent_run / agent_resume jobs, tracked enqueue |
ids |
RunId, SessionId |
plugin |
AgentPlugin, AgentHandle extractor |
health |
Provider health indicator |
Known issues
- The default session store and the in-memory memory store lose data on
restart. Supply database-backed
SessionStore/MemoryStoreimplementations in production. - A retried
agent_runjob re-runs the whole loop. Step checkpoints are a follow-up; tools can deduplicate onToolContext::run_idmeanwhile. - Non-streaming only. Streaming chat completions is an explicit follow-up (see CHANGELOG).
- Token accounting mixes provider-reported usage with a
bytes / 4heuristic between calls. Good enough for a guardrail, never for billing. - The plugin reads
[agent]fromautumn.tomlitself with thetomlcrate; Autumn 0.8 still exposes no per-section accessor to plugins, so the framework's own profile layering does not apply to plugin keys yet.
License
Apache-2.0.