Give an Agent typed tools
Expose async Rust functions through JSON Schema without granting ambient authority.
Define a tool
A Runifold Tool has a stable descriptor, JSON input schema, effect
classification, and an asynchronous execution boundary. Prefer the
#[runifold::tool] macro for ordinary typed functions.
Add the derive dependencies once:
cargo add serde --features derive
cargo add schemarsuse runifold::{JsonSchema, ToolContext, ToolError, tool};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, JsonSchema)]
struct WeatherInput {
city: String,
}
#[derive(Debug, Serialize, JsonSchema)]
struct Weather {
city: String,
celsius: i32,
}
#[tool(description = "Get the current temperature for a city")]
async fn current_weather(
input: WeatherInput,
_context: ToolContext,
) -> Result<Weather, ToolError> {
weather_service::lookup(&input.city).await
}Use domain input types instead of a bag of strings. Validation should happen at the boundary, before external work begins.
Register the tool
Register the tool on only the Agent that needs it:
let agent = runtime
.agent("travel-planner")
.tool(current_weather_tool())
.max_turns(6)
.build()?;Duplicate names and collisions with child-Agent routes are build errors. A registered tool does not automatically become available to every child Run; execution still requires the matching capability grant.
Return rich results
Since 0.9.0, a manual Tool returns ToolOutput, not one scalar value.
Choose the constructor that matches the real boundary:
| Need | Constructor | Meaning |
|---|---|---|
| ordinary JSON or text | ToolOutput::model_visible(value) | keeps structured JSON and a text fallback |
| ordered text, image, audio, document, or resource parts | ToolOutput::rich(parts) | preserves the provider-neutral content sequence |
| recoverable domain failure | ToolOutput::model_error(parts) | tells the model the Tool ran but the application rejected the request |
| host-only data | ToolOutput::host_only(parts) | fails closed if code tries to expose it to a model |
Attach separately validated output with with_structured_content, and keep
application-only annotations in namespaced with_metadata entries. A
ToolError still means the Tool runtime itself failed; model_error is a
completed, model-visible application result.
Code written against 0.3.x that constructs ToolOutput { value, ... } must
migrate to one of these constructors.
Keep binary artifacts out of transcripts
Large Tool outputs should remain references in conversations and checkpoints.
Configure an ArtifactStore and a validated scope once on the Agent:
use std::sync::Arc;
use runifold::{ArtifactScope, InMemoryArtifactStore};
let artifact_store = Arc::new(InMemoryArtifactStore::new());
let agent = runtime
.agent("report-analyst")
.artifacts(ArtifactScope::parse("tenant.acme")?, artifact_store)
.tool(render_chart_tool())
.build()?;Inside the Tool, read the configured store and scope from ToolContext, write
with a stable idempotency key, then return the resulting ArtifactRef as a
MediaSource::Artifact. Runifold resolves and verifies the bytes only at the
Provider transport boundary.
Use InMemoryArtifactStore for tests, SqliteStore for one durable process,
or PostgresStore for shared production workers. Every reference binds scope,
MIME type, byte length, SHA-256 digest, optional name, and expiry. Listing is
bounded and cursor-based; deletion and expiry purging stay scope-bound.
Provider support is explicit: OpenAI Responses and Gemini preserve native multimodal Tool results; Anthropic supports text, image, and resource forms; Bedrock supports JSON, images, and documents. Text-only Chat Completions and Ollama reject unsupported media instead of silently flattening it.
Authority and effects
Tool registration answers “what could this Agent call?” A RunContext
capability answers “what may this execution call?” Both must agree.
Classify external actions accurately:
- pure reads can usually be retried;
- idempotent writes require stable idempotency keys;
- non-idempotent writes must fail closed after ambiguous transport failure;
- destructive or high-risk operations should pass through application policy.
For recoverable writes, use the write-ahead effect boundary so intent is recorded before execution and a completed result can be replayed without repeating the action.
Handle tool errors
ToolErrorPolicy controls whether a tool failure becomes model-visible input or
terminates the Agent. Do not expose secrets, raw credentials, database URLs, or
internal stack traces in model-visible error text.
An operational error should preserve enough structure for the application to decide whether to retry, compensate, ask a human, or fail the run.