Refactor syscall modules to use async/await patterns
- Updated `DisplaySyscall` to use `tokio::io::AsyncWriteExt` for asynchronous text output. - Refactored `FsSyscall` to read files asynchronously with offset and length handling. - Modified `MemorySyscall` to use `tokio::task::spawn_blocking` for database operations, allowing async access to SQLite. - Enhanced `NetworkSyscall` to utilize `reqwest` for HTTP requests, replacing the previous `curl` command execution. - Improved `ProcessSyscall` to manage subprocesses with async tasks for stdin, stdout, and stderr handling. - Updated `TimerSyscall` to simplify timer management. - Adjusted plugin implementations for better async support and error handling. - Added `tokio-stream` and `tracing-subscriber` dependencies to `Cargo.toml` for enhanced async stream handling and logging.
This commit is contained in:
@@ -304,7 +304,7 @@ impl CheckRunner {
|
||||
.into_inner();
|
||||
let names: Vec<&str> = list.entries.iter().map(|e| e.name.as_str()).collect();
|
||||
ensure!(
|
||||
list.ok && names.iter().any(|name| *name == "sample.txt"),
|
||||
list.ok && names.contains(&"sample.txt"),
|
||||
"list mismatch ok={} names={:?} error={:?}",
|
||||
list.ok,
|
||||
names,
|
||||
@@ -341,12 +341,13 @@ impl CheckRunner {
|
||||
|
||||
async fn process_roundtrip(&mut self) -> Result<String> {
|
||||
let mut process = ProcessClient::new(self.channel.clone());
|
||||
let (cmd, args) = process_command();
|
||||
let resp = process
|
||||
.spawn(request_with_plugin(
|
||||
PLUGIN_ID,
|
||||
SpawnRequest {
|
||||
cmd: "cmd".to_string(),
|
||||
args: vec!["/C".to_string(), format!("echo {PROCESS_OK}")],
|
||||
cmd,
|
||||
args,
|
||||
env: HashMap::new(),
|
||||
cwd: executable_dir()
|
||||
.map(|path| path_string(&path))
|
||||
@@ -504,6 +505,20 @@ fn executable_data_dir() -> PathBuf {
|
||||
.join("data")
|
||||
}
|
||||
|
||||
fn process_command() -> (String, Vec<String>) {
|
||||
if cfg!(windows) {
|
||||
(
|
||||
"cmd".to_string(),
|
||||
vec!["/C".to_string(), format!("echo {PROCESS_OK}")],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"sh".to_string(),
|
||||
vec!["-c".to_string(), format!("printf %s {PROCESS_OK}")],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn path_string(path: &std::path::Path) -> String {
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
tonic = { workspace = true }
|
||||
futures-core = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
@@ -12,7 +12,7 @@ use tonic::{transport::Server, Request, Response, Status};
|
||||
const PLUGIN_ID: &str = "claudecode";
|
||||
const DEFAULT_PORT: u16 = 50053;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct CallArgs {
|
||||
#[serde(default)]
|
||||
prompt: String,
|
||||
@@ -32,26 +32,22 @@ fn find_claude_bin() -> Option<String> {
|
||||
return Some(bin);
|
||||
}
|
||||
|
||||
let candidates = vec![
|
||||
"claude",
|
||||
"claude.cmd",
|
||||
&std::env::var("APPDATA")
|
||||
.map(|p| format!("{}\\npm\\claude.cmd", p))
|
||||
.unwrap_or_default(),
|
||||
];
|
||||
let appdata_claude = std::env::var("APPDATA")
|
||||
.ok()
|
||||
.map(|p| format!("{}\\npm\\claude.cmd", p));
|
||||
let mut candidates = vec!["claude".to_string(), "claude.cmd".to_string()];
|
||||
if let Some(path) = appdata_claude {
|
||||
candidates.push(path);
|
||||
}
|
||||
|
||||
for candidate in candidates {
|
||||
if std::process::Command::new(candidate)
|
||||
candidates.into_iter().find(|candidate| {
|
||||
std::process::Command::new(candidate)
|
||||
.arg("--version")
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok()
|
||||
{
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn call_claude(
|
||||
@@ -97,9 +93,12 @@ async fn call_claude(
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
let output = tokio::time::timeout(std::time::Duration::from_secs(300), child.wait_with_output())
|
||||
.await
|
||||
.context("claude timeout (300s)")??;
|
||||
let output = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(300),
|
||||
child.wait_with_output(),
|
||||
)
|
||||
.await
|
||||
.context("claude timeout (300s)")??;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
@@ -148,8 +147,10 @@ impl ClaudeCodePlugin {
|
||||
.await
|
||||
}
|
||||
"review" => {
|
||||
let review_prompt =
|
||||
format!("Review the following code or changes. Be concise and actionable:\n\n{}", prompt);
|
||||
let review_prompt = format!(
|
||||
"Review the following code or changes. Be concise and actionable:\n\n{}",
|
||||
prompt
|
||||
);
|
||||
call_claude(
|
||||
&self.claude_bin,
|
||||
&review_prompt,
|
||||
@@ -184,7 +185,10 @@ impl ClaudeCodePlugin {
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl PluginBridge for ClaudeCodePlugin {
|
||||
async fn register(&self, _req: Request<PluginInfo>) -> Result<Response<RegisterResponse>, Status> {
|
||||
async fn register(
|
||||
&self,
|
||||
_req: Request<PluginInfo>,
|
||||
) -> Result<Response<RegisterResponse>, Status> {
|
||||
Err(Status::unimplemented("not a bridge"))
|
||||
}
|
||||
|
||||
@@ -195,13 +199,19 @@ impl PluginBridge for ClaudeCodePlugin {
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamCallStream = futures_core::stream::Empty<Result<CallResponse, Status>>;
|
||||
type StreamCallStream = tokio_stream::Empty<Result<CallResponse, Status>>;
|
||||
|
||||
async fn stream_call(&self, _req: Request<CallRequest>) -> Result<Response<Self::StreamCallStream>, Status> {
|
||||
async fn stream_call(
|
||||
&self,
|
||||
_req: Request<CallRequest>,
|
||||
) -> Result<Response<Self::StreamCallStream>, Status> {
|
||||
Err(Status::unimplemented("stream_call not supported"))
|
||||
}
|
||||
|
||||
async fn heartbeat(&self, _req: Request<HeartbeatRequest>) -> Result<Response<HeartbeatResponse>, Status> {
|
||||
async fn heartbeat(
|
||||
&self,
|
||||
_req: Request<HeartbeatRequest>,
|
||||
) -> Result<Response<HeartbeatResponse>, Status> {
|
||||
Ok(Response::new(HeartbeatResponse { ok: true }))
|
||||
}
|
||||
}
|
||||
@@ -210,7 +220,8 @@ impl PluginBridge for ClaudeCodePlugin {
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let claude_bin = find_claude_bin().context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
|
||||
let claude_bin = find_claude_bin()
|
||||
.context("Claude Code CLI not found. Install: npm install -g @anthropic-ai/claude-code")?;
|
||||
tracing::info!("Found Claude Code CLI: {}", claude_bin);
|
||||
|
||||
let endpoint = agentsd_plugin_sdk::plugin_endpoint_from_env(DEFAULT_PORT);
|
||||
@@ -224,21 +235,42 @@ async fn main() -> Result<()> {
|
||||
"Claude Code",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"standard",
|
||||
&["process.spawn", "display.text", "fs.read", "fs.write", "network.request"],
|
||||
&[
|
||||
"process.spawn",
|
||||
"display.text",
|
||||
"fs.read",
|
||||
"fs.write",
|
||||
"network.request",
|
||||
],
|
||||
&["hermes"],
|
||||
vec![
|
||||
export("chat", "Send prompt to Claude Code, get text response (no tools)"),
|
||||
export("code", "Generate or edit code with Claude Code (Bash/Read/Edit/Write)"),
|
||||
export(
|
||||
"chat",
|
||||
"Send prompt to Claude Code, get text response (no tools)",
|
||||
),
|
||||
export(
|
||||
"code",
|
||||
"Generate or edit code with Claude Code (Bash/Read/Edit/Write)",
|
||||
),
|
||||
export("review", "Review code changes (Read/Glob/Grep only)"),
|
||||
export("ask", "Ask about the codebase (Read/Glob/Grep, max 3 turns)"),
|
||||
export("run", "Full Claude Code access (all tools, unlimited turns)"),
|
||||
export(
|
||||
"ask",
|
||||
"Ask about the codebase (Read/Glob/Grep, max 3 turns)",
|
||||
),
|
||||
export(
|
||||
"run",
|
||||
"Full Claude Code access (all tools, unlimited turns)",
|
||||
),
|
||||
],
|
||||
&endpoint,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
tracing::info!("Registered with agentsd: session={}", register_resp.session_id);
|
||||
tracing::info!(
|
||||
"Registered with agentsd: session={}",
|
||||
register_resp.session_id
|
||||
);
|
||||
|
||||
let plugin = ClaudeCodePlugin::new(claude_bin);
|
||||
let addr = listen_addr.parse()?;
|
||||
|
||||
@@ -26,7 +26,10 @@ async fn main() -> Result<()> {
|
||||
.await?
|
||||
.into_inner();
|
||||
ensure!(register.ok, "register failed: {}", register.error);
|
||||
println!("Register: ok={} session={}", register.ok, register.session_id);
|
||||
println!(
|
||||
"Register: ok={} session={}",
|
||||
register.ok, register.session_id
|
||||
);
|
||||
|
||||
let mut display = DisplayClient::new(channel.clone());
|
||||
let resp = display
|
||||
@@ -84,7 +87,11 @@ async fn main() -> Result<()> {
|
||||
ensure!(resp.ok, "FS.List failed: {}", resp.error);
|
||||
println!("FS.List: ok={} entries={}", resp.ok, resp.entries.len());
|
||||
for entry in resp.entries.iter().take(5) {
|
||||
println!(" {} {}", entry.name, if entry.is_dir { "[dir]" } else { "" });
|
||||
println!(
|
||||
" {} {}",
|
||||
entry.name,
|
||||
if entry.is_dir { "[dir]" } else { "" }
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nAll syscalls verified OK!");
|
||||
|
||||
@@ -44,7 +44,9 @@ impl PluginBridge for HermesPlugin {
|
||||
&self,
|
||||
_req: Request<PluginInfo>,
|
||||
) -> std::result::Result<Response<RegisterResponse>, Status> {
|
||||
Err(Status::unimplemented("hermes plugin is not an agentsd bridge"))
|
||||
Err(Status::unimplemented(
|
||||
"hermes plugin is not an agentsd bridge",
|
||||
))
|
||||
}
|
||||
|
||||
async fn call(
|
||||
@@ -62,13 +64,16 @@ impl PluginBridge for HermesPlugin {
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamCallStream = tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>;
|
||||
type StreamCallStream =
|
||||
tokio_stream::wrappers::ReceiverStream<std::result::Result<CallResponse, Status>>;
|
||||
|
||||
async fn stream_call(
|
||||
&self,
|
||||
_req: Request<CallRequest>,
|
||||
) -> std::result::Result<Response<Self::StreamCallStream>, Status> {
|
||||
Err(Status::unimplemented("stream_call is not implemented for hermes"))
|
||||
Err(Status::unimplemented(
|
||||
"stream_call is not implemented for hermes",
|
||||
))
|
||||
}
|
||||
|
||||
async fn heartbeat(
|
||||
@@ -110,7 +115,7 @@ async fn main() -> Result<()> {
|
||||
println!("Hermes PluginBridge server listening on {}", listen_addr);
|
||||
|
||||
Server::builder()
|
||||
.add_service(PluginBridgeServer::new(HermesPlugin::default()))
|
||||
.add_service(PluginBridgeServer::new(HermesPlugin))
|
||||
.serve_with_incoming(incoming)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -59,6 +59,7 @@ pub fn plugin_info(
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn plugin_info_with_endpoint(
|
||||
id: &str,
|
||||
name: &str,
|
||||
@@ -95,7 +96,11 @@ pub fn encode_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
|
||||
serde_json::to_vec(value).context("encode JSON")
|
||||
}
|
||||
|
||||
pub fn call_response_json<T: Serialize>(value: &T, ok: bool, error: impl Into<String>) -> CallResponse {
|
||||
pub fn call_response_json<T: Serialize>(
|
||||
value: &T,
|
||||
ok: bool,
|
||||
error: impl Into<String>,
|
||||
) -> CallResponse {
|
||||
match encode_json(value) {
|
||||
Ok(result) => CallResponse {
|
||||
result,
|
||||
|
||||
Reference in New Issue
Block a user