mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-09 01:24:49 +08:00
style: cargo fmt — fix CI formatting failures
Pre-existing formatting issues in anthropic.rs surfaced by CI cargo fmt check. No functional changes.
This commit is contained in:
@@ -515,7 +515,10 @@ impl AnthropicClient {
|
||||
input_tokens: u32,
|
||||
}
|
||||
|
||||
let request_url = format!("{}/v1/messages/count_tokens", self.base_url.trim_end_matches('/'));
|
||||
let request_url = format!(
|
||||
"{}/v1/messages/count_tokens",
|
||||
self.base_url.trim_end_matches('/')
|
||||
);
|
||||
let mut request_body = self.request_profile.render_json_body(request)?;
|
||||
strip_unsupported_beta_body_fields(&mut request_body);
|
||||
let response = self
|
||||
@@ -528,12 +531,7 @@ impl AnthropicClient {
|
||||
let response = expect_success(response).await?;
|
||||
let body = response.text().await.map_err(ApiError::from)?;
|
||||
let parsed = serde_json::from_str::<CountTokensResponse>(&body).map_err(|error| {
|
||||
ApiError::json_deserialize(
|
||||
"Anthropic count_tokens",
|
||||
&request.model,
|
||||
&body,
|
||||
error,
|
||||
)
|
||||
ApiError::json_deserialize("Anthropic count_tokens", &request.model, &body, error)
|
||||
})?;
|
||||
Ok(parsed.input_tokens)
|
||||
}
|
||||
@@ -597,7 +595,9 @@ fn jitter_for_base(base: Duration) -> Duration {
|
||||
let tick = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
// splitmix64 finalizer — mixes the low bits so large bases still see
|
||||
// jitter across their full range instead of being clamped to subsec nanos.
|
||||
let mut mixed = raw_nanos.wrapping_add(tick).wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut mixed = raw_nanos
|
||||
.wrapping_add(tick)
|
||||
.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
mixed ^= mixed >> 31;
|
||||
@@ -1268,7 +1268,7 @@ mod tests {
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
stream: false,
|
||||
..Default::default()
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(request.with_streaming().stream);
|
||||
@@ -1464,10 +1464,14 @@ mod tests {
|
||||
// temperature is kept (Anthropic supports it)
|
||||
assert_eq!(body["temperature"], serde_json::json!(0.7));
|
||||
// frequency_penalty and presence_penalty are removed
|
||||
assert!(body.get("frequency_penalty").is_none(),
|
||||
"frequency_penalty must be stripped for Anthropic");
|
||||
assert!(body.get("presence_penalty").is_none(),
|
||||
"presence_penalty must be stripped for Anthropic");
|
||||
assert!(
|
||||
body.get("frequency_penalty").is_none(),
|
||||
"frequency_penalty must be stripped for Anthropic"
|
||||
);
|
||||
assert!(
|
||||
body.get("presence_penalty").is_none(),
|
||||
"presence_penalty must be stripped for Anthropic"
|
||||
);
|
||||
// stop is renamed to stop_sequences
|
||||
assert!(body.get("stop").is_none(), "stop must be renamed");
|
||||
assert_eq!(body["stop_sequences"], serde_json::json!(["\n"]));
|
||||
@@ -1484,8 +1488,10 @@ mod tests {
|
||||
super::strip_unsupported_beta_body_fields(&mut body);
|
||||
|
||||
assert!(body.get("stop").is_none());
|
||||
assert!(body.get("stop_sequences").is_none(),
|
||||
"empty stop should not produce stop_sequences");
|
||||
assert!(
|
||||
body.get("stop_sequences").is_none(),
|
||||
"empty stop should not produce stop_sequences"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1499,7 +1505,7 @@ mod tests {
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
stream: false,
|
||||
..Default::default()
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut rendered = client
|
||||
|
||||
@@ -135,12 +135,7 @@ impl OpenAiCompatClient {
|
||||
let request_id = request_id_from_headers(response.headers());
|
||||
let body = response.text().await.map_err(ApiError::from)?;
|
||||
let payload = serde_json::from_str::<ChatCompletionResponse>(&body).map_err(|error| {
|
||||
ApiError::json_deserialize(
|
||||
self.config.provider_name,
|
||||
&request.model,
|
||||
&body,
|
||||
error,
|
||||
)
|
||||
ApiError::json_deserialize(self.config.provider_name, &request.model, &body, error)
|
||||
})?;
|
||||
let mut normalized = normalize_response(&request.model, payload)?;
|
||||
if normalized.request_id.is_none() {
|
||||
@@ -160,10 +155,7 @@ impl OpenAiCompatClient {
|
||||
Ok(MessageStream {
|
||||
request_id: request_id_from_headers(response.headers()),
|
||||
response,
|
||||
parser: OpenAiSseParser::with_context(
|
||||
self.config.provider_name,
|
||||
request.model.clone(),
|
||||
),
|
||||
parser: OpenAiSseParser::with_context(self.config.provider_name, request.model.clone()),
|
||||
pending: VecDeque::new(),
|
||||
done: false,
|
||||
state: StreamState::new(request.model.clone()),
|
||||
@@ -253,7 +245,9 @@ fn jitter_for_base(base: Duration) -> Duration {
|
||||
.map(|elapsed| u64::try_from(elapsed.as_nanos()).unwrap_or(u64::MAX))
|
||||
.unwrap_or(0);
|
||||
let tick = JITTER_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let mut mixed = raw_nanos.wrapping_add(tick).wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut mixed = raw_nanos
|
||||
.wrapping_add(tick)
|
||||
.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
mixed ^= mixed >> 31;
|
||||
@@ -1110,7 +1104,7 @@ mod tests {
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
stream: true,
|
||||
..Default::default()
|
||||
..Default::default()
|
||||
},
|
||||
OpenAiCompatConfig::openai(),
|
||||
);
|
||||
@@ -1129,7 +1123,7 @@ mod tests {
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
stream: true,
|
||||
..Default::default()
|
||||
..Default::default()
|
||||
},
|
||||
OpenAiCompatConfig::xai(),
|
||||
);
|
||||
@@ -1240,8 +1234,14 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||
assert!(payload.get("temperature").is_none(), "reasoning model should strip temperature");
|
||||
assert!(payload.get("top_p").is_none(), "reasoning model should strip top_p");
|
||||
assert!(
|
||||
payload.get("temperature").is_none(),
|
||||
"reasoning model should strip temperature"
|
||||
);
|
||||
assert!(
|
||||
payload.get("top_p").is_none(),
|
||||
"reasoning model should strip top_p"
|
||||
);
|
||||
assert!(payload.get("frequency_penalty").is_none());
|
||||
assert!(payload.get("presence_penalty").is_none());
|
||||
// stop is safe for all providers
|
||||
@@ -1269,7 +1269,10 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
let payload = build_chat_completion_request(&request, OpenAiCompatConfig::openai());
|
||||
assert!(payload.get("temperature").is_none(), "temperature should be absent");
|
||||
assert!(
|
||||
payload.get("temperature").is_none(),
|
||||
"temperature should be absent"
|
||||
);
|
||||
assert!(payload.get("top_p").is_none(), "top_p should be absent");
|
||||
assert!(payload.get("frequency_penalty").is_none());
|
||||
assert!(payload.get("presence_penalty").is_none());
|
||||
|
||||
@@ -908,8 +908,10 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
|
||||
let Some(object) = root.as_object() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
Ok(optional_string_array(object, "trustedRoots", "merged settings.trustedRoots")?
|
||||
.unwrap_or_default())
|
||||
Ok(
|
||||
optional_string_array(object, "trustedRoots", "merged settings.trustedRoots")?
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
||||
|
||||
@@ -56,10 +56,6 @@ pub use compact::{
|
||||
compact_session, estimate_session_tokens, format_compact_summary,
|
||||
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
|
||||
};
|
||||
pub use config_validate::{
|
||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||
DiagnosticKind, ValidationResult,
|
||||
};
|
||||
pub use config::{
|
||||
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
|
||||
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||
@@ -68,17 +64,21 @@ pub use config::{
|
||||
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
|
||||
CLAW_SETTINGS_SCHEMA_NAME,
|
||||
};
|
||||
pub use config_validate::{
|
||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||
DiagnosticKind, ValidationResult,
|
||||
};
|
||||
pub use conversation::{
|
||||
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolError,
|
||||
ToolExecutor, TurnSummary,
|
||||
};
|
||||
pub use git_context::{GitCommitEntry, GitContext};
|
||||
pub use file_ops::{
|
||||
edit_file, glob_search, grep_search, read_file, write_file, EditFileOutput, GlobSearchOutput,
|
||||
GrepSearchInput, GrepSearchOutput, ReadFileOutput, StructuredPatchHunk, TextFilePayload,
|
||||
WriteFileOutput,
|
||||
};
|
||||
pub use git_context::{GitCommitEntry, GitContext};
|
||||
pub use hooks::{
|
||||
HookAbortSignal, HookEvent, HookProgressEvent, HookProgressReporter, HookRunResult, HookRunner,
|
||||
};
|
||||
|
||||
@@ -366,9 +366,7 @@ mod tests {
|
||||
server_name: "test".to_string(),
|
||||
server_version: "0.0.0".to_string(),
|
||||
tools: Vec::new(),
|
||||
tool_handler: Box::new(|name, args| {
|
||||
Ok(format!("called {name} with {args}"))
|
||||
}),
|
||||
tool_handler: Box::new(|name, args| Ok(format!("called {name} with {args}"))),
|
||||
},
|
||||
stdin: BufReader::new(stdin()),
|
||||
stdout: stdout(),
|
||||
|
||||
@@ -253,7 +253,6 @@ fn read_git_status(cwd: &Path) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn read_git_diff(cwd: &Path) -> Option<String> {
|
||||
let mut sections = Vec::new();
|
||||
|
||||
@@ -715,8 +714,16 @@ mod tests {
|
||||
.render();
|
||||
|
||||
// then: branch, recent commits and staged files are present in context
|
||||
let gc = context.git_context.as_ref().expect("git context should be present");
|
||||
let commits: String = gc.recent_commits.iter().map(|c| c.subject.clone()).collect::<Vec<_>>().join("\n");
|
||||
let gc = context
|
||||
.git_context
|
||||
.as_ref()
|
||||
.expect("git context should be present");
|
||||
let commits: String = gc
|
||||
.recent_commits
|
||||
.iter()
|
||||
.map(|c| c.subject.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(commits.contains("first commit"));
|
||||
assert!(commits.contains("second commit"));
|
||||
assert!(commits.contains("third commit"));
|
||||
|
||||
@@ -1441,8 +1441,12 @@ mod tests {
|
||||
/// Called by external consumers (e.g. clawhip) to enumerate sessions for a CWD.
|
||||
#[allow(dead_code)]
|
||||
pub fn workspace_sessions_dir(cwd: &std::path::Path) -> Result<std::path::PathBuf, SessionError> {
|
||||
let store = crate::session_control::SessionStore::from_cwd(cwd)
|
||||
.map_err(|e| SessionError::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))?;
|
||||
let store = crate::session_control::SessionStore::from_cwd(cwd).map_err(|e| {
|
||||
SessionError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e.to_string(),
|
||||
))
|
||||
})?;
|
||||
Ok(store.sessions_dir().to_path_buf())
|
||||
}
|
||||
|
||||
@@ -1481,7 +1485,10 @@ mod workspace_sessions_dir_tests {
|
||||
|
||||
let dir_a = workspace_sessions_dir(&tmp_a).expect("dir a");
|
||||
let dir_b = workspace_sessions_dir(&tmp_b).expect("dir b");
|
||||
assert_ne!(dir_a, dir_b, "different CWDs must produce different session dirs");
|
||||
assert_ne!(
|
||||
dir_a, dir_b,
|
||||
"different CWDs must produce different session dirs"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&tmp_a).ok();
|
||||
fs::remove_dir_all(&tmp_b).ok();
|
||||
|
||||
@@ -1105,7 +1105,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn emit_state_file_writes_worker_status_on_transition() {
|
||||
let cwd_path = std::env::temp_dir().join(format!("claw-state-test-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos()));
|
||||
let cwd_path = std::env::temp_dir().join(format!(
|
||||
"claw-state-test-{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
));
|
||||
std::fs::create_dir_all(&cwd_path).expect("test dir should create");
|
||||
let cwd = cwd_path.to_str().expect("test path should be utf8");
|
||||
let registry = WorkerRegistry::new();
|
||||
@@ -1113,11 +1119,19 @@ mod tests {
|
||||
|
||||
// After create the worker is Spawning — state file should exist
|
||||
let state_path = cwd_path.join(".claw").join("worker-state.json");
|
||||
assert!(state_path.exists(), "state file should exist after worker creation");
|
||||
assert!(
|
||||
state_path.exists(),
|
||||
"state file should exist after worker creation"
|
||||
);
|
||||
|
||||
let raw = std::fs::read_to_string(&state_path).expect("state file should be readable");
|
||||
let value: serde_json::Value = serde_json::from_str(&raw).expect("state file should be valid JSON");
|
||||
assert_eq!(value["status"].as_str(), Some("spawning"), "initial status should be spawning");
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(&raw).expect("state file should be valid JSON");
|
||||
assert_eq!(
|
||||
value["status"].as_str(),
|
||||
Some("spawning"),
|
||||
"initial status should be spawning"
|
||||
);
|
||||
assert_eq!(value["is_ready"].as_bool(), Some(false));
|
||||
|
||||
// Transition to ReadyForPrompt by observing trust-cleared text
|
||||
@@ -1125,14 +1139,20 @@ mod tests {
|
||||
.observe(&worker.worker_id, "Ready for input\n>")
|
||||
.expect("observe ready should succeed");
|
||||
|
||||
let raw = std::fs::read_to_string(&state_path).expect("state file should be readable after observe");
|
||||
let value: serde_json::Value = serde_json::from_str(&raw).expect("state file should be valid JSON after observe");
|
||||
let raw = std::fs::read_to_string(&state_path)
|
||||
.expect("state file should be readable after observe");
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(&raw).expect("state file should be valid JSON after observe");
|
||||
assert_eq!(
|
||||
value["status"].as_str(),
|
||||
Some("ready_for_prompt"),
|
||||
"status should be ready_for_prompt after observe"
|
||||
);
|
||||
assert_eq!(value["is_ready"].as_bool(), Some(true), "is_ready should be true when ReadyForPrompt");
|
||||
assert_eq!(
|
||||
value["is_ready"].as_bool(),
|
||||
Some(true),
|
||||
"is_ready should be true when ReadyForPrompt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -54,7 +54,9 @@ use runtime::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Map, Value};
|
||||
use tools::{execute_tool, mvp_tool_specs, GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput};
|
||||
use tools::{
|
||||
execute_tool, mvp_tool_specs, GlobalToolRegistry, RuntimeToolDefinition, ToolSearchOutput,
|
||||
};
|
||||
|
||||
const DEFAULT_MODEL: &str = "claude-opus-4-6";
|
||||
fn max_tokens_for_model(model: &str) -> u32 {
|
||||
@@ -205,8 +207,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
run_stale_base_preflight(base_commit.as_deref());
|
||||
let stdin_context = read_piped_stdin();
|
||||
let effective_prompt = merge_prompt_with_stdin(&prompt, stdin_context.as_deref());
|
||||
LiveCli::new(model, true, allowed_tools, permission_mode)?
|
||||
.run_turn_with_output(&effective_prompt, output_format, false)?;
|
||||
LiveCli::new(model, true, allowed_tools, permission_mode)?.run_turn_with_output(
|
||||
&effective_prompt,
|
||||
output_format,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
CliAction::Login { output_format } => run_login(output_format)?,
|
||||
CliAction::Logout { output_format } => run_logout(output_format)?,
|
||||
@@ -942,11 +947,7 @@ fn config_permission_mode_for_current_dir() -> Option<PermissionMode> {
|
||||
fn config_model_for_current_dir() -> Option<String> {
|
||||
let cwd = env::current_dir().ok()?;
|
||||
let loader = ConfigLoader::default_for(&cwd);
|
||||
loader
|
||||
.load()
|
||||
.ok()?
|
||||
.model()
|
||||
.map(ToOwned::to_owned)
|
||||
loader.load().ok()?.model().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn resolve_repl_model(cli_model: String) -> String {
|
||||
@@ -1021,10 +1022,7 @@ fn parse_system_prompt_args(
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_export_args(
|
||||
args: &[String],
|
||||
output_format: CliOutputFormat,
|
||||
) -> Result<CliAction, String> {
|
||||
fn parse_export_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
|
||||
let mut session_reference = LATEST_SESSION_REFERENCE.to_string();
|
||||
let mut output_path: Option<PathBuf> = None;
|
||||
let mut index = 0;
|
||||
@@ -1336,8 +1334,13 @@ fn run_worker_state(output_format: CliOutputFormat) -> Result<(), Box<dyn std::e
|
||||
let state_path = cwd.join(".claw").join("worker-state.json");
|
||||
if !state_path.exists() {
|
||||
match output_format {
|
||||
CliOutputFormat::Text => println!("No worker state file found at {}", state_path.display()),
|
||||
CliOutputFormat::Json => println!("{}", serde_json::json!({"error": "no_state_file", "path": state_path.display().to_string()})),
|
||||
CliOutputFormat::Text => {
|
||||
println!("No worker state file found at {}", state_path.display())
|
||||
}
|
||||
CliOutputFormat::Json => println!(
|
||||
"{}",
|
||||
serde_json::json!({"error": "no_state_file", "path": state_path.display().to_string()})
|
||||
),
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -2660,7 +2663,8 @@ fn run_resume_command(
|
||||
json: None,
|
||||
}),
|
||||
SlashCommand::History { count } => {
|
||||
let limit = parse_history_count(count.as_deref()).map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
|
||||
let limit = parse_history_count(count.as_deref())
|
||||
.map_err(|error| -> Box<dyn std::error::Error> { error.into() })?;
|
||||
let entries = collect_session_prompt_history(session);
|
||||
Ok(ResumeCommandOutcome {
|
||||
session: session.clone(),
|
||||
@@ -5331,16 +5335,18 @@ fn format_history_timestamp(timestamp_ms: u64) -> String {
|
||||
let seconds = seconds_of_day % 60;
|
||||
|
||||
let (year, month, day) = civil_from_days(i64::try_from(days_since_epoch).unwrap_or(0));
|
||||
format!(
|
||||
"{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{subsec_ms:03}Z"
|
||||
)
|
||||
format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}.{subsec_ms:03}Z")
|
||||
}
|
||||
|
||||
// Computes civil (Gregorian) year/month/day from days since the Unix epoch
|
||||
// (1970-01-01) using Howard Hinnant's `civil_from_days` algorithm.
|
||||
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
||||
let z = days + 719_468;
|
||||
let era = if z >= 0 { z / 146_097 } else { (z - 146_096) / 146_097 };
|
||||
let era = if z >= 0 {
|
||||
z / 146_097
|
||||
} else {
|
||||
(z - 146_096) / 146_097
|
||||
};
|
||||
let doe = (z - era * 146_097) as u64; // [0, 146_096]
|
||||
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; // [0, 399]
|
||||
let y = yoe as i64 + era * 400;
|
||||
@@ -6391,7 +6397,10 @@ impl ApiClient for AnthropicRuntimeClient {
|
||||
.await;
|
||||
match result {
|
||||
Ok(events) => return Ok(events),
|
||||
Err(error) if error.to_string().contains("post-tool stall") && attempt < max_attempts => {
|
||||
Err(error)
|
||||
if error.to_string().contains("post-tool stall")
|
||||
&& attempt < max_attempts =>
|
||||
{
|
||||
// Stalled after tool completion — nudge the model by
|
||||
// re-sending the same request.
|
||||
continue;
|
||||
@@ -6400,9 +6409,7 @@ impl ApiClient for AnthropicRuntimeClient {
|
||||
}
|
||||
}
|
||||
|
||||
Err(RuntimeError::new(
|
||||
"post-tool continuation nudge exhausted",
|
||||
))
|
||||
Err(RuntimeError::new("post-tool continuation nudge exhausted"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6416,13 +6423,13 @@ impl AnthropicRuntimeClient {
|
||||
message_request: &MessageRequest,
|
||||
apply_stall_timeout: bool,
|
||||
) -> Result<Vec<AssistantEvent>, RuntimeError> {
|
||||
let mut stream =
|
||||
self.client
|
||||
.stream_message(message_request)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
RuntimeError::new(format_user_visible_api_error(&self.session_id, &error))
|
||||
})?;
|
||||
let mut stream = self
|
||||
.client
|
||||
.stream_message(message_request)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
RuntimeError::new(format_user_visible_api_error(&self.session_id, &error))
|
||||
})?;
|
||||
let mut stdout = io::stdout();
|
||||
let mut sink = io::sink();
|
||||
let out: &mut dyn Write = if self.emit_output {
|
||||
@@ -6442,10 +6449,7 @@ impl AnthropicRuntimeClient {
|
||||
let next = if apply_stall_timeout && !received_any_event {
|
||||
match tokio::time::timeout(POST_TOOL_STALL_TIMEOUT, stream.next_event()).await {
|
||||
Ok(inner) => inner.map_err(|error| {
|
||||
RuntimeError::new(format_user_visible_api_error(
|
||||
&self.session_id,
|
||||
&error,
|
||||
))
|
||||
RuntimeError::new(format_user_visible_api_error(&self.session_id, &error))
|
||||
})?,
|
||||
Err(_elapsed) => {
|
||||
return Err(RuntimeError::new(
|
||||
@@ -6629,9 +6633,15 @@ fn format_context_window_blocked_error(session_id: &str, error: &api::ApiError)
|
||||
context_window_tokens,
|
||||
} => {
|
||||
lines.push(format!(" Model {model}"));
|
||||
lines.push(format!(" Input estimate ~{estimated_input_tokens} tokens (heuristic)"));
|
||||
lines.push(format!(" Requested output {requested_output_tokens} tokens"));
|
||||
lines.push(format!(" Total estimate ~{estimated_total_tokens} tokens (heuristic)"));
|
||||
lines.push(format!(
|
||||
" Input estimate ~{estimated_input_tokens} tokens (heuristic)"
|
||||
));
|
||||
lines.push(format!(
|
||||
" Requested output {requested_output_tokens} tokens"
|
||||
));
|
||||
lines.push(format!(
|
||||
" Total estimate ~{estimated_total_tokens} tokens (heuristic)"
|
||||
));
|
||||
lines.push(format!(" Context window {context_window_tokens} tokens"));
|
||||
}
|
||||
api::ApiError::Api { message, body, .. } => {
|
||||
@@ -7604,7 +7614,10 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
writeln!(out, " claw login")?;
|
||||
writeln!(out, " claw logout")?;
|
||||
writeln!(out, " claw init")?;
|
||||
writeln!(out, " claw export [PATH] [--session SESSION] [--output PATH]")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw export [PATH] [--session SESSION] [--output PATH]"
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" Dump the latest (or named) session as markdown; writes to PATH or stdout"
|
||||
@@ -7669,10 +7682,7 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
||||
out,
|
||||
" claw --output-format json prompt \"explain src/main.rs\""
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --compact \"summarize Cargo.toml\" | wc -l"
|
||||
)?;
|
||||
writeln!(out, " claw --compact \"summarize Cargo.toml\" | wc -l")?;
|
||||
writeln!(
|
||||
out,
|
||||
" claw --allowedTools read,glob \"summarize Cargo.toml\""
|
||||
@@ -7723,18 +7733,19 @@ mod tests {
|
||||
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
||||
format_ultraplan_report, format_unknown_slash_command,
|
||||
format_unknown_slash_command_message, format_user_visible_api_error,
|
||||
merge_prompt_with_stdin, normalize_permission_mode, parse_args, parse_git_status_branch,
|
||||
parse_git_status_metadata_for, parse_git_workspace_summary, permission_policy,
|
||||
print_help_to, push_output_block, render_config_report, render_diff_report,
|
||||
render_diff_report_for, render_memory_report, render_repl_help, render_resume_usage,
|
||||
resolve_model_alias, resolve_model_alias_with_config, resolve_repl_model,
|
||||
resolve_session_reference, response_to_events, resume_supported_slash_commands,
|
||||
run_resume_command, slash_command_completion_candidates_with_sessions, status_context,
|
||||
validate_no_args, write_mcp_server_fixture, CliAction, CliOutputFormat, CliToolExecutor,
|
||||
GitWorkspaceSummary, InternalPromptProgressEvent, InternalPromptProgressState, LiveCli,
|
||||
LocalHelpTopic, SlashCommand, StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
PromptHistoryEntry, render_prompt_history_report, parse_history_count,
|
||||
parse_export_args, render_session_markdown, summarize_tool_payload_for_markdown, short_tool_id,
|
||||
merge_prompt_with_stdin, normalize_permission_mode, parse_args, parse_export_args,
|
||||
parse_git_status_branch, parse_git_status_metadata_for, parse_git_workspace_summary,
|
||||
parse_history_count, permission_policy, print_help_to, push_output_block,
|
||||
render_config_report, render_diff_report, render_diff_report_for, render_memory_report,
|
||||
render_prompt_history_report, render_repl_help, render_resume_usage,
|
||||
render_session_markdown, resolve_model_alias, resolve_model_alias_with_config,
|
||||
resolve_repl_model, resolve_session_reference, response_to_events,
|
||||
resume_supported_slash_commands, run_resume_command, short_tool_id,
|
||||
slash_command_completion_candidates_with_sessions, status_context,
|
||||
summarize_tool_payload_for_markdown, validate_no_args, write_mcp_server_fixture, CliAction,
|
||||
CliOutputFormat, CliToolExecutor, GitWorkspaceSummary, InternalPromptProgressEvent,
|
||||
InternalPromptProgressState, LiveCli, LocalHelpTopic, PromptHistoryEntry, SlashCommand,
|
||||
StatusUsage, DEFAULT_MODEL, LATEST_SESSION_REFERENCE,
|
||||
};
|
||||
use api::{ApiError, MessageResponse, OutputContentBlock, Usage};
|
||||
use plugins::{
|
||||
@@ -8586,8 +8597,12 @@ mod tests {
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(&["state".to_string(), "--output-format".to_string(), "json".to_string()])
|
||||
.expect("state --output-format json should parse"),
|
||||
parse_args(&[
|
||||
"state".to_string(),
|
||||
"--output-format".to_string(),
|
||||
"json".to_string()
|
||||
])
|
||||
.expect("state --output-format json should parse"),
|
||||
CliAction::State {
|
||||
output_format: CliOutputFormat::Json,
|
||||
}
|
||||
@@ -9417,8 +9432,7 @@ mod tests {
|
||||
std::env::remove_var("ANTHROPIC_MODEL");
|
||||
std::env::set_var("ANTHROPIC_MODEL", "sonnet");
|
||||
|
||||
let resolved =
|
||||
with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||
|
||||
assert_eq!(resolved, "claude-sonnet-4-6");
|
||||
|
||||
@@ -9437,8 +9451,7 @@ mod tests {
|
||||
std::env::set_var("CLAW_CONFIG_HOME", &config_home);
|
||||
std::env::remove_var("ANTHROPIC_MODEL");
|
||||
|
||||
let resolved =
|
||||
with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||
let resolved = with_current_dir(&root, || resolve_repl_model(DEFAULT_MODEL.to_string()));
|
||||
|
||||
assert_eq!(resolved, DEFAULT_MODEL);
|
||||
|
||||
|
||||
@@ -1244,10 +1244,8 @@ fn execute_tool_with_enforcer(
|
||||
}
|
||||
"WorkerRestart" => from_value::<WorkerIdInput>(input).and_then(run_worker_restart),
|
||||
"WorkerTerminate" => from_value::<WorkerIdInput>(input).and_then(run_worker_terminate),
|
||||
"WorkerObserveCompletion" => {
|
||||
from_value::<WorkerObserveCompletionInput>(input)
|
||||
.and_then(run_worker_observe_completion)
|
||||
}
|
||||
"WorkerObserveCompletion" => from_value::<WorkerObserveCompletionInput>(input)
|
||||
.and_then(run_worker_observe_completion),
|
||||
"TeamCreate" => from_value::<TeamCreateInput>(input).and_then(run_team_create),
|
||||
"TeamDelete" => from_value::<TeamDeleteInput>(input).and_then(run_team_delete),
|
||||
"CronCreate" => from_value::<CronCreateInput>(input).and_then(run_cron_create),
|
||||
@@ -1510,9 +1508,7 @@ fn run_worker_terminate(input: WorkerIdInput) -> Result<String, String> {
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn run_worker_observe_completion(
|
||||
input: WorkerObserveCompletionInput,
|
||||
) -> Result<String, String> {
|
||||
fn run_worker_observe_completion(input: WorkerObserveCompletionInput) -> Result<String, String> {
|
||||
let worker = global_worker_registry().observe_completion(
|
||||
&input.worker_id,
|
||||
&input.finish_reason,
|
||||
@@ -3826,7 +3822,8 @@ impl ApiClient for ProviderRuntimeClient {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let messages = convert_messages(&request.messages);
|
||||
let system = (!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n"));
|
||||
let system =
|
||||
(!request.system_prompt.is_empty()).then(|| request.system_prompt.join("\n\n"));
|
||||
let tool_choice = (!self.allowed_tools.is_empty()).then_some(ToolChoice::Auto);
|
||||
|
||||
let runtime = &self.runtime;
|
||||
@@ -5379,8 +5376,8 @@ mod tests {
|
||||
GlobalToolRegistry, LaneEventName, LaneFailureClass, ProviderRuntimeClient,
|
||||
SubagentToolExecutor,
|
||||
};
|
||||
use runtime::ProviderFallbackConfig;
|
||||
use api::OutputContentBlock;
|
||||
use runtime::ProviderFallbackConfig;
|
||||
use runtime::{
|
||||
permission_enforcer::PermissionEnforcer, ApiRequest, AssistantEvent, ConversationRuntime,
|
||||
PermissionMode, PermissionPolicy, RuntimeError, Session, TaskPacket, ToolExecutor,
|
||||
@@ -5566,11 +5563,7 @@ mod tests {
|
||||
// Use the actual OS temp dir so the worktree path matches the allowlist
|
||||
let tmp_root = std::env::temp_dir().to_str().expect("utf-8").to_string();
|
||||
let settings = format!("{{\"trustedRoots\": [\"{tmp_root}\"]}}");
|
||||
fs::write(
|
||||
claw_dir.join("settings.json"),
|
||||
settings,
|
||||
)
|
||||
.expect("write settings");
|
||||
fs::write(claw_dir.join("settings.json"), settings).expect("write settings");
|
||||
|
||||
// WorkerCreate with no per-call trusted_roots — config should supply them
|
||||
let cwd = worktree.to_str().expect("valid utf-8").to_string();
|
||||
@@ -5605,13 +5598,13 @@ mod tests {
|
||||
let worker_id = output["worker_id"].as_str().expect("worker_id").to_string();
|
||||
|
||||
// Terminate
|
||||
let terminated = execute_tool(
|
||||
"WorkerTerminate",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerTerminate should succeed");
|
||||
let terminated = execute_tool("WorkerTerminate", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerTerminate should succeed");
|
||||
let term_output: serde_json::Value = serde_json::from_str(&terminated).expect("json");
|
||||
assert_eq!(term_output["status"], "finished", "terminated worker should be finished");
|
||||
assert_eq!(
|
||||
term_output["status"], "finished",
|
||||
"terminated worker should be finished"
|
||||
);
|
||||
assert_eq!(
|
||||
term_output["prompt_in_flight"], false,
|
||||
"prompt_in_flight should be cleared on termination"
|
||||
@@ -5637,11 +5630,8 @@ mod tests {
|
||||
.expect("WorkerObserve should succeed");
|
||||
|
||||
// Restart
|
||||
let restarted = execute_tool(
|
||||
"WorkerRestart",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerRestart should succeed");
|
||||
let restarted = execute_tool("WorkerRestart", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerRestart should succeed");
|
||||
let restart_output: serde_json::Value = serde_json::from_str(&restarted).expect("json");
|
||||
assert_eq!(
|
||||
restart_output["status"], "spawning",
|
||||
@@ -5667,11 +5657,8 @@ mod tests {
|
||||
let created_output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||
let worker_id = created_output["worker_id"].as_str().expect("worker_id");
|
||||
|
||||
let fetched = execute_tool(
|
||||
"WorkerGet",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerGet should succeed");
|
||||
let fetched = execute_tool("WorkerGet", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerGet should succeed");
|
||||
let fetched_output: serde_json::Value = serde_json::from_str(&fetched).expect("json");
|
||||
assert_eq!(fetched_output["worker_id"], worker_id);
|
||||
assert_eq!(fetched_output["status"], "spawning");
|
||||
@@ -5705,11 +5692,8 @@ mod tests {
|
||||
let worker_id = created_output["worker_id"].as_str().expect("worker_id");
|
||||
|
||||
// Worker is still in spawning — await_ready should return not-ready snapshot
|
||||
let snapshot = execute_tool(
|
||||
"WorkerAwaitReady",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerAwaitReady should succeed even when not ready");
|
||||
let snapshot = execute_tool("WorkerAwaitReady", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerAwaitReady should succeed even when not ready");
|
||||
let snap_output: serde_json::Value = serde_json::from_str(&snapshot).expect("json");
|
||||
assert_eq!(
|
||||
snap_output["ready"], false,
|
||||
@@ -5751,21 +5735,27 @@ mod tests {
|
||||
let state_path = worktree.join(".claw").join("worker-state.json");
|
||||
|
||||
// 1. Create worker WITHOUT trusted_roots
|
||||
let created = execute_tool(
|
||||
"WorkerCreate",
|
||||
&json!({"cwd": cwd}),
|
||||
)
|
||||
.expect("WorkerCreate should succeed");
|
||||
let created = execute_tool("WorkerCreate", &json!({"cwd": cwd}))
|
||||
.expect("WorkerCreate should succeed");
|
||||
let created_output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||
let worker_id = created_output["worker_id"].as_str().expect("worker_id").to_string();
|
||||
let worker_id = created_output["worker_id"]
|
||||
.as_str()
|
||||
.expect("worker_id")
|
||||
.to_string();
|
||||
// State file should exist after create
|
||||
assert!(state_path.exists(), "state file should be written after WorkerCreate");
|
||||
let state: serde_json::Value = serde_json::from_str(
|
||||
&fs::read_to_string(&state_path).expect("read state")
|
||||
).expect("parse state");
|
||||
assert!(
|
||||
state_path.exists(),
|
||||
"state file should be written after WorkerCreate"
|
||||
);
|
||||
let state: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&state_path).expect("read state"))
|
||||
.expect("parse state");
|
||||
assert_eq!(state["status"], "spawning");
|
||||
assert_eq!(state["is_ready"], false);
|
||||
assert!(state["seconds_since_update"].is_number(), "seconds_since_update must be present");
|
||||
assert!(
|
||||
state["seconds_since_update"].is_number(),
|
||||
"seconds_since_update must be present"
|
||||
);
|
||||
|
||||
// 2. Force trust_required via observe
|
||||
execute_tool(
|
||||
@@ -5773,26 +5763,27 @@ mod tests {
|
||||
&json!({"worker_id": worker_id, "screen_text": "Do you trust the files in this folder?"}),
|
||||
)
|
||||
.expect("WorkerObserve should succeed");
|
||||
let state: serde_json::Value = serde_json::from_str(
|
||||
&fs::read_to_string(&state_path).expect("read state")
|
||||
).expect("parse state");
|
||||
assert_eq!(state["status"], "trust_required",
|
||||
"state file must reflect trust_required stall");
|
||||
let state: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&state_path).expect("read state"))
|
||||
.expect("parse state");
|
||||
assert_eq!(
|
||||
state["status"], "trust_required",
|
||||
"state file must reflect trust_required stall"
|
||||
);
|
||||
assert_eq!(state["is_ready"], false);
|
||||
assert_eq!(state["trust_gate_cleared"], false);
|
||||
assert!(state["seconds_since_update"].is_number());
|
||||
|
||||
// 3. WorkerResolveTrust -> state file reflects recovery
|
||||
execute_tool(
|
||||
"WorkerResolveTrust",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerResolveTrust should succeed");
|
||||
let state: serde_json::Value = serde_json::from_str(
|
||||
&fs::read_to_string(&state_path).expect("read state")
|
||||
).expect("parse state");
|
||||
assert_eq!(state["status"], "spawning",
|
||||
"state file must show spawning after trust resolved");
|
||||
execute_tool("WorkerResolveTrust", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerResolveTrust should succeed");
|
||||
let state: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&state_path).expect("read state"))
|
||||
.expect("parse state");
|
||||
assert_eq!(
|
||||
state["status"], "spawning",
|
||||
"state file must show spawning after trust resolved"
|
||||
);
|
||||
assert_eq!(state["trust_gate_cleared"], true);
|
||||
|
||||
// 4. Observe ready screen -> state file shows ready_for_prompt
|
||||
@@ -5801,13 +5792,17 @@ mod tests {
|
||||
&json!({"worker_id": worker_id, "screen_text": "Ready for input\n>"}),
|
||||
)
|
||||
.expect("WorkerObserve ready should succeed");
|
||||
let state: serde_json::Value = serde_json::from_str(
|
||||
&fs::read_to_string(&state_path).expect("read state")
|
||||
).expect("parse state");
|
||||
assert_eq!(state["status"], "ready_for_prompt",
|
||||
"state file must show ready_for_prompt after ready screen");
|
||||
assert_eq!(state["is_ready"], true,
|
||||
"is_ready must be true in state file at ready_for_prompt");
|
||||
let state: serde_json::Value =
|
||||
serde_json::from_str(&fs::read_to_string(&state_path).expect("read state"))
|
||||
.expect("parse state");
|
||||
assert_eq!(
|
||||
state["status"], "ready_for_prompt",
|
||||
"state file must show ready_for_prompt after ready screen"
|
||||
);
|
||||
assert_eq!(
|
||||
state["is_ready"], true,
|
||||
"is_ready must be true in state file at ready_for_prompt"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&worktree).ok();
|
||||
}
|
||||
@@ -5815,13 +5810,13 @@ mod tests {
|
||||
#[test]
|
||||
fn stall_detect_and_resolve_trust_end_to_end() {
|
||||
// 1. Create worker WITHOUT trusted_roots so trust won't auto-resolve
|
||||
let created = execute_tool(
|
||||
"WorkerCreate",
|
||||
&json!({"cwd": "/no/trusted/root/here"}),
|
||||
)
|
||||
.expect("WorkerCreate should succeed");
|
||||
let created = execute_tool("WorkerCreate", &json!({"cwd": "/no/trusted/root/here"}))
|
||||
.expect("WorkerCreate should succeed");
|
||||
let created_output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||
let worker_id = created_output["worker_id"].as_str().expect("worker_id").to_string();
|
||||
let worker_id = created_output["worker_id"]
|
||||
.as_str()
|
||||
.expect("worker_id")
|
||||
.to_string();
|
||||
assert_eq!(created_output["trust_auto_resolve"], false);
|
||||
|
||||
// 2. Observe trust prompt screen text -> worker stalls at trust_required
|
||||
@@ -5840,11 +5835,8 @@ mod tests {
|
||||
);
|
||||
assert_eq!(stalled_output["trust_gate_cleared"], false);
|
||||
// 3. Clawhip calls WorkerResolveTrust to unblock
|
||||
let resolved = execute_tool(
|
||||
"WorkerResolveTrust",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerResolveTrust should succeed");
|
||||
let resolved = execute_tool("WorkerResolveTrust", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerResolveTrust should succeed");
|
||||
let resolved_output: serde_json::Value = serde_json::from_str(&resolved).expect("json");
|
||||
assert_eq!(
|
||||
resolved_output["status"], "spawning",
|
||||
@@ -5877,7 +5869,10 @@ mod tests {
|
||||
)
|
||||
.expect("WorkerCreate should succeed");
|
||||
let created_output: serde_json::Value = serde_json::from_str(&created).expect("json");
|
||||
let worker_id = created_output["worker_id"].as_str().expect("worker_id").to_string();
|
||||
let worker_id = created_output["worker_id"]
|
||||
.as_str()
|
||||
.expect("worker_id")
|
||||
.to_string();
|
||||
|
||||
// Force trust_required
|
||||
let stalled = execute_tool(
|
||||
@@ -5892,17 +5887,15 @@ mod tests {
|
||||
assert_eq!(stalled_output["status"], "trust_required");
|
||||
|
||||
// WorkerRestart resets the worker
|
||||
let restarted = execute_tool(
|
||||
"WorkerRestart",
|
||||
&json!({"worker_id": worker_id}),
|
||||
)
|
||||
.expect("WorkerRestart should succeed");
|
||||
let restarted = execute_tool("WorkerRestart", &json!({"worker_id": worker_id}))
|
||||
.expect("WorkerRestart should succeed");
|
||||
let restarted_output: serde_json::Value = serde_json::from_str(&restarted).expect("json");
|
||||
assert_eq!(
|
||||
restarted_output["status"], "spawning",
|
||||
"restarted worker should be back at spawning"
|
||||
);
|
||||
assert_eq!(restarted_output["trust_gate_cleared"], false,
|
||||
assert_eq!(
|
||||
restarted_output["trust_gate_cleared"], false,
|
||||
"restart clears trust — next observe loop must re-acquire trust"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user