v0.19.1
v0.19.1 release summary
Before this release, Agent was a stateless single-turn executor. It could run tasks, call tools, and return a response β but you had to orchestrate everything yourself. No loop, no memory, no way to compose agents together without writing your own plumbing.
This release adds all of that. Two PRs: #56 introduces the full agentic loop engine, and #57 layers potato-spec on top so you can define agents and workflows in YAML instead of code.
Breaking changes
None. All new fields on Agent are Option or empty Vec and default to no-op. Existing execute_task, execute, and execute_with_tools signatures are unchanged.
The one structural change worth noting: ToolCall gains an optional call_id: Option<String> field. If you're exhaustively destructuring ToolCall somewhere, you'll need to add that field.
Changes
potato-spec β define agents in YAML (#57)
New crate. SpecLoader reads a YAML file and builds fully-configured Agent, SequentialAgent, ParallelAgent, and DAG Workflow instances. The before/after is pretty stark:
Before:
let agent = AgentBuilder::new()
.provider(Provider::Anthropic)
.model("claude-haiku-4-5")
.system_prompt("You are a summarization assistant.")
.with_windowed_memory(10)
.stop_on_keyword("DONE")
.with_callback(Arc::new(LoggingCallback))
.build()
.await?;After:
agents:
- id: summarizer
provider: anthropic
model: claude-haiku-4-5
system_prompt: You are a summarization assistant.
memory:
type: windowed
window_size: 10
criteria:
- type: keyword
keyword: "DONE"
callbacks:
- type: logginglet spec = SpecLoader::from_spec_path("agents.yaml").await?;
let agent = spec.agent("summarizer").unwrap();Workflows support three types: sequential, parallel, and workflow (DAG). Sequential and parallel steps accept both ref: (a named agent defined elsewhere in the file) and inline agent definitions. DAG tasks use a dependencies list β SpecLoader topologically sorts them at load time and returns SpecError::CyclicDependency if there's a cycle. That's a load-time error, not a runtime one, which is the right call.
If you need tools or custom callbacks that can't be expressed in YAML, register them first:
let spec = SpecLoader::new()
.register_async_tool(Arc::new(MyTool))
.register_callback("my_hook", Arc::new(MyCallback))
.load_file("agents.yaml")
.await?;SpecError has 7 variants covering the main failure modes: unknown provider, unsupported memory type, missing agent ref, cyclic dependency, YAML parse error, and file I/O.
Agentic loop β potato-agent (#56)
Agent now implements AgentRunner:
pub trait AgentRunner: Send + Sync + Debug {
async fn run(
&self,
input: &str,
session: &SessionState,
) -> Result<AgentRunOutcome, AgentError>;
}AgentRunOutcome is either Complete(AgentRunResult) or NeedsInput { question, resume_context }. The NeedsInput arm is how human-in-the-loop works β the caller persists ResumeContext and calls resume() with the user's response when ready.
AgentRunResult has four fields: final_response, iterations, completion_reason (a human-readable string from whichever criteria fired), and combined_text (populated only by the CollectAll merge strategy in parallel runs).
AgentBuilder is the new construction path. It's fluent and handles wiring memory, criteria, callbacks, and stores in one chain. The old constructors still work.
Completion criteria
CompletionCriteria is a trait. Three built-ins:
| Criteria | Stops when |
|---|---|
MaxIterationsCriteria |
ctx.iteration >= max |
KeywordCriteria |
response text contains the keyword |
StructuredOutputCriteria |
response parses as valid JSON (optionally schema-validated) |
Multiple criteria are OR'd β the first one that returns true stops the loop. If you need AND semantics you'll need to write a custom implementation that wraps multiple criteria.
Lifecycle callbacks
AgentCallback fires at six points: before_model_call, after_model_call, on_tool_call, on_tool_result, on_complete, on_error. Each returns a CallbackAction:
Continueβ keep goingOverrideResponse(String)β replace the model response and continueAbort(String)β surface the string asAgentErrorand stop
LoggingCallback is built in and traces all six hooks via tracing::info!. It's the type: logging option in YAML specs.
Memory
Three implementations of the Memory trait:
InMemoryMemoryβ unbounded, no persistenceWindowedMemoryβ keeps the last N turns, evicts oldest on overflowPersistentMemoryβ delegates to aMemoryStorebackend, optionally windowed
Memory lives on Agent as Option<Arc<Mutex<Box<dyn Memory>>>> and is deliberately excluded from Clone. When you clone an agent (e.g., for parallel runs), it gets empty memory β shared mutable history across concurrent agents is not something you want by default.
Store layer
Four store traits, each with a SQLite implementation behind feature = "sqlite":
| Trait | SQLite impl | Scope |
|---|---|---|
MemoryStore |
SqliteMemoryStore |
Conversation turns, keyed by UUIDv7 for time-ordered queries |
SessionStore |
SqliteSessionStore |
SessionSnapshot per session_id |
UserStateStore |
SqliteUserStateStore |
Per (app_name, user_id) |
AppStateStore |
SqliteAppStateStore |
Per app_name, spans all users |
SessionState is a cheap-clone Arc<RwLock<HashMap<String, Value>>> for sharing mutable data across agents within a single run. SessionSnapshot is its serializable form for the store layer.
Agent-as-tool
AgentTool wraps any AgentRunner as an AsyncTool. The LLM calls it with {"input": "<string>"} and gets back the agent's final response. This is how you build hierarchical agent systems β a top-level agent calls sub-agents through the same tool-use loop it uses for everything else.
AgentToolPolicy lets you constrain what a sub-agent can do: set disallow_sub_agent_calls to block recursive AgentTool invocations, or populate disallowed_agent_ids to block specific agents by ID.
AsyncTool is a new trait in potato-type for non-blocking tool execution:
#[async_trait]
pub trait AsyncTool: Send + Sync + Debug {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameter_schema(&self) -> Value;
async fn execute(&self, args: Value) -> Result<Value, TypeError>;
fn as_any(&self) -> Option<&dyn std::any::Any> { None }
}ToolRegistry gains register_async_tool, get_async_tool, and get_all_definitions. The existing sync execute path is unchanged.
Python stubs
Prompt.__init__ is replaced with @overload __new__ signatures. No behavior change β this fixes mypy/pyright resolving the multiple constructor forms.
Upgrading from v0.19.0
No changes required for existing code.
To use the SQLite store layer, add the feature flag:
potato-agent = { version = "0.19.1", features = ["sqlite"] }If you're exhaustively destructuring ToolCall, add the new call_id: Option<String> field.