First Tool
Goal
Implement one tool that reads typed state from ToolCallContext during execution.
State is optional. Many tools (API calls, search, shell commands) don’t need state – just implement
executeand return aToolResult.
Prerequisites
- Complete First Agent first.
- Reuse the runtime dependencies from First Agent.
[dependencies]
awaken = { package = "awaken-agent", version = "0.1" }
tokio = { version = "1", features = ["full"] }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
1. Define a StateKey
A StateKey describes one named slot in the state map. It declares the value type, how updates are applied, and the lifetime scope.
use awaken::{StateKey, KeyScope, MergeStrategy};
/// Tracks how many times the greeting tool has been called.
struct GreetCount;
impl StateKey for GreetCount {
const KEY: &'static str = "greet_count";
const MERGE: MergeStrategy = MergeStrategy::Commutative;
const SCOPE: KeyScope = KeyScope::Run;
type Value = u32;
type Update = u32;
fn apply(value: &mut Self::Value, update: Self::Update) {
*value += update;
}
}
fn main() {}
Key choices:
KeyScope::Run– state resets at the start of each run. UseKeyScope::Threadto persist across runs.MergeStrategy::Commutative– safe for concurrent updates. UseExclusivewhen only one writer is expected.applydefines how anUpdatemodifies the currentValue. Here it increments.
2. Implement the Tool
The tool reads the current count via ctx.state::<GreetCount>() and returns a personalized greeting.
use awaken::{StateKey, KeyScope, MergeStrategy};
struct GreetCount;
impl StateKey for GreetCount {
const KEY: &'static str = "greet_count";
const MERGE: MergeStrategy = MergeStrategy::Commutative;
const SCOPE: KeyScope = KeyScope::Run;
type Value = u32;
type Update = u32;
fn apply(value: &mut Self::Value, update: Self::Update) { *value += update; }
}
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use awaken::contract::tool::{Tool, ToolDescriptor, ToolResult, ToolOutput, ToolError, ToolCallContext};
struct GreetTool;
#[async_trait]
impl Tool for GreetTool {
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor::new("greet", "Greet", "Greet a user by name")
.with_parameters(json!({
"type": "object",
"properties": {
"name": { "type": "string", "description": "Name to greet" }
},
"required": ["name"]
}))
}
fn validate_args(&self, args: &Value) -> Result<(), ToolError> {
args["name"]
.as_str()
.filter(|s| !s.is_empty())
.ok_or_else(|| ToolError::InvalidArguments("name is required".into()))?;
Ok(())
}
async fn execute(
&self,
args: Value,
ctx: &ToolCallContext,
) -> Result<ToolOutput, ToolError> {
let name = args["name"].as_str().unwrap_or("world");
// Read state -- returns None if the key has not been set yet.
let count = ctx.state::<GreetCount>().copied().unwrap_or(0);
Ok(ToolResult::success("greet", json!({
"greeting": format!("Hello, {}!", name),
"times_greeted": count,
})).into())
}
}
fn main() {}
3. Register the Tool
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use awaken::{StateKey, KeyScope, MergeStrategy};
use awaken::contract::tool::{Tool, ToolDescriptor, ToolResult, ToolOutput, ToolError, ToolCallContext};
struct GreetCount;
impl StateKey for GreetCount {
const KEY: &'static str = "greet_count";
const MERGE: MergeStrategy = MergeStrategy::Commutative;
const SCOPE: KeyScope = KeyScope::Run;
type Value = u32;
type Update = u32;
fn apply(value: &mut Self::Value, update: Self::Update) { *value += update; }
}
struct GreetTool;
#[async_trait]
impl Tool for GreetTool {
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor::new("greet", "Greet", "Greet a user by name")
}
async fn execute(&self, args: Value, ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
Ok(ToolResult::success("greet", json!({})).into())
}
}
use awaken::registry_spec::AgentSpec;
use awaken::AgentRuntimeBuilder;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let agent_spec = AgentSpec::new("assistant")
.with_model("gpt-4o-mini")
.with_system_prompt("You are a helpful assistant. Use the greet tool when asked.")
.with_max_rounds(5);
let runtime = AgentRuntimeBuilder::new()
.with_agent_spec(agent_spec)
.with_tool("greet", Arc::new(GreetTool))
.build()?;
Ok(())
}
4. Run
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use awaken::{StateKey, KeyScope, MergeStrategy};
use awaken::contract::tool::{Tool, ToolDescriptor, ToolResult, ToolOutput, ToolError, ToolCallContext};
use awaken::registry_spec::AgentSpec;
use awaken::AgentRuntimeBuilder;
struct GreetCount;
impl StateKey for GreetCount {
const KEY: &'static str = "greet_count";
const MERGE: MergeStrategy = MergeStrategy::Commutative;
const SCOPE: KeyScope = KeyScope::Run;
type Value = u32;
type Update = u32;
fn apply(value: &mut Self::Value, update: Self::Update) { *value += update; }
}
struct GreetTool;
#[async_trait]
impl Tool for GreetTool {
fn descriptor(&self) -> ToolDescriptor {
ToolDescriptor::new("greet", "Greet", "Greet a user by name")
}
async fn execute(&self, args: Value, ctx: &ToolCallContext) -> Result<ToolOutput, ToolError> {
Ok(ToolResult::success("greet", json!({})).into())
}
}
use awaken::contract::message::Message;
use awaken::contract::event_sink::VecEventSink;
use awaken::RunRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let runtime = AgentRuntimeBuilder::new()
.with_agent_spec(AgentSpec::new("assistant").with_model("gpt-4o-mini"))
.with_tool("greet", Arc::new(GreetTool))
.build()?;
let request = RunRequest::new(
"thread-1",
vec![Message::user("Greet Alice")],
)
.with_agent_id("assistant");
let sink = Arc::new(VecEventSink::new());
runtime.run(request, sink.clone()).await?;
Ok(())
}
5. Verify
Check the collected events for a ToolCallDone event with name == "greet":
use awaken::contract::event::AgentEvent;
let events = sink.take();
let tool_done = events.iter().any(|e| matches!(
e,
AgentEvent::ToolCallDone { id: _, message_id: _, result: _, outcome: _ }
));
println!("tool_call_done_seen: {}", tool_done);
Expected:
tool_call_done_seen: true- The
resultinsideToolCallDonecontainsgreetingandtimes_greetedfields.
What You Created
A tool that:
- Declares a JSON Schema for its arguments via
descriptor(). - Validates arguments before execution via
validate_args(). - Reads typed state from the snapshot via
ctx.state::<K>(). - Returns structured JSON via
ToolResult::success().
The StateKey trait gives you type-safe, scoped state without raw JSON manipulation.
Which Doc To Read Next
- understand the full tool lifecycle: Tool Trait
- add plugins that manage state across runs: Add a Plugin
- learn about state scoping rules: State Keys
Common Errors
ctx.state::<K>()returnsNone: the state key has not been written yet in this run. Use.unwrap_or_default()or.copied().unwrap_or(0)for numeric defaults.StateError::KeyEncode/StateError::KeyDecode: theValuetype does not round-trip through JSON. EnsureSerializeandDeserializeare derived correctly.ToolError::InvalidArgumentsnot surfaced:validate_argsis called beforeexecuteby the runtime. If you skip validation, bad input reachesexecuteand may panic on.unwrap().- Scope mismatch:
KeyScope::Runstate is cleared between runs. If you expect persistence, useKeyScope::Thread.