Files
openhuman/src/openhuman/tools/read_diff.rs
T
9b73d00b2e fix(agent): inject skill tools into agent registry and unify ToolResult (#341) (#360)
* fix(agent): inject skill tools into agent registry and unify ToolResult (#341)

Skill tools (Notion, Gmail, etc.) from the QuickJS runtime were invisible
to the agent's tool loop — the LLM could never call them. This change:

1. Adds SkillToolBridge to wrap skill ToolDefinitions as Tool trait impls
   and inject them into the agent's tool registry via collect_skill_tools().

2. Unifies ToolResult across built-in and skill tools — both now use the
   MCP-spec type (content blocks + is_error) from skills::types, eliminating
   the need for result conversion between the two systems.

3. Adds convenience constructors (ToolResult::success/error/json) and
   accessor methods (output/text) to simplify all tool implementations.

4. Adds diagnostic logging in the tool loop for tool registry contents
   and tool lookup results.

Verified: agent_chat RPC successfully called Notion list-all-pages tool
and returned real workspace data through the agent loop.

Closes #341

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(tools): address CodeRabbit review feedback on PR #360

- cron_run: preserve execution details (result_output) on failure
- delegate: fix mismatched bracket in agent response header format string
- run_linter: include lint diagnostics in error output, not just exit code
- run_tests: include test output in error result, not just exit code
- skill_bridge: prevent namespaced tool-name collisions with dedup + __
  delimiter validation; sanitize runtime errors in model-facing output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt to skill_bridge.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:03:07 -07:00

105 lines
3.1 KiB
Rust

//! Tool: read_diff — structured git diff output for the Critic archetype.
use super::traits::{PermissionLevel, Tool, ToolResult};
use async_trait::async_trait;
use serde_json::json;
use std::path::PathBuf;
/// Returns `git diff` output in a structured format.
pub struct ReadDiffTool {
workspace_dir: PathBuf,
}
impl ReadDiffTool {
pub fn new(workspace_dir: PathBuf) -> Self {
Self { workspace_dir }
}
}
#[async_trait]
impl Tool for ReadDiffTool {
fn name(&self) -> &str {
"read_diff"
}
fn description(&self) -> &str {
"Get the git diff of current changes. Can diff staged, unstaged, or against a \
specific base branch/commit. Returns file paths and hunks."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"base": {
"type": "string",
"description": "Base ref to diff against (e.g. 'main', 'HEAD~3'). Default: unstaged changes."
},
"staged": {
"type": "boolean",
"description": "Show staged changes only (--cached). Default: false."
},
"path_filter": {
"type": "string",
"description": "Limit diff to a specific path or glob."
}
}
})
}
fn permission_level(&self) -> PermissionLevel {
PermissionLevel::ReadOnly
}
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult> {
let base = args.get("base").and_then(|v| v.as_str());
let staged = args
.get("staged")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let path_filter = args.get("path_filter").and_then(|v| v.as_str());
let mut git_args = vec!["diff", "--stat", "-p"];
if staged {
git_args.push("--cached");
}
let base_str = base.map(|b| b.to_string());
if let Some(ref bs) = base_str {
git_args.push(bs);
}
if let Some(pf) = path_filter {
git_args.push("--");
git_args.push(pf);
}
tracing::debug!(
workspace = %self.workspace_dir.display(),
?git_args,
"[read_diff] running git diff"
);
let output = tokio::process::Command::new("git")
.args(&git_args)
.current_dir(&self.workspace_dir)
.output()
.await?;
if output.status.success() {
let diff = String::from_utf8_lossy(&output.stdout);
tracing::debug!("[read_diff] success, diff length={}", diff.len());
if diff.trim().is_empty() {
Ok(ToolResult::success("No changes found."))
} else {
Ok(ToolResult::success(diff.to_string()))
}
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::debug!("[read_diff] failed: {stderr}");
Ok(ToolResult::error(stderr.to_string()))
}
}
}