Runtime-Aware Tools
RuntimeAwareTool extends the basic Tool trait with runtime context -- current graph state, a store reference, stream writer, tool call ID, and runnable config. Implement this trait for tools that need to read or modify graph state during execution.
The ToolRuntime Struct
When a runtime-aware tool is invoked, it receives a ToolRuntime with the following fields:
pub struct ToolRuntime {
pub store: Option<Arc<dyn Store>>,
pub stream_writer: Option<StreamWriter>,
pub state: Option<Value>,
pub tool_call_id: String,
pub config: Option<RunnableConfig>,
}
| Field | Description |
|---|---|
store | Shared key-value store for cross-tool persistence |
stream_writer | Writer for pushing streaming output from within a tool |
state | Serialized snapshot of the current graph state |
tool_call_id | The ID of the tool call being executed |
config | Runnable config with tags, metadata, and run ID |
Implementing with #[tool] and #[inject]
The recommended way to define a runtime-aware tool is with the #[tool] macro. Use #[inject(store)], #[inject(state)], or #[inject(tool_call_id)] on parameters to receive runtime context. These injected parameters are hidden from the LLM schema. Using any #[inject] attribute automatically switches the generated impl to RuntimeAwareTool:
use std::sync::Arc;
use serde_json::{json, Value};
use synaptic::macros::tool;
use synaptic::core::{Store, SynapticError};
/// Save a note to the store.
#[tool]
async fn save_note(
/// The note key
key: String,
/// The note text
text: String,
#[inject(store)] store: Arc<dyn Store>,
) -> Result<Value, SynapticError> {
store.put(
&["notes"],
&key,
json!({"text": text}),
).await?;
Ok(json!({"saved": key}))
}
// save_note() returns Arc<dyn RuntimeAwareTool>
let tool = save_note();
The #[inject(store)] parameter receives the Arc<dyn Store> from the ToolRuntime at execution time. Only key and text appear in the JSON Schema sent to the model.
Using with ToolNode in a Graph
ToolNode automatically injects runtime context into registered RuntimeAwareTool instances. Register them with with_runtime_tool() and optionally attach a store with with_store():
use synaptic::graph::ToolNode;
use synaptic::tools::{ToolRegistry, SerialToolExecutor};
let registry = ToolRegistry::new();
let executor = SerialToolExecutor::new(registry);
let tool_node = ToolNode::new(executor)
.with_store(store.clone())
.with_runtime_tool(save_note()); // save_note() returns Arc<dyn RuntimeAwareTool>
When the graph executes this tool node and encounters a tool call matching "save_note", it builds a ToolRuntime populated with the current graph state, the store, and the tool call ID, then calls call_with_runtime().
RuntimeAwareToolAdapter -- Using Outside a Graph
If you need to use a RuntimeAwareTool in a context that expects the standard Tool trait (for example, with SerialToolExecutor directly), wrap it in a RuntimeAwareToolAdapter:
use std::sync::Arc;
use synaptic::core::{RuntimeAwareTool, RuntimeAwareToolAdapter, ToolRuntime};
let tool = save_note(); // Arc<dyn RuntimeAwareTool>
let adapter = RuntimeAwareToolAdapter::new(tool);
// Optionally inject a runtime before calling
adapter.set_runtime(ToolRuntime {
store: Some(store.clone()),
stream_writer: None,
state: None,
tool_call_id: "call-1".to_string(),
config: None,
}).await;
// Now use it as a regular Tool
let result = adapter.call(json!({"key": "k", "text": "hello"})).await?;
If set_runtime() is not called before call(), the adapter uses a default empty ToolRuntime with all optional fields set to None and an empty tool_call_id.
create_react_agent with a Store
When building a ReAct agent via create_react_agent, pass a store through AgentOptions to have it automatically wired into the ToolNode for all registered runtime-aware tools:
use synaptic::graph::{create_react_agent, AgentOptions};
let graph = create_react_agent(
model,
tools,
AgentOptions {
store: Some(store),
..Default::default()
},
);