refactor: deduplicate tool code and remove dead stubs (#98)

* refactor: deduplicate tool parameter extraction and remove dead stub tools

Delete 4 never-registered stub tools (marketplace, restaurant, ecommerce,
taskrabbit) removing ~625 lines of dead code. Add require_str/require_param
helpers to tool.rs and refactor ~30 call sites across 10 tool files from
4-6 line inline extractions to single-line calls. Consolidate worker HTTP
client with get_json/post_json helpers, reducing boilerplate in 4 methods.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: return JSON from orchestrator /complete endpoint

The report_complete handler returned bare StatusCode::OK (no body),
which broke the post_json helper that expects a JSON response.
Return {"status": "ok"} for consistency with other worker endpoints.

Addresses review feedback on PR #98.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Illia Polosukhin
2026-02-15 05:39:52 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9fed8453c7
commit ca8d5c6b5e
20 changed files with 159 additions and 879 deletions
+1 -5
View File
@@ -20,10 +20,6 @@ impl CostEstimator {
// Default tool costs (in USD or equivalent) // Default tool costs (in USD or equivalent)
tool_costs.insert("http".to_string(), dec!(0.0001)); // API call tool_costs.insert("http".to_string(), dec!(0.0001)); // API call
tool_costs.insert("marketplace".to_string(), dec!(0.01)); // Gas costs
tool_costs.insert("ecommerce".to_string(), dec!(0.001)); // API call
tool_costs.insert("taskrabbit".to_string(), dec!(0.0)); // Cost comes from task itself
tool_costs.insert("restaurant".to_string(), dec!(0.001)); // API call
tool_costs.insert("echo".to_string(), dec!(0.0)); // Free tool_costs.insert("echo".to_string(), dec!(0.0)); // Free
tool_costs.insert("time".to_string(), dec!(0.0)); // Free tool_costs.insert("time".to_string(), dec!(0.0)); // Free
tool_costs.insert("json".to_string(), dec!(0.0)); // Free tool_costs.insert("json".to_string(), dec!(0.0)); // Free
@@ -74,7 +70,7 @@ mod tests {
let estimator = CostEstimator::new(); let estimator = CostEstimator::new();
assert_eq!(estimator.estimate_tool("echo"), dec!(0.0)); assert_eq!(estimator.estimate_tool("echo"), dec!(0.0));
assert_eq!(estimator.estimate_tool("marketplace"), dec!(0.01)); assert_eq!(estimator.estimate_tool("http"), dec!(0.0001));
assert!(estimator.estimate_tool("unknown") > dec!(0.0)); assert!(estimator.estimate_tool("unknown") > dec!(0.0));
} }
-4
View File
@@ -16,10 +16,6 @@ impl TimeEstimator {
// Default tool durations // Default tool durations
tool_durations.insert("http".to_string(), Duration::from_secs(5)); tool_durations.insert("http".to_string(), Duration::from_secs(5));
tool_durations.insert("marketplace".to_string(), Duration::from_secs(10));
tool_durations.insert("ecommerce".to_string(), Duration::from_secs(8));
tool_durations.insert("taskrabbit".to_string(), Duration::from_secs(30)); // Just API, not task itself
tool_durations.insert("restaurant".to_string(), Duration::from_secs(5));
tool_durations.insert("echo".to_string(), Duration::from_millis(10)); tool_durations.insert("echo".to_string(), Duration::from_millis(10));
tool_durations.insert("time".to_string(), Duration::from_millis(1)); tool_durations.insert("time".to_string(), Duration::from_millis(1));
tool_durations.insert("json".to_string(), Duration::from_millis(5)); tool_durations.insert("json".to_string(), Duration::from_millis(5));
+2 -2
View File
@@ -202,7 +202,7 @@ async fn report_complete(
State(state): State<OrchestratorState>, State(state): State<OrchestratorState>,
Path(job_id): Path<Uuid>, Path(job_id): Path<Uuid>,
Json(report): Json<CompletionReport>, Json(report): Json<CompletionReport>,
) -> Result<StatusCode, StatusCode> { ) -> Result<Json<serde_json::Value>, StatusCode> {
if report.success { if report.success {
tracing::info!( tracing::info!(
job_id = %job_id, job_id = %job_id,
@@ -223,7 +223,7 @@ async fn report_complete(
}; };
let _ = state.job_manager.complete_job(job_id, result).await; let _ = state.job_manager.complete_job(job_id, result).await;
Ok(StatusCode::OK) Ok(Json(serde_json::json!({"status": "ok"})))
} }
// -- Sandbox job event handlers -- // -- Sandbox job event handlers --
+2 -7
View File
@@ -3,7 +3,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::context::JobContext; use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
/// Simple echo tool for testing. /// Simple echo tool for testing.
pub struct EchoTool; pub struct EchoTool;
@@ -38,12 +38,7 @@ impl Tool for EchoTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let message = params let message = require_str(&params, "message")?;
.get("message")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'message' parameter".to_string())
})?;
Ok(ToolOutput::text(message, start.elapsed())) Ok(ToolOutput::text(message, start.elapsed()))
} }
-136
View File
@@ -1,136 +0,0 @@
//! E-commerce tool for shopping and price comparison.
use async_trait::async_trait;
use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput};
/// Tool for e-commerce operations (Amazon, price comparison, etc.).
pub struct EcommerceTool {
// TODO: Add API clients
}
impl EcommerceTool {
/// Create a new e-commerce tool.
pub fn new() -> Self {
Self {}
}
}
impl Default for EcommerceTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for EcommerceTool {
fn name(&self) -> &str {
"ecommerce"
}
fn description(&self) -> &str {
"Search products, compare prices, and find deals across e-commerce platforms."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "get_product", "compare_prices", "track_price"],
"description": "The e-commerce action to perform"
},
"query": {
"type": "string",
"description": "Search query (for search action)"
},
"product_id": {
"type": "string",
"description": "Product ID or ASIN (for get_product, compare_prices)"
},
"platform": {
"type": "string",
"enum": ["amazon", "ebay", "walmart", "all"],
"description": "E-commerce platform to search"
},
"max_price": {
"type": "number",
"description": "Maximum price filter"
},
"category": {
"type": "string",
"description": "Product category filter"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now();
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'action' parameter".to_string())
})?;
// TODO: Implement actual e-commerce API integrations
let result = match action {
"search" => {
let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
serde_json::json!({
"query": query,
"results": [],
"message": "E-commerce integration not yet implemented"
})
}
"get_product" => {
let product_id = params
.get("product_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'product_id' parameter".to_string())
})?;
serde_json::json!({
"product_id": product_id,
"found": false,
"message": "E-commerce integration not yet implemented"
})
}
"compare_prices" => {
serde_json::json!({
"prices": [],
"message": "E-commerce integration not yet implemented"
})
}
"track_price" => {
serde_json::json!({
"tracking": false,
"message": "E-commerce integration not yet implemented"
})
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"unknown action: {}",
action
)));
}
};
Ok(ToolOutput::success(result, start.elapsed()))
}
fn requires_sanitization(&self) -> bool {
true // External e-commerce data
}
}
+5 -17
View File
@@ -9,7 +9,7 @@ use async_trait::async_trait;
use crate::context::JobContext; use crate::context::JobContext;
use crate::extensions::{ExtensionKind, ExtensionManager}; use crate::extensions::{ExtensionKind, ExtensionManager};
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
// ── tool_search ────────────────────────────────────────────────────────── // ── tool_search ──────────────────────────────────────────────────────────
@@ -133,10 +133,7 @@ impl Tool for ToolInstallTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("name is required".to_string()))?;
let url = params.get("url").and_then(|v| v.as_str()); let url = params.get("url").and_then(|v| v.as_str());
@@ -210,10 +207,7 @@ impl Tool for ToolAuthTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("name is required".to_string()))?;
let result = self let result = self
.manager .manager
@@ -306,10 +300,7 @@ impl Tool for ToolActivateTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("name is required".to_string()))?;
match self.manager.activate(name).await { match self.manager.activate(name).await {
Ok(result) => { Ok(result) => {
@@ -471,10 +462,7 @@ impl Tool for ToolRemoveTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("name is required".to_string()))?;
let message = self let message = self
.manager .manager
+7 -25
View File
@@ -11,7 +11,7 @@ use async_trait::async_trait;
use tokio::fs; use tokio::fs;
use crate::context::JobContext; use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolDomain, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolDomain, ToolError, ToolOutput, require_str};
use crate::workspace::paths as ws_paths; use crate::workspace::paths as ws_paths;
/// Well-known workspace filenames that must go through memory_write, not write_file. /// Well-known workspace filenames that must go through memory_write, not write_file.
@@ -203,10 +203,7 @@ impl Tool for ReadFileTool {
params: serde_json::Value, params: serde_json::Value,
_ctx: &JobContext, _ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let path_str = params let path_str = require_str(&params, "path")?;
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'path' parameter".into()))?;
let offset = params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize; let offset = params.get("offset").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let limit = params.get("limit").and_then(|v| v.as_u64()); let limit = params.get("limit").and_then(|v| v.as_u64());
@@ -328,10 +325,7 @@ impl Tool for WriteFileTool {
params: serde_json::Value, params: serde_json::Value,
_ctx: &JobContext, _ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let path_str = params let path_str = require_str(&params, "path")?;
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'path' parameter".into()))?;
// Reject workspace paths: these live in the database, not on disk. // Reject workspace paths: these live in the database, not on disk.
if is_workspace_path(path_str) { if is_workspace_path(path_str) {
@@ -342,10 +336,7 @@ impl Tool for WriteFileTool {
))); )));
} }
let content = params let content = require_str(&params, "content")?;
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'content' parameter".into()))?;
let start = std::time::Instant::now(); let start = std::time::Instant::now();
@@ -650,20 +641,11 @@ impl Tool for ApplyPatchTool {
params: serde_json::Value, params: serde_json::Value,
_ctx: &JobContext, _ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let path_str = params let path_str = require_str(&params, "path")?;
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'path' parameter".into()))?;
let old_string = params let old_string = require_str(&params, "old_string")?;
.get("old_string")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'old_string' parameter".into()))?;
let new_string = params let new_string = require_str(&params, "new_string")?;
.get("new_string")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'new_string' parameter".into()))?;
let replace_all = params let replace_all = params
.get("replace_all") .get("replace_all")
+3 -11
View File
@@ -9,7 +9,7 @@ use reqwest::Client;
use crate::context::JobContext; use crate::context::JobContext;
use crate::safety::LeakDetector; use crate::safety::LeakDetector;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
/// Maximum response body size (5 MB). Prevents OOM from unbounded responses. /// Maximum response body size (5 MB). Prevents OOM from unbounded responses.
const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024; const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024;
@@ -154,17 +154,9 @@ impl Tool for HttpTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let method = params let method = require_str(&params, "method")?;
.get("method")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'method' parameter".to_string())
})?;
let url = params let url = require_str(&params, "url")?;
.get("url")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'url' parameter".to_string()))?;
let parsed_url = validate_url(url)?; let parsed_url = validate_url(url)?;
// Parse headers // Parse headers
+5 -19
View File
@@ -18,7 +18,7 @@ use crate::context::{ContextManager, JobContext, JobState};
use crate::db::Database; use crate::db::Database;
use crate::history::SandboxJobRecord; use crate::history::SandboxJobRecord;
use crate::orchestrator::job_manager::{ContainerJobManager, JobMode}; use crate::orchestrator::job_manager::{ContainerJobManager, JobMode};
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
/// Tool for creating a new job. /// Tool for creating a new job.
/// ///
@@ -467,17 +467,9 @@ impl Tool for CreateJobTool {
params: serde_json::Value, params: serde_json::Value,
ctx: &JobContext, ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let title = params let title = require_str(&params, "title")?;
.get("title")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'title' parameter".into()))?;
let description = params let description = require_str(&params, "description")?;
.get("description")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'description' parameter".into())
})?;
if self.sandbox_enabled() { if self.sandbox_enabled() {
let wait = params.get("wait").and_then(|v| v.as_bool()).unwrap_or(true); let wait = params.get("wait").and_then(|v| v.as_bool()).unwrap_or(true);
@@ -635,10 +627,7 @@ impl Tool for JobStatusTool {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let requester_id = ctx.user_id.clone(); let requester_id = ctx.user_id.clone();
let job_id_str = params let job_id_str = require_str(&params, "job_id")?;
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
let job_id = Uuid::parse_str(job_id_str).map_err(|_| { let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str)) ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
@@ -720,10 +709,7 @@ impl Tool for CancelJobTool {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let requester_id = ctx.user_id.clone(); let requester_id = ctx.user_id.clone();
let job_id_str = params let job_id_str = require_str(&params, "job_id")?;
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'job_id' parameter".into()))?;
let job_id = Uuid::parse_str(job_id_str).map_err(|_| { let job_id = Uuid::parse_str(job_id_str).map_err(|_| {
ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str)) ToolError::InvalidParameters(format!("invalid job ID format: {}", job_id_str))
+3 -10
View File
@@ -3,7 +3,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use crate::context::JobContext; use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_param, require_str};
/// Tool for JSON manipulation (parse, query, transform). /// Tool for JSON manipulation (parse, query, transform).
pub struct JsonTool; pub struct JsonTool;
@@ -46,16 +46,9 @@ impl Tool for JsonTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let operation = params let operation = require_str(&params, "operation")?;
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'operation' parameter".to_string())
})?;
let data = params let data = require_param(&params, "data")?;
.get("data")
.ok_or_else(|| ToolError::InvalidParameters("missing 'data' parameter".to_string()))?;
let result = match operation { let result = match operation {
"parse" => { "parse" => {
-160
View File
@@ -1,160 +0,0 @@
//! NEAR AI Marketplace tool.
use async_trait::async_trait;
use rust_decimal::Decimal;
use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput};
/// Tool for interacting with the NEAR AI marketplace.
pub struct MarketplaceTool {
// TODO: Add marketplace client
}
impl MarketplaceTool {
/// Create a new marketplace tool.
pub fn new() -> Self {
Self {}
}
}
impl Default for MarketplaceTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for MarketplaceTool {
fn name(&self) -> &str {
"marketplace"
}
fn description(&self) -> &str {
"Interact with the NEAR AI marketplace: search jobs, submit bids, deliver work."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search_jobs", "get_job", "submit_bid", "accept_job", "submit_work", "get_status"],
"description": "The marketplace action to perform"
},
"job_id": {
"type": "string",
"description": "Job ID (for get_job, submit_bid, accept_job, submit_work)"
},
"query": {
"type": "string",
"description": "Search query (for search_jobs)"
},
"category": {
"type": "string",
"description": "Job category filter (for search_jobs)"
},
"bid_amount": {
"type": "number",
"description": "Bid amount in NEAR (for submit_bid)"
},
"work_url": {
"type": "string",
"description": "URL to submitted work (for submit_work)"
},
"work_description": {
"type": "string",
"description": "Description of completed work (for submit_work)"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now();
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'action' parameter".to_string())
})?;
// TODO: Implement actual marketplace integration
let result = match action {
"search_jobs" => {
// Placeholder response
serde_json::json!({
"jobs": [],
"total": 0,
"message": "Marketplace integration not yet implemented"
})
}
"get_job" => {
let job_id = params
.get("job_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'job_id' parameter".to_string())
})?;
serde_json::json!({
"job_id": job_id,
"status": "not_found",
"message": "Marketplace integration not yet implemented"
})
}
"submit_bid" => {
serde_json::json!({
"success": false,
"message": "Marketplace integration not yet implemented"
})
}
"accept_job" => {
serde_json::json!({
"success": false,
"message": "Marketplace integration not yet implemented"
})
}
"submit_work" => {
serde_json::json!({
"success": false,
"message": "Marketplace integration not yet implemented"
})
}
"get_status" => {
serde_json::json!({
"connected": false,
"message": "Marketplace integration not yet implemented"
})
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"unknown action: {}",
action
)));
}
};
Ok(ToolOutput::success(result, start.elapsed()))
}
fn estimated_cost(&self, params: &serde_json::Value) -> Option<Decimal> {
// Bidding has a cost
if params.get("action").and_then(|v| v.as_str()) == Some("submit_bid") {
Some(Decimal::new(1, 2)) // 0.01 NEAR gas cost
} else {
None
}
}
fn requires_sanitization(&self) -> bool {
true // External marketplace data
}
}
+4 -15
View File
@@ -17,7 +17,7 @@ use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use crate::context::JobContext; use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
use crate::workspace::{Workspace, paths}; use crate::workspace::{Workspace, paths};
/// Identity files that the LLM must not overwrite via tool calls. /// Identity files that the LLM must not overwrite via tool calls.
@@ -81,10 +81,7 @@ impl Tool for MemorySearchTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let query = params let query = require_str(&params, "query")?;
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'query' parameter".to_string()))?;
let limit = params let limit = params
.get("limit") .get("limit")
@@ -176,12 +173,7 @@ impl Tool for MemoryWriteTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let content = params let content = require_str(&params, "content")?;
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'content' parameter".to_string())
})?;
if content.trim().is_empty() { if content.trim().is_empty() {
return Err(ToolError::InvalidParameters( return Err(ToolError::InvalidParameters(
@@ -337,10 +329,7 @@ impl Tool for MemoryReadTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let path = params let path = require_str(&params, "path")?;
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'path' parameter".to_string()))?;
let doc = self let doc = self
.workspace .workspace
-8
View File
@@ -1,22 +1,17 @@
//! Built-in tools that come with the agent. //! Built-in tools that come with the agent.
mod echo; mod echo;
mod ecommerce;
pub mod extension_tools; pub mod extension_tools;
mod file; mod file;
mod http; mod http;
mod job; mod job;
mod json; mod json;
mod marketplace;
mod memory; mod memory;
mod restaurant;
pub mod routine; pub mod routine;
pub(crate) mod shell; pub(crate) mod shell;
mod taskrabbit;
mod time; mod time;
pub use echo::EchoTool; pub use echo::EchoTool;
pub use ecommerce::EcommerceTool;
pub use extension_tools::{ pub use extension_tools::{
ToolActivateTool, ToolAuthTool, ToolInstallTool, ToolListTool, ToolRemoveTool, ToolSearchTool, ToolActivateTool, ToolAuthTool, ToolInstallTool, ToolListTool, ToolRemoveTool, ToolSearchTool,
}; };
@@ -24,12 +19,9 @@ pub use file::{ApplyPatchTool, ListDirTool, ReadFileTool, WriteFileTool};
pub use http::HttpTool; pub use http::HttpTool;
pub use job::{CancelJobTool, CreateJobTool, JobStatusTool, ListJobsTool}; pub use job::{CancelJobTool, CreateJobTool, JobStatusTool, ListJobsTool};
pub use json::JsonTool; pub use json::JsonTool;
pub use marketplace::MarketplaceTool;
pub use memory::{MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool}; pub use memory::{MemoryReadTool, MemorySearchTool, MemoryTreeTool, MemoryWriteTool};
pub use restaurant::RestaurantTool;
pub use routine::{ pub use routine::{
RoutineCreateTool, RoutineDeleteTool, RoutineHistoryTool, RoutineListTool, RoutineUpdateTool, RoutineCreateTool, RoutineDeleteTool, RoutineHistoryTool, RoutineListTool, RoutineUpdateTool,
}; };
pub use shell::ShellTool; pub use shell::ShellTool;
pub use taskrabbit::TaskRabbitTool;
pub use time::TimeTool; pub use time::TimeTool;
-172
View File
@@ -1,172 +0,0 @@
//! Restaurant reservation tool.
use async_trait::async_trait;
use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput};
/// Tool for restaurant reservations (OpenTable, Resy, etc.).
pub struct RestaurantTool {
// TODO: Add reservation API clients
}
impl RestaurantTool {
/// Create a new restaurant tool.
pub fn new() -> Self {
Self {}
}
}
impl Default for RestaurantTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for RestaurantTool {
fn name(&self) -> &str {
"restaurant"
}
fn description(&self) -> &str {
"Search restaurants, check availability, and make reservations via OpenTable, Resy, etc."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search", "check_availability", "make_reservation", "cancel_reservation", "get_reservation"],
"description": "The restaurant action to perform"
},
"query": {
"type": "string",
"description": "Search query (cuisine type, restaurant name, etc.)"
},
"location": {
"type": "object",
"properties": {
"city": { "type": "string" },
"neighborhood": { "type": "string" },
"latitude": { "type": "number" },
"longitude": { "type": "number" }
},
"description": "Location to search near"
},
"date": {
"type": "string",
"description": "Reservation date (YYYY-MM-DD)"
},
"time": {
"type": "string",
"description": "Preferred time (HH:MM)"
},
"party_size": {
"type": "integer",
"description": "Number of guests"
},
"restaurant_id": {
"type": "string",
"description": "Restaurant ID (for check_availability, make_reservation)"
},
"reservation_id": {
"type": "string",
"description": "Reservation ID (for cancel_reservation, get_reservation)"
},
"guest_name": {
"type": "string",
"description": "Name for the reservation"
},
"guest_phone": {
"type": "string",
"description": "Phone number for the reservation"
},
"guest_email": {
"type": "string",
"description": "Email for the reservation"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now();
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'action' parameter".to_string())
})?;
// TODO: Implement actual restaurant reservation API integrations
let result = match action {
"search" => {
let query = params.get("query").and_then(|v| v.as_str()).unwrap_or("");
serde_json::json!({
"query": query,
"restaurants": [],
"message": "Restaurant integration not yet implemented"
})
}
"check_availability" => {
let restaurant_id = params
.get("restaurant_id")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters(
"missing 'restaurant_id' parameter".to_string(),
)
})?;
serde_json::json!({
"restaurant_id": restaurant_id,
"available_times": [],
"message": "Restaurant integration not yet implemented"
})
}
"make_reservation" => {
serde_json::json!({
"success": false,
"message": "Restaurant integration not yet implemented"
})
}
"cancel_reservation" => {
serde_json::json!({
"cancelled": false,
"message": "Restaurant integration not yet implemented"
})
}
"get_reservation" => {
let reservation_id = params.get("reservation_id").and_then(|v| v.as_str());
serde_json::json!({
"reservation_id": reservation_id,
"found": false,
"message": "Restaurant integration not yet implemented"
})
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"unknown action: {}",
action
)));
}
};
Ok(ToolOutput::success(result, start.elapsed()))
}
fn requires_sanitization(&self) -> bool {
true // External restaurant data
}
}
+7 -25
View File
@@ -20,7 +20,7 @@ use crate::agent::routine::{
use crate::agent::routine_engine::RoutineEngine; use crate::agent::routine_engine::RoutineEngine;
use crate::context::JobContext; use crate::context::JobContext;
use crate::db::Database; use crate::db::Database;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
// ==================== routine_create ==================== // ==================== routine_create ====================
@@ -106,25 +106,16 @@ impl Tool for RoutineCreateTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'name'".to_string()))?;
let description = params let description = params
.get("description") .get("description")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or(""); .unwrap_or("");
let trigger_type = params let trigger_type = require_str(&params, "trigger_type")?;
.get("trigger_type")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'trigger_type'".to_string()))?;
let prompt = params let prompt = require_str(&params, "prompt")?;
.get("prompt")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'prompt'".to_string()))?;
// Build trigger // Build trigger
let trigger = match trigger_type { let trigger = match trigger_type {
@@ -408,10 +399,7 @@ impl Tool for RoutineUpdateTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'name'".to_string()))?;
let mut routine = self let mut routine = self
.store .store
@@ -514,10 +502,7 @@ impl Tool for RoutineDeleteTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'name'".to_string()))?;
let routine = self let routine = self
.store .store
@@ -595,10 +580,7 @@ impl Tool for RoutineHistoryTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let name = params let name = require_str(&params, "name")?;
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'name'".to_string()))?;
let limit = params let limit = params
.get("limit") .get("limit")
+2 -5
View File
@@ -30,7 +30,7 @@ use tokio::process::Command;
use crate::context::JobContext; use crate::context::JobContext;
use crate::sandbox::{SandboxManager, SandboxPolicy}; use crate::sandbox::{SandboxManager, SandboxPolicy};
use crate::tools::tool::{Tool, ToolDomain, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolDomain, ToolError, ToolOutput, require_str};
/// Maximum output size before truncation (64KB). /// Maximum output size before truncation (64KB).
const MAX_OUTPUT_SIZE: usize = 64 * 1024; const MAX_OUTPUT_SIZE: usize = 64 * 1024;
@@ -401,10 +401,7 @@ impl Tool for ShellTool {
params: serde_json::Value, params: serde_json::Value,
_ctx: &JobContext, _ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let command = params let command = require_str(&params, "command")?;
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters("missing 'command' parameter".into()))?;
let workdir = params.get("workdir").and_then(|v| v.as_str()); let workdir = params.get("workdir").and_then(|v| v.as_str());
let timeout = params.get("timeout").and_then(|v| v.as_u64()); let timeout = params.get("timeout").and_then(|v| v.as_u64());
-157
View File
@@ -1,157 +0,0 @@
//! TaskRabbit tool for real-world task delegation.
use async_trait::async_trait;
use rust_decimal::Decimal;
use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput};
/// Tool for delegating real-world tasks via TaskRabbit.
pub struct TaskRabbitTool {
// TODO: Add TaskRabbit API client
}
impl TaskRabbitTool {
/// Create a new TaskRabbit tool.
pub fn new() -> Self {
Self {}
}
}
impl Default for TaskRabbitTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for TaskRabbitTool {
fn name(&self) -> &str {
"taskrabbit"
}
fn description(&self) -> &str {
"Delegate real-world tasks to TaskRabbit taskers (delivery, assembly, cleaning, etc.)."
}
fn parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["search_taskers", "get_quote", "book_task", "get_status", "cancel_task"],
"description": "The TaskRabbit action to perform"
},
"task_type": {
"type": "string",
"enum": ["delivery", "assembly", "moving", "cleaning", "handyman", "other"],
"description": "Type of task"
},
"description": {
"type": "string",
"description": "Detailed description of the task"
},
"location": {
"type": "object",
"properties": {
"address": { "type": "string" },
"city": { "type": "string" },
"state": { "type": "string" },
"zip": { "type": "string" }
},
"description": "Location for the task"
},
"scheduled_time": {
"type": "string",
"description": "ISO 8601 datetime for when the task should be performed"
},
"budget": {
"type": "number",
"description": "Maximum budget for the task in USD"
},
"task_id": {
"type": "string",
"description": "Task ID (for get_status, cancel_task)"
}
},
"required": ["action"]
})
}
async fn execute(
&self,
params: serde_json::Value,
_ctx: &JobContext,
) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now();
let action = params
.get("action")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'action' parameter".to_string())
})?;
// TODO: Implement actual TaskRabbit API integration
let result = match action {
"search_taskers" => {
serde_json::json!({
"taskers": [],
"message": "TaskRabbit integration not yet implemented"
})
}
"get_quote" => {
serde_json::json!({
"quotes": [],
"message": "TaskRabbit integration not yet implemented"
})
}
"book_task" => {
serde_json::json!({
"booked": false,
"message": "TaskRabbit integration not yet implemented"
})
}
"get_status" => {
let task_id = params.get("task_id").and_then(|v| v.as_str());
serde_json::json!({
"task_id": task_id,
"status": "unknown",
"message": "TaskRabbit integration not yet implemented"
})
}
"cancel_task" => {
serde_json::json!({
"cancelled": false,
"message": "TaskRabbit integration not yet implemented"
})
}
_ => {
return Err(ToolError::InvalidParameters(format!(
"unknown action: {}",
action
)));
}
};
Ok(ToolOutput::success(result, start.elapsed()))
}
fn estimated_cost(&self, params: &serde_json::Value) -> Option<Decimal> {
// Booking a task has associated costs
if params.get("action").and_then(|v| v.as_str()) == Some("book_task") {
params
.get("budget")
.and_then(|v| v.as_f64())
.map(|b| Decimal::try_from(b).unwrap_or_default())
} else {
None
}
}
fn requires_sanitization(&self) -> bool {
true // External TaskRabbit data
}
}
+5 -25
View File
@@ -4,7 +4,7 @@ use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use crate::context::JobContext; use crate::context::JobContext;
use crate::tools::tool::{Tool, ToolError, ToolOutput}; use crate::tools::tool::{Tool, ToolError, ToolOutput, require_str};
/// Tool for getting current time and date operations. /// Tool for getting current time and date operations.
pub struct TimeTool; pub struct TimeTool;
@@ -52,12 +52,7 @@ impl Tool for TimeTool {
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let operation = params let operation = require_str(&params, "operation")?;
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'operation' parameter".to_string())
})?;
let result = match operation { let result = match operation {
"now" => { "now" => {
@@ -69,12 +64,7 @@ impl Tool for TimeTool {
}) })
} }
"parse" => { "parse" => {
let timestamp = params let timestamp = require_str(&params, "timestamp")?;
.get("timestamp")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'timestamp' parameter".to_string())
})?;
let dt: DateTime<Utc> = timestamp.parse().map_err(|e| { let dt: DateTime<Utc> = timestamp.parse().map_err(|e| {
ToolError::InvalidParameters(format!("invalid timestamp: {}", e)) ToolError::InvalidParameters(format!("invalid timestamp: {}", e))
@@ -87,19 +77,9 @@ impl Tool for TimeTool {
}) })
} }
"diff" => { "diff" => {
let ts1 = params let ts1 = require_str(&params, "timestamp")?;
.get("timestamp")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'timestamp' parameter".to_string())
})?;
let ts2 = params let ts2 = require_str(&params, "timestamp2")?;
.get("timestamp2")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'timestamp2' parameter".to_string())
})?;
let dt1: DateTime<Utc> = ts1.parse().map_err(|e| { let dt1: DateTime<Utc> = ts1.parse().map_err(|e| {
ToolError::InvalidParameters(format!("invalid timestamp: {}", e)) ToolError::InvalidParameters(format!("invalid timestamp: {}", e))
+59 -6
View File
@@ -199,6 +199,28 @@ pub trait Tool: Send + Sync {
} }
} }
/// Extract a required string parameter from a JSON object.
///
/// Returns `ToolError::InvalidParameters` if the key is missing or not a string.
pub fn require_str<'a>(params: &'a serde_json::Value, name: &str) -> Result<&'a str, ToolError> {
params
.get(name)
.and_then(|v| v.as_str())
.ok_or_else(|| ToolError::InvalidParameters(format!("missing '{}' parameter", name)))
}
/// Extract a required parameter of any type from a JSON object.
///
/// Returns `ToolError::InvalidParameters` if the key is missing.
pub fn require_param<'a>(
params: &'a serde_json::Value,
name: &str,
) -> Result<&'a serde_json::Value, ToolError> {
params
.get(name)
.ok_or_else(|| ToolError::InvalidParameters(format!("missing '{}' parameter", name)))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -235,12 +257,7 @@ mod tests {
params: serde_json::Value, params: serde_json::Value,
_ctx: &JobContext, _ctx: &JobContext,
) -> Result<ToolOutput, ToolError> { ) -> Result<ToolOutput, ToolError> {
let message = params let message = require_str(&params, "message")?;
.get("message")
.and_then(|v| v.as_str())
.ok_or_else(|| {
ToolError::InvalidParameters("missing 'message' parameter".to_string())
})?;
Ok(ToolOutput::text(message, Duration::from_millis(1))) Ok(ToolOutput::text(message, Duration::from_millis(1)))
} }
@@ -277,4 +294,40 @@ mod tests {
let tool = EchoTool; let tool = EchoTool;
assert_eq!(tool.execution_timeout(), Duration::from_secs(60)); assert_eq!(tool.execution_timeout(), Duration::from_secs(60));
} }
#[test]
fn test_require_str_present() {
let params = serde_json::json!({"name": "alice"});
assert_eq!(require_str(&params, "name").unwrap(), "alice");
}
#[test]
fn test_require_str_missing() {
let params = serde_json::json!({});
let err = require_str(&params, "name").unwrap_err();
assert!(err.to_string().contains("missing 'name'"));
}
#[test]
fn test_require_str_wrong_type() {
let params = serde_json::json!({"name": 42});
let err = require_str(&params, "name").unwrap_err();
assert!(err.to_string().contains("missing 'name'"));
}
#[test]
fn test_require_param_present() {
let params = serde_json::json!({"data": [1, 2, 3]});
assert_eq!(
require_param(&params, "data").unwrap(),
&serde_json::json!([1, 2, 3])
);
}
#[test]
fn test_require_param_missing() {
let params = serde_json::json!({});
let err = require_param(&params, "data").unwrap_err();
assert!(err.to_string().contains("missing 'data'"));
}
} }
+54 -70
View File
@@ -129,11 +129,15 @@ impl WorkerHttpClient {
format!("{}/worker/{}/{}", self.orchestrator_url, self.job_id, path) format!("{}/worker/{}/{}", self.orchestrator_url, self.job_id, path)
} }
/// Fetch the job description from the orchestrator. /// Send a GET request, check the status, and deserialize the JSON body.
pub async fn get_job(&self) -> Result<JobDescription, WorkerError> { async fn get_json<T: serde::de::DeserializeOwned>(
&self,
path: &str,
context: &str,
) -> Result<T, WorkerError> {
let resp = self let resp = self
.client .client
.get(self.url("job")) .get(self.url(path))
.bearer_auth(&self.token) .bearer_auth(&self.token)
.send() .send()
.await .await
@@ -145,15 +149,51 @@ impl WorkerHttpClient {
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(WorkerError::OrchestratorRejected { return Err(WorkerError::OrchestratorRejected {
job_id: self.job_id, job_id: self.job_id,
reason: format!("GET /job returned {}", resp.status()), reason: format!("{} returned {}", context, resp.status()),
}); });
} }
resp.json().await.map_err(|e| WorkerError::LlmProxyFailed { resp.json().await.map_err(|e| WorkerError::LlmProxyFailed {
reason: format!("failed to parse job description: {}", e), reason: format!("{}: failed to parse response: {}", context, e),
}) })
} }
/// Send a POST request with a JSON body, check the status, and deserialize the response.
async fn post_json<B: Serialize, T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &B,
context: &str,
) -> Result<T, WorkerError> {
let resp = self
.client
.post(self.url(path))
.bearer_auth(&self.token)
.json(body)
.send()
.await
.map_err(|e| WorkerError::LlmProxyFailed {
reason: format!("{}: {}", context, e),
})?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(WorkerError::LlmProxyFailed {
reason: format!("{}: orchestrator returned {}: {}", context, status, body),
});
}
resp.json().await.map_err(|e| WorkerError::LlmProxyFailed {
reason: format!("{}: failed to parse response: {}", context, e),
})
}
/// Fetch the job description from the orchestrator.
pub async fn get_job(&self) -> Result<JobDescription, WorkerError> {
self.get_json("job", "GET /job").await
}
/// Proxy an LLM completion request through the orchestrator. /// Proxy an LLM completion request through the orchestrator.
pub async fn llm_complete( pub async fn llm_complete(
&self, &self,
@@ -166,29 +206,9 @@ impl WorkerHttpClient {
stop_sequences: request.stop_sequences.clone(), stop_sequences: request.stop_sequences.clone(),
}; };
let resp = self let proxy_resp: ProxyCompletionResponse = self
.client .post_json("llm/complete", &proxy_req, "LLM complete")
.post(self.url("llm/complete")) .await?;
.bearer_auth(&self.token)
.json(&proxy_req)
.send()
.await
.map_err(|e| WorkerError::LlmProxyFailed {
reason: e.to_string(),
})?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(WorkerError::LlmProxyFailed {
reason: format!("orchestrator returned {}: {}", status, body),
});
}
let proxy_resp: ProxyCompletionResponse =
resp.json().await.map_err(|e| WorkerError::LlmProxyFailed {
reason: format!("failed to parse LLM response: {}", e),
})?;
Ok(CompletionResponse { Ok(CompletionResponse {
content: proxy_resp.content, content: proxy_resp.content,
@@ -212,29 +232,9 @@ impl WorkerHttpClient {
tool_choice: request.tool_choice.clone(), tool_choice: request.tool_choice.clone(),
}; };
let resp = self let proxy_resp: ProxyToolCompletionResponse = self
.client .post_json("llm/complete_with_tools", &proxy_req, "LLM tool complete")
.post(self.url("llm/complete_with_tools")) .await?;
.bearer_auth(&self.token)
.json(&proxy_req)
.send()
.await
.map_err(|e| WorkerError::LlmProxyFailed {
reason: e.to_string(),
})?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(WorkerError::LlmProxyFailed {
reason: format!("orchestrator returned {}: {}", status, body),
});
}
let proxy_resp: ProxyToolCompletionResponse =
resp.json().await.map_err(|e| WorkerError::LlmProxyFailed {
reason: format!("failed to parse tool completion response: {}", e),
})?;
Ok(ToolCompletionResponse { Ok(ToolCompletionResponse {
content: proxy_resp.content, content: proxy_resp.content,
@@ -337,25 +337,9 @@ impl WorkerHttpClient {
/// Signal job completion to the orchestrator. /// Signal job completion to the orchestrator.
pub async fn report_complete(&self, report: &CompletionReport) -> Result<(), WorkerError> { pub async fn report_complete(&self, report: &CompletionReport) -> Result<(), WorkerError> {
let resp = self let _: serde_json::Value = self
.client .post_json("complete", report, "report complete")
.post(self.url("complete")) .await?;
.bearer_auth(&self.token)
.json(report)
.send()
.await
.map_err(|e| WorkerError::ConnectionFailed {
url: self.orchestrator_url.clone(),
reason: e.to_string(),
})?;
if !resp.status().is_success() {
return Err(WorkerError::OrchestratorRejected {
job_id: self.job_id,
reason: format!("completion report rejected: {}", resp.status()),
});
}
Ok(()) Ok(())
} }
} }