Skip to content

Tool Trait

The Tool trait is the primary extension point for giving agents capabilities. Tools are stateless functions that receive JSON arguments and a read-only context, and return a ToolOutput.

#[async_trait]
pub trait Tool: Send + Sync {
/// Return the descriptor for this tool.
fn descriptor(&self) -> ToolDescriptor;
/// Validate arguments before execution. Default: accept all.
fn validate_args(&self, _args: &Value) -> Result<(), ToolError> {
Ok(())
}
/// Execute the tool with the given arguments and context.
async fn execute(
&self,
args: Value,
ctx: &ToolCallContext,
) -> Result<ToolOutput, ToolError>;
}

Crate path: awaken::contract::tool::Tool

Describes a tool’s identity and parameter schema. Registered with the runtime and sent to the LLM as available functions.

pub struct ToolDescriptor {
pub id: String,
pub name: String,
pub description: String,
/// JSON Schema for parameters.
pub parameters: Value,
pub category: Option<String>,
}

Builder methods:

ToolDescriptor::new(id, name, description) -> Self
.with_parameters(schema: Value) -> Self
.with_category(category: impl Into<String>) -> Self

Returned by Tool::execute. Carries the execution outcome back to the agent loop.

pub struct ToolResult {
pub tool_name: String,
pub status: ToolStatus,
pub data: Value,
pub message: Option<String>,
pub suspension: Option<Box<SuspendTicket>>,
}
pub enum ToolStatus {
Success, // Execution succeeded
Pending, // Suspended, waiting for external resume
Error, // Execution failed; content sent back to LLM
}
MethodStatusUse case
ToolResult::success(name, data)SuccessNormal completion
ToolResult::success_with_message(name, data, msg)SuccessCompletion with description
ToolResult::error(name, message)ErrorRecoverable failure
ToolResult::error_with_code(name, code, message)ErrorStructured error with code
ToolResult::suspended(name, message)PendingHITL suspension
ToolResult::suspended_with(name, message, ticket)PendingSuspension with ticket
  • is_success() -> bool
  • is_pending() -> bool
  • is_error() -> bool
  • to_json() -> Value

Errors returned from validate_args or execute. Unlike ToolResult::Error (which is sent to the LLM), a ToolError aborts the tool call.

pub enum ToolError {
InvalidArguments(String),
ExecutionFailed(String),
Denied(String),
NotFound(String),
Internal(String),
}

Read-only context provided to a tool during execution.

pub struct ToolCallContext {
pub call_id: String,
pub tool_name: String,
pub run_identity: RunIdentity,
pub agent_spec: Arc<AgentSpec>,
pub snapshot: Snapshot,
pub activity_sink: Option<Arc<dyn EventSink>>,
/// Optional cancellation token for cooperative cancellation.
/// Long-running tools (MCP calls, sub-agent execution) should check
/// `is_cancelled()` periodically or use `cancelled()` in `tokio::select!`.
pub cancellation_token: Option<CancellationToken>,
/// Resume decision input — set when the runtime is replaying a suspended
/// tool call (see `ToolCallResumeMode`).
pub resume_input: Option<ToolCallResume>,
/// Active suspension id, if this execution is a resumed suspension.
pub suspension_id: Option<String>,
/// Suspension reason/action, if this execution is a resumed suspension.
pub suspension_reason: Option<String>,
}
/// Read a typed state key from the snapshot.
fn state<K: StateKey>(&self) -> Option<&K::Value>
/// Report an activity snapshot for this tool call.
async fn report_activity(&self, activity_type: &str, content: &str)
/// Report an incremental activity delta.
async fn report_activity_delta(&self, activity_type: &str, patch: Value)
/// Report structured tool call progress.
async fn report_progress(
&self,
status: ProgressStatus,
message: Option<&str>,
progress: Option<f64>,
)
use async_trait::async_trait;
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolResult, ToolOutput};
use serde_json::{Value, json};
struct Greet;
#[async_trait]
impl Tool for Greet {
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor::new("greet", "greet", "Greet a user by name")
.with_parameters(json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}))
}
async fn execute(
&self,
args: Value,
_ctx: &ToolCallContext,
) -> Result<ToolOutput, ToolError> {
let name = args["name"]
.as_str()
.ok_or_else(|| ToolError::InvalidArguments("name required".into()))?;
Ok(ToolResult::success("greet", json!({ "greeting": format!("Hello, {name}!") })).into())
}
}
use async_trait::async_trait;
use awaken::contract::tool::{Tool, ToolCallContext, ToolDescriptor, ToolError, ToolResult, ToolOutput};
use awaken::state::StateKey;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
// Assume a state key is defined elsewhere:
// struct UserPreferences;
// impl StateKey for UserPreferences { ... }
struct GetPreferences;
#[async_trait]
impl Tool for GetPreferences {
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor::new("get_prefs", "get_preferences", "Get user preferences")
}
async fn execute(
&self,
_args: Value,
ctx: &ToolCallContext,
) -> Result<ToolOutput, ToolError> {
// Read typed state through the snapshot
// let prefs = ctx.state::<UserPreferences>().cloned().unwrap_or_default();
// Ok(ToolResult::success("get_prefs", serde_json::to_value(&prefs).unwrap()).into())
Ok(ToolResult::success("get_prefs", json!({})).into())
}
}

Every tool call passes through plugin hooks before and after execution. This allows plugins to intercept, observe, or modify tool call behavior without changing the tool itself.

LLM selects tool
-> validate_args()
-> ToolGate phase (pure allow/block/suspend/set-result decision)
-> BeforeToolExecute phase (execution-time hooks for allowed calls only)
-> execute() (only if ToolGate allows)
-> AfterToolExecute phase (plugins run hooks)
Plugins observe the ToolResult and may modify state

Runs after argument validation and before any execution-time hook. Plugins implement ToolGateHook and return Option<ToolInterceptPayload>:

VariantEffect
Block { reason }Terminate the call and fail the run with the reason
Suspend(SuspendTicket)Pause the run and wait for an external decision
SetResult(ToolResult)Skip tool execution and use the provided result

When multiple gate hooks return a decision, priority is: Block > Suspend > SetResult.

ToolGate is pure and may be re-evaluated after earlier allowed tool calls commit new state in the same step.

Runs once per tool call batch after ToolGate has already allowed the call. Plugins receive a PhaseContext containing the tool name, call ID, and validated arguments. Hooks return a StateCommand for execution-time work such as counters, throttling state, or observability setup.

Runs after execute() completes (or after ToolGate supplies a result). Plugins receive the PhaseContext with the ToolResult attached. Hooks can update state, emit events, or schedule actions for subsequent phases.

Each tool call tracks a ToolCallStatus through its lifecycle:

New -> Running -> Succeeded
Failed
Suspended -> Resuming -> Running -> ...
Cancelled

Terminal states (Succeeded, Failed, Cancelled) cannot transition further.

See Plugin Internals for intercept priority details and the full phase convergence loop.