openpencil/crates/copilot-sdk/src/error.rs

158 lines
4.2 KiB
Rust
Raw Normal View History

chore(vendor): add anthropic-agent-sdk + copilot-sdk-rust as IPC source-of-truth User direction: instead of hand-rolling subprocess JSON bridges in `chat_subprocess.rs`, pull the community SDKs into vendor/ + adapt them. Both repos are MIT-licensed Rust SDKs purpose-built for their respective CLIs and ship more capable wire-protocol parsers than the generic line-based approach in this branch's HEAD. `vendor/anthropic-agent-sdk/` (was bartolli/anthropic-agent-sdk @ main, 2026-05-14): - SubprocessTransport for `claude --print --verbose --output-format stream-json -- <prompt>` - Recognized message envelope (system / user / assistant / result shapes per Claude Code's documented headless protocol) - Binary-lookup fallback through ~/.npm-global/bin, /usr/local/bin, ~/.local/bin, ~/node_modules/.bin, ~/.yarn/bin (via `which` + manual probe) - Dangerous-env-var scrub (LD_PRELOAD / DYLD_INSERT_LIBRARIES / NODE_OPTIONS / ...) for spawn safety - CancellationToken-based abort wiring - Trimmed locally: removed examples/, demos/, tests/, docs/, .git/. Inner `[workspace]` block stripped so OP's root workspace owns the build. `typed-builder` pinned to `=0.21.0` because upstream's `0.23.2` uses stable `let`-chains (Rust 1.88+) and OP rust-toolchain is 1.85 to stay compatible with the skia-safe-op fork. `vendor/copilot-sdk-rust/` (was copilot-community-sdk/copilot-sdk-rust @ main, 2026-05-14): - LSP-style Content-Length-framed JSON-RPC over stdio for `gh copilot` (the new community CLI that succeeds the legacy `gh-copilot suggest` subcommand) - Client + Session abstraction with event subscription (`AssistantMessage` / `SessionIdle` / tool-use events) - Trimmed: examples/, tests/, .git/ removed. Cargo.toml unchanged (already 2021 edition + 1.85 rust-version + no problematic deps). Workspace integration: - Both directories appear in OP root Cargo.toml's `exclude` list so `cargo build --workspace` doesn't try to compile them (each declares its own `edition` / `rust-version` distinct from OP). - openpencil-desktop will consume them via target-gated path deps in the next commit + replace the hand-rolled provider in `chat_subprocess.rs` with thin adapters that route per CliName: Claude Code → anthropic_agent_sdk::SubprocessTransport Copilot → copilot_sdk::Client + Session Gemini → keep the generic stdin/stdout bridge (no upstream Rust SDK exists yet for the gemini CLI) Codex / OpenCode → HttpServer bridge (separate, spawn `<bin> serve` + connect to local 127.0.0.1:port) Per the user clarification "opencode 和 codex 我们调用 http server, 通过 ipc 启动本地的 server 模式": Codex + OpenCode stay on the HttpServer path even though they're also spawned subprocesses — the local server is what we IPC with via HTTP, not their stdio. Standalone build verified for both vendored crates: ✓ check passes on rustc 1.85.1 (this host).
2026-05-14 08:36:27 +00:00
// Copyright (c) 2026 Elias Bachaalany
// SPDX-License-Identifier: MIT
//! Error types for the Copilot SDK.
use std::time::Duration;
use thiserror::Error;
/// Main error type for the Copilot SDK.
#[derive(Debug, Error)]
pub enum CopilotError {
/// Transport/IO error
#[error("Transport error: {0}")]
Transport(#[from] std::io::Error),
/// Connection was closed unexpectedly
#[error("Connection closed")]
ConnectionClosed,
/// Client is not connected
#[error("Not connected")]
NotConnected,
/// JSON-RPC error from server
#[error("JSON-RPC error {code}: {message}")]
JsonRpc {
code: i32,
message: String,
data: Option<serde_json::Value>,
},
/// Protocol version mismatch
#[error("Protocol version mismatch: SDK supports versions {min}-{max}, but server reports version {actual}. Please update your SDK or server to ensure compatibility.")]
ProtocolMismatch { min: u32, max: u32, actual: u32 },
/// Protocol error (framing, invalid messages, etc.)
#[error("Protocol error: {0}")]
Protocol(String),
/// JSON serialization/deserialization error
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
/// Request timed out
#[error("Request timed out after {0:?}")]
Timeout(Duration),
/// Session not found
#[error("Session not found: {0}")]
SessionNotFound(String),
/// Session was already destroyed
#[error("Session already destroyed")]
SessionDestroyed,
/// Invalid configuration
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
/// Failed to start CLI process
#[error("Failed to start CLI: {0}")]
ProcessStart(std::io::Error),
/// CLI process exited unexpectedly
#[error("CLI exited unexpectedly with code {0:?}")]
ProcessExit(Option<i32>),
/// Port detection failed in TCP mode
#[error("Failed to detect CLI server port")]
PortDetectionFailed,
/// Client is shutting down
#[error("Client is shutting down")]
Shutdown,
/// Tool not found
#[error("Tool not found: {0}")]
ToolNotFound(String),
/// Tool execution error
#[error("Tool execution error: {0}")]
ToolError(String),
/// Permission denied
#[error("Permission denied: {0}")]
PermissionDenied(String),
/// Channel send error (internal)
#[error("Internal channel error")]
ChannelError,
}
/// Result type alias for Copilot SDK operations.
pub type Result<T> = std::result::Result<T, CopilotError>;
impl CopilotError {
/// Create a JSON-RPC error from components.
pub fn json_rpc(
code: i32,
message: impl Into<String>,
data: Option<serde_json::Value>,
) -> Self {
Self::JsonRpc {
code,
message: message.into(),
data,
}
}
/// Create an invalid config error.
pub fn invalid_config(msg: impl Into<String>) -> Self {
Self::InvalidConfig(msg.into())
}
/// Returns true if this error indicates the connection is no longer usable.
pub fn is_fatal(&self) -> bool {
matches!(
self,
CopilotError::ConnectionClosed
| CopilotError::ProcessExit(_)
| CopilotError::Shutdown
| CopilotError::ProtocolMismatch { .. }
)
}
}
// Convert oneshot channel errors to our error type
impl From<tokio::sync::oneshot::error::RecvError> for CopilotError {
fn from(_: tokio::sync::oneshot::error::RecvError) -> Self {
CopilotError::ConnectionClosed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = CopilotError::ProtocolMismatch {
min: 2,
max: 3,
actual: 1,
};
assert_eq!(
err.to_string(),
"Protocol version mismatch: SDK supports versions 2-3, but server reports version 1. Please update your SDK or server to ensure compatibility."
);
}
#[test]
fn test_is_fatal() {
assert!(CopilotError::ConnectionClosed.is_fatal());
assert!(CopilotError::Shutdown.is_fatal());
assert!(!CopilotError::Timeout(Duration::from_secs(30)).is_fatal());
}
}