# Extending The SDK
This SDK is designed around a few extension seams:
- `Tool`
- `PromptBuilder`
- `Hook`
- `LlmClient`
## Custom Tools
Implement `Tool` to expose new actions to an agent:
```rust
use async_trait::async_trait;
use serde_json::json;
use agent_sdk::{SdkResult, Tool, ToolDefinition};
struct EchoTool;
#[async_trait]
impl Tool for EchoTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "echo".to_string(),
description: "Echo a string.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"text": { "type": "string" }
},
"required": ["text"]
}),
}
}
async fn execute(&self, arguments: serde_json::Value) -> SdkResult<serde_json::Value> {
Ok(json!({
"echo": arguments["text"].as_str().unwrap_or("")
}))
}
}
```
Register it in a `ToolRegistry`:
```rust
use std::sync::Arc;
use agent_sdk::tools::registry::ToolRegistry;
let mut registry = ToolRegistry::new();
registry.register(Arc::new(EchoTool));
```
## Custom Prompt Builder
Use a custom `PromptBuilder` when generic task prompts are not enough:
```rust
use agent_sdk::traits::prompt_builder::PromptBuilder;
use agent_sdk::tools::registry::ToolRegistry;
use agent_sdk::Task;
struct ApiReviewPromptBuilder;
impl PromptBuilder for ApiReviewPromptBuilder {
fn build_system_prompt(
&self,
task: &Task,
source_root: &std::path::Path,
work_dir: &std::path::Path,
) -> String {
format!(
"You review public Rust APIs.\nSource: {}\nWork: {}\nTask: {}",
source_root.display(),
work_dir.display(),
task.title
)
}
fn build_user_message(&self, task: &Task) -> String {
format!("Complete this API review task:\n{}", task.description)
}
fn customize_tools(&self, _task: &Task, registry: ToolRegistry) -> ToolRegistry {
registry
}
}
```
Attach it with:
```rust
use std::sync::Arc;
let team = agent_sdk::AgentTeam::new(
agent_sdk::LlmConfig::default(),
agent_sdk::AgentConfig::default(),
)
.prompt_builder(Arc::new(ApiReviewPromptBuilder));
```
## Hooks
Hooks let you veto important lifecycle actions:
```rust
use agent_sdk::{Hook, HookEvent, HookResult};
struct NoEmptyTasks;
impl Hook for NoEmptyTasks {
fn on_event(&self, event: &HookEvent) -> HookResult {
match event {
HookEvent::TaskCreated { task } if task.description.trim().is_empty() => {
HookResult::Reject {
feedback: "Task descriptions must not be empty".to_string(),
}
}
_ => HookResult::Continue,
}
}
}
```
Attach with:
```rust
let team = agent_sdk::AgentTeam::new(
agent_sdk::LlmConfig::default(),
agent_sdk::AgentConfig::default(),
)
.add_hook(NoEmptyTasks);
```
## Custom LLM Client
The crate-level factory is:
```rust
let client = agent_sdk::create_client(&llm_config)?;
```
If you need a different backend, implement `LlmClient`:
```rust
use async_trait::async_trait;
use agent_sdk::{ChatMessage, LlmClient, SdkResult, ToolDefinition};
struct MockClient;
#[async_trait]
impl LlmClient for MockClient {
async fn ask(&self, _system: &str, _user_message: &str) -> SdkResult<(String, u64)> {
Ok(("ok".to_string(), 0))
}
async fn chat(
&self,
_messages: &[ChatMessage],
_tools: &[ToolDefinition],
) -> SdkResult<(ChatMessage, u64)> {
Ok((ChatMessage::assistant("done"), 0))
}
}
```
Inject it into `AgentTeam`:
```rust
use std::sync::Arc;
let team = agent_sdk::AgentTeam::new(
agent_sdk::LlmConfig::default(),
agent_sdk::AgentConfig::default(),
)
.llm_client(Arc::new(MockClient));
```
## Event Consumers
The SDK already emits structured `AgentEvent` values. Build adapters around that stream for:
- logging
- metrics
- TUI or GUI updates
- audit trails
Example:
```rust
use agent_sdk::AgentEvent;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentEvent>();
tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
AgentEvent::TaskCompleted { task_id, .. } => {
println!("task done: {}", task_id);
}
_ => {}
}
}
});
```
## Testing Strategy
For library integrations, the safest pattern is:
1. use a mock `LlmClient`
2. use a temporary `work_dir`
3. register only the tools your test needs
4. assert on generated files, task JSON, and emitted events