mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-01 23:31:53 +08:00
fix: critical parity bugs - enable tools, default permissions, tool input
Tighten prompt-mode parity for the Rust CLI by enabling native tools in one-shot runs, defaulting fresh sessions to danger-full-access, and documenting the remaining TS-vs-Rust gaps.
The JSON prompt path now runs through the full conversation loop so tool use and tool results are preserved without streaming terminal noise, while the tool-input accumulator keeps the streaming {} placeholder fix without corrupting legitimate non-stream empty objects.
Constraint: Original TypeScript source was treated as read-only for parity analysis
Constraint: No new dependencies; keep the fix localized to the Rust port
Rejected: Leave JSON prompt mode on a direct non-tool API path | preserved the one-shot parity bug
Rejected: Keep workspace-write as the default permission mode | contradicted requested parity target
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep prompt text and prompt JSON paths on the same tool-capable runtime semantics unless upstream behavior proves they must diverge
Tested: cargo build --release; cargo test
Not-tested: live remote prompt run against LayoffLabs endpoint in this session
This commit is contained in:
214
PARITY.md
Normal file
214
PARITY.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# PARITY GAP ANALYSIS
|
||||||
|
|
||||||
|
Scope: read-only comparison between the original TypeScript source at `/home/bellman/Workspace/claude-code/src/` and the Rust port under `rust/crates/`.
|
||||||
|
|
||||||
|
Method: compared feature surfaces, registries, entrypoints, and runtime plumbing only. No TypeScript source was copied.
|
||||||
|
|
||||||
|
## Executive summary
|
||||||
|
|
||||||
|
The Rust port has a good foundation for:
|
||||||
|
- Anthropic API/OAuth basics
|
||||||
|
- local conversation/session state
|
||||||
|
- a core tool loop
|
||||||
|
- MCP stdio/bootstrap support
|
||||||
|
- CLAUDE.md discovery
|
||||||
|
- a small but usable built-in tool set
|
||||||
|
|
||||||
|
It is **not feature-parity** with the TypeScript CLI.
|
||||||
|
|
||||||
|
Largest gaps:
|
||||||
|
- **plugins** are effectively absent in Rust
|
||||||
|
- **hooks** are parsed but not executed in Rust
|
||||||
|
- **CLI breadth** is much narrower in Rust
|
||||||
|
- **skills** are local-file only in Rust, without the TS registry/bundled pipeline
|
||||||
|
- **assistant orchestration** lacks TS hook-aware orchestration and remote/structured transports
|
||||||
|
- **services** beyond core API/OAuth/MCP are mostly missing in Rust
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## tools/
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- `src/tools/` contains broad tool families including `AgentTool`, `AskUserQuestionTool`, `BashTool`, `ConfigTool`, `FileReadTool`, `FileWriteTool`, `GlobTool`, `GrepTool`, `LSPTool`, `ListMcpResourcesTool`, `MCPTool`, `McpAuthTool`, `ReadMcpResourceTool`, `RemoteTriggerTool`, `ScheduleCronTool`, `SkillTool`, `Task*`, `Team*`, `TodoWriteTool`, `ToolSearchTool`, `WebFetchTool`, `WebSearchTool`.
|
||||||
|
- Tool execution/orchestration is split across `src/services/tools/StreamingToolExecutor.ts`, `src/services/tools/toolExecution.ts`, `src/services/tools/toolHooks.ts`, and `src/services/tools/toolOrchestration.ts`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- Tool registry is centralized in `rust/crates/tools/src/lib.rs` via `mvp_tool_specs()`.
|
||||||
|
- Current built-ins include shell/file/search/web/todo/skill/agent/config/notebook/repl/powershell primitives.
|
||||||
|
- Runtime execution is wired through `rust/crates/tools/src/lib.rs` and `rust/crates/runtime/src/conversation.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- No Rust equivalents for major TS tools such as `AskUserQuestionTool`, `LSPTool`, `ListMcpResourcesTool`, `MCPTool`, `McpAuthTool`, `ReadMcpResourceTool`, `RemoteTriggerTool`, `ScheduleCronTool`, `Task*`, `Team*`, and several workflow/system tools.
|
||||||
|
- Rust tool surface is still explicitly an MVP registry, not a parity registry.
|
||||||
|
- Rust lacks TS’s layered tool orchestration split.
|
||||||
|
|
||||||
|
**Status:** partial core only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## hooks/
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- Hook command surface under `src/commands/hooks/`.
|
||||||
|
- Runtime hook machinery in `src/services/tools/toolHooks.ts` and `src/services/tools/toolExecution.ts`.
|
||||||
|
- TS supports `PreToolUse`, `PostToolUse`, and broader hook-driven behaviors configured through settings and documented in `src/skills/bundled/updateConfig.ts`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- Hook config is parsed and merged in `rust/crates/runtime/src/config.rs`.
|
||||||
|
- Hook config can be inspected via Rust config reporting in `rust/crates/commands/src/lib.rs` and `rust/crates/rusty-claude-cli/src/main.rs`.
|
||||||
|
- Prompt guidance mentions hooks in `rust/crates/runtime/src/prompt.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- No actual hook execution pipeline in `rust/crates/runtime/src/conversation.rs`.
|
||||||
|
- No PreToolUse/PostToolUse mutation/deny/rewrite/result-hook behavior.
|
||||||
|
- No Rust `/hooks` parity command.
|
||||||
|
|
||||||
|
**Status:** config-only; runtime behavior missing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## plugins/
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- Built-in plugin scaffolding in `src/plugins/builtinPlugins.ts` and `src/plugins/bundled/index.ts`.
|
||||||
|
- Plugin lifecycle/services in `src/services/plugins/PluginInstallationManager.ts` and `src/services/plugins/pluginOperations.ts`.
|
||||||
|
- CLI/plugin command surface under `src/commands/plugin/` and `src/commands/reload-plugins/`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- No dedicated plugin subsystem appears under `rust/crates/`.
|
||||||
|
- Repo-wide Rust references to plugins are effectively absent beyond text/help mentions.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- No plugin loader.
|
||||||
|
- No marketplace install/update/enable/disable flow.
|
||||||
|
- No `/plugin` or `/reload-plugins` parity.
|
||||||
|
- No plugin-provided hook/tool/command/MCP extension path.
|
||||||
|
|
||||||
|
**Status:** missing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## skills/ and CLAUDE.md discovery
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- Skill loading/registry pipeline in `src/skills/loadSkillsDir.ts`, `src/skills/bundledSkills.ts`, and `src/skills/mcpSkillBuilders.ts`.
|
||||||
|
- Bundled skills under `src/skills/bundled/`.
|
||||||
|
- Skills command surface under `src/commands/skills/`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- `Skill` tool in `rust/crates/tools/src/lib.rs` resolves and reads local `SKILL.md` files.
|
||||||
|
- CLAUDE.md discovery is implemented in `rust/crates/runtime/src/prompt.rs`.
|
||||||
|
- Rust supports `/memory` and `/init` via `rust/crates/commands/src/lib.rs` and `rust/crates/rusty-claude-cli/src/main.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- No bundled skill registry equivalent.
|
||||||
|
- No `/skills` command.
|
||||||
|
- No MCP skill-builder pipeline.
|
||||||
|
- No TS-style live skill discovery/reload/change handling.
|
||||||
|
- No comparable session-memory / team-memory integration around skills.
|
||||||
|
|
||||||
|
**Status:** basic local skill loading only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## cli/
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- Large command surface under `src/commands/` including `agents`, `hooks`, `mcp`, `memory`, `model`, `permissions`, `plan`, `plugin`, `resume`, `review`, `skills`, `tasks`, and many more.
|
||||||
|
- Structured/remote transport stack in `src/cli/structuredIO.ts`, `src/cli/remoteIO.ts`, and `src/cli/transports/*`.
|
||||||
|
- CLI handler split in `src/cli/handlers/*`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- Shared slash command registry in `rust/crates/commands/src/lib.rs`.
|
||||||
|
- Rust slash commands currently cover `help`, `status`, `compact`, `model`, `permissions`, `clear`, `cost`, `resume`, `config`, `memory`, `init`, `diff`, `version`, `export`, `session`.
|
||||||
|
- Main CLI/repl/prompt handling lives in `rust/crates/rusty-claude-cli/src/main.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- Missing major TS command families: `/agents`, `/hooks`, `/mcp`, `/plugin`, `/skills`, `/plan`, `/review`, `/tasks`, and many others.
|
||||||
|
- No Rust equivalent to TS structured IO / remote transport layers.
|
||||||
|
- No TS-style handler decomposition for auth/plugins/MCP/agents.
|
||||||
|
- JSON prompt mode is improved on this branch, but still not clean transport parity: empirical verification shows tool-capable JSON output can emit human-readable tool-result lines before the final JSON object.
|
||||||
|
|
||||||
|
**Status:** functional local CLI core, much narrower than TS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## assistant/ (agentic loop, streaming, tool calling)
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- Assistant/session surface at `src/assistant/sessionHistory.ts`.
|
||||||
|
- Tool orchestration in `src/services/tools/StreamingToolExecutor.ts`, `src/services/tools/toolExecution.ts`, `src/services/tools/toolOrchestration.ts`.
|
||||||
|
- Remote/structured streaming layers in `src/cli/structuredIO.ts` and `src/cli/remoteIO.ts`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- Core loop in `rust/crates/runtime/src/conversation.rs`.
|
||||||
|
- Stream/tool event translation in `rust/crates/rusty-claude-cli/src/main.rs`.
|
||||||
|
- Session persistence in `rust/crates/runtime/src/session.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- No TS-style hook-aware orchestration layer.
|
||||||
|
- No TS structured/remote assistant transport stack.
|
||||||
|
- No richer TS assistant/session-history/background-task integration.
|
||||||
|
- JSON output path is no longer single-turn only on this branch, but output cleanliness still lags TS transport expectations.
|
||||||
|
|
||||||
|
**Status:** strong core loop, missing orchestration layers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## services/ (API client, auth, models, MCP)
|
||||||
|
|
||||||
|
### TS exists
|
||||||
|
Evidence:
|
||||||
|
- API services under `src/services/api/*`.
|
||||||
|
- OAuth services under `src/services/oauth/*`.
|
||||||
|
- MCP services under `src/services/mcp/*`.
|
||||||
|
- Additional service layers for analytics, prompt suggestion, session memory, plugin operations, settings sync, policy limits, team memory sync, notifier, voice, and more under `src/services/*`.
|
||||||
|
|
||||||
|
### Rust exists
|
||||||
|
Evidence:
|
||||||
|
- Core Anthropic API client in `rust/crates/api/src/{client,error,sse,types}.rs`.
|
||||||
|
- OAuth support in `rust/crates/runtime/src/oauth.rs`.
|
||||||
|
- MCP config/bootstrap/client support in `rust/crates/runtime/src/{config,mcp,mcp_client,mcp_stdio}.rs`.
|
||||||
|
- Usage accounting in `rust/crates/runtime/src/usage.rs`.
|
||||||
|
- Remote upstream-proxy support in `rust/crates/runtime/src/remote.rs`.
|
||||||
|
|
||||||
|
### Missing or broken in Rust
|
||||||
|
- Most TS service ecosystem beyond core messaging/auth/MCP is absent.
|
||||||
|
- No TS-equivalent plugin service layer.
|
||||||
|
- No TS-equivalent analytics/settings-sync/policy-limit/team-memory subsystems.
|
||||||
|
- No TS-style MCP connection-manager/UI layer.
|
||||||
|
- Model/provider ergonomics remain thinner than TS.
|
||||||
|
|
||||||
|
**Status:** core foundation exists; broader service ecosystem missing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical bug status in this worktree
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **Prompt mode tools enabled**
|
||||||
|
- `rust/crates/rusty-claude-cli/src/main.rs` now constructs prompt mode with `LiveCli::new(model, true, ...)`.
|
||||||
|
- **Default permission mode = DangerFullAccess**
|
||||||
|
- Runtime default now resolves to `DangerFullAccess` in `rust/crates/rusty-claude-cli/src/main.rs`.
|
||||||
|
- Clap default also uses `DangerFullAccess` in `rust/crates/rusty-claude-cli/src/args.rs`.
|
||||||
|
- Init template writes `dontAsk` in `rust/crates/rusty-claude-cli/src/init.rs`.
|
||||||
|
- **Streaming `{}` tool-input prefix bug**
|
||||||
|
- `rust/crates/rusty-claude-cli/src/main.rs` now strips the initial empty object only for streaming tool input, while preserving legitimate `{}` in non-stream responses.
|
||||||
|
- **Unlimited max_iterations**
|
||||||
|
- Verified at `rust/crates/runtime/src/conversation.rs` with `usize::MAX`.
|
||||||
|
|
||||||
|
### Remaining notable parity issue
|
||||||
|
- **JSON prompt output cleanliness**
|
||||||
|
- Tool-capable JSON mode now loops, but empirical verification still shows pre-JSON human-readable tool-result output when tools fire.
|
||||||
@@ -386,13 +386,13 @@ mod tests {
|
|||||||
fn session_state_tracks_config_values() {
|
fn session_state_tracks_config_values() {
|
||||||
let config = SessionConfig {
|
let config = SessionConfig {
|
||||||
model: "claude".into(),
|
model: "claude".into(),
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
config: Some(PathBuf::from("settings.toml")),
|
config: Some(PathBuf::from("settings.toml")),
|
||||||
output_format: OutputFormat::Text,
|
output_format: OutputFormat::Text,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(config.model, "claude");
|
assert_eq!(config.model, "claude");
|
||||||
assert_eq!(config.permission_mode, PermissionMode::WorkspaceWrite);
|
assert_eq!(config.permission_mode, PermissionMode::DangerFullAccess);
|
||||||
assert_eq!(config.config, Some(PathBuf::from("settings.toml")));
|
assert_eq!(config.config, Some(PathBuf::from("settings.toml")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub struct Cli {
|
|||||||
#[arg(long, default_value = "claude-opus-4-6")]
|
#[arg(long, default_value = "claude-opus-4-6")]
|
||||||
pub model: String,
|
pub model: String,
|
||||||
|
|
||||||
#[arg(long, value_enum, default_value_t = PermissionMode::WorkspaceWrite)]
|
#[arg(long, value_enum, default_value_t = PermissionMode::DangerFullAccess)]
|
||||||
pub permission_mode: PermissionMode,
|
pub permission_mode: PermissionMode,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -99,4 +99,10 @@ mod tests {
|
|||||||
let logout = Cli::parse_from(["rusty-claude-cli", "logout"]);
|
let logout = Cli::parse_from(["rusty-claude-cli", "logout"]);
|
||||||
assert_eq!(logout.command, Some(Command::Logout));
|
assert_eq!(logout.command, Some(Command::Logout));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_to_danger_full_access_permission_mode() {
|
||||||
|
let cli = Cli::parse_from(["rusty-claude-cli"]);
|
||||||
|
assert_eq!(cli.permission_mode, PermissionMode::DangerFullAccess);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
|
|||||||
const STARTER_CLAUDE_JSON: &str = concat!(
|
const STARTER_CLAUDE_JSON: &str = concat!(
|
||||||
"{\n",
|
"{\n",
|
||||||
" \"permissions\": {\n",
|
" \"permissions\": {\n",
|
||||||
" \"defaultMode\": \"acceptEdits\"\n",
|
" \"defaultMode\": \"dontAsk\"\n",
|
||||||
" }\n",
|
" }\n",
|
||||||
"}\n",
|
"}\n",
|
||||||
);
|
);
|
||||||
@@ -366,7 +366,7 @@ mod tests {
|
|||||||
concat!(
|
concat!(
|
||||||
"{\n",
|
"{\n",
|
||||||
" \"permissions\": {\n",
|
" \"permissions\": {\n",
|
||||||
" \"defaultMode\": \"acceptEdits\"\n",
|
" \"defaultMode\": \"dontAsk\"\n",
|
||||||
" }\n",
|
" }\n",
|
||||||
"}\n",
|
"}\n",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
output_format,
|
output_format,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
} => LiveCli::new(model, false, allowed_tools, permission_mode)?
|
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
|
||||||
.run_turn_with_output(&prompt, output_format)?,
|
.run_turn_with_output(&prompt, output_format)?,
|
||||||
CliAction::Login => run_login()?,
|
CliAction::Login => run_login()?,
|
||||||
CliAction::Logout => run_logout()?,
|
CliAction::Logout => run_logout()?,
|
||||||
@@ -350,7 +350,7 @@ fn default_permission_mode() -> PermissionMode {
|
|||||||
.ok()
|
.ok()
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(normalize_permission_mode)
|
.and_then(normalize_permission_mode)
|
||||||
.map_or(PermissionMode::WorkspaceWrite, permission_mode_from_label)
|
.map_or(PermissionMode::DangerFullAccess, permission_mode_from_label)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
|
fn filter_tool_specs(allowed_tools: Option<&AllowedToolSet>) -> Vec<tools::ToolSpec> {
|
||||||
@@ -968,6 +968,7 @@ impl LiveCli {
|
|||||||
model.clone(),
|
model.clone(),
|
||||||
system_prompt.clone(),
|
system_prompt.clone(),
|
||||||
enable_tools,
|
enable_tools,
|
||||||
|
true,
|
||||||
allowed_tools.clone(),
|
allowed_tools.clone(),
|
||||||
permission_mode,
|
permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1052,43 +1053,33 @@ impl LiveCli {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
fn run_prompt_json(&mut self, input: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let client = AnthropicClient::from_auth(resolve_cli_auth_source()?)
|
let session = self.runtime.session().clone();
|
||||||
.with_base_url(api::read_base_url());
|
let mut runtime = build_runtime(
|
||||||
let request = MessageRequest {
|
session,
|
||||||
model: self.model.clone(),
|
self.model.clone(),
|
||||||
max_tokens: max_tokens_for_model(&self.model),
|
self.system_prompt.clone(),
|
||||||
messages: vec![InputMessage {
|
true,
|
||||||
role: "user".to_string(),
|
false,
|
||||||
content: vec![InputContentBlock::Text {
|
self.allowed_tools.clone(),
|
||||||
text: input.to_string(),
|
self.permission_mode,
|
||||||
}],
|
)?;
|
||||||
}],
|
let mut permission_prompter = CliPermissionPrompter::new(self.permission_mode);
|
||||||
system: (!self.system_prompt.is_empty()).then(|| self.system_prompt.join("\n\n")),
|
let summary = runtime.run_turn(input, Some(&mut permission_prompter))?;
|
||||||
tools: None,
|
self.runtime = runtime;
|
||||||
tool_choice: None,
|
self.persist_session()?;
|
||||||
stream: false,
|
|
||||||
};
|
|
||||||
let runtime = tokio::runtime::Runtime::new()?;
|
|
||||||
let response = runtime.block_on(client.send_message(&request))?;
|
|
||||||
let text = response
|
|
||||||
.content
|
|
||||||
.iter()
|
|
||||||
.filter_map(|block| match block {
|
|
||||||
OutputContentBlock::Text { text } => Some(text.as_str()),
|
|
||||||
OutputContentBlock::ToolUse { .. } => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("");
|
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
json!({
|
json!({
|
||||||
"message": text,
|
"message": final_assistant_text(&summary),
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
|
"iterations": summary.iterations,
|
||||||
|
"tool_uses": collect_tool_uses(&summary),
|
||||||
|
"tool_results": collect_tool_results(&summary),
|
||||||
"usage": {
|
"usage": {
|
||||||
"input_tokens": response.usage.input_tokens,
|
"input_tokens": summary.usage.input_tokens,
|
||||||
"output_tokens": response.usage.output_tokens,
|
"output_tokens": summary.usage.output_tokens,
|
||||||
"cache_creation_input_tokens": response.usage.cache_creation_input_tokens,
|
"cache_creation_input_tokens": summary.usage.cache_creation_input_tokens,
|
||||||
"cache_read_input_tokens": response.usage.cache_read_input_tokens,
|
"cache_read_input_tokens": summary.usage.cache_read_input_tokens,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -1214,6 +1205,7 @@ impl LiveCli {
|
|||||||
model.clone(),
|
model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1256,6 +1248,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1280,6 +1273,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1314,6 +1308,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1385,6 +1380,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1414,6 +1410,7 @@ impl LiveCli {
|
|||||||
self.model.clone(),
|
self.model.clone(),
|
||||||
self.system_prompt.clone(),
|
self.system_prompt.clone(),
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
self.allowed_tools.clone(),
|
self.allowed_tools.clone(),
|
||||||
self.permission_mode,
|
self.permission_mode,
|
||||||
)?;
|
)?;
|
||||||
@@ -1881,14 +1878,15 @@ fn build_runtime(
|
|||||||
model: String,
|
model: String,
|
||||||
system_prompt: Vec<String>,
|
system_prompt: Vec<String>,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
emit_output: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
|
||||||
{
|
{
|
||||||
Ok(ConversationRuntime::new(
|
Ok(ConversationRuntime::new(
|
||||||
session,
|
session,
|
||||||
AnthropicRuntimeClient::new(model, enable_tools, allowed_tools.clone())?,
|
AnthropicRuntimeClient::new(model, enable_tools, emit_output, allowed_tools.clone())?,
|
||||||
CliToolExecutor::new(allowed_tools),
|
CliToolExecutor::new(allowed_tools, emit_output),
|
||||||
permission_policy(permission_mode),
|
permission_policy(permission_mode),
|
||||||
system_prompt,
|
system_prompt,
|
||||||
))
|
))
|
||||||
@@ -1945,6 +1943,7 @@ struct AnthropicRuntimeClient {
|
|||||||
client: AnthropicClient,
|
client: AnthropicClient,
|
||||||
model: String,
|
model: String,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
emit_output: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1952,6 +1951,7 @@ impl AnthropicRuntimeClient {
|
|||||||
fn new(
|
fn new(
|
||||||
model: String,
|
model: String,
|
||||||
enable_tools: bool,
|
enable_tools: bool,
|
||||||
|
emit_output: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
) -> Result<Self, Box<dyn std::error::Error>> {
|
) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -1960,6 +1960,7 @@ impl AnthropicRuntimeClient {
|
|||||||
.with_base_url(api::read_base_url()),
|
.with_base_url(api::read_base_url()),
|
||||||
model,
|
model,
|
||||||
enable_tools,
|
enable_tools,
|
||||||
|
emit_output,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2004,6 +2005,12 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
.await
|
.await
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
|
let mut sink = io::sink();
|
||||||
|
let out: &mut dyn Write = if self.emit_output {
|
||||||
|
&mut stdout
|
||||||
|
} else {
|
||||||
|
&mut sink
|
||||||
|
};
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut pending_tool: Option<(String, String, String)> = None;
|
let mut pending_tool: Option<(String, String, String)> = None;
|
||||||
let mut saw_stop = false;
|
let mut saw_stop = false;
|
||||||
@@ -2016,22 +2023,23 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
match event {
|
match event {
|
||||||
ApiStreamEvent::MessageStart(start) => {
|
ApiStreamEvent::MessageStart(start) => {
|
||||||
for block in start.message.content {
|
for block in start.message.content {
|
||||||
push_output_block(block, &mut stdout, &mut events, &mut pending_tool)?;
|
push_output_block(block, out, &mut events, &mut pending_tool, true)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ApiStreamEvent::ContentBlockStart(start) => {
|
ApiStreamEvent::ContentBlockStart(start) => {
|
||||||
push_output_block(
|
push_output_block(
|
||||||
start.content_block,
|
start.content_block,
|
||||||
&mut stdout,
|
out,
|
||||||
&mut events,
|
&mut events,
|
||||||
&mut pending_tool,
|
&mut pending_tool,
|
||||||
|
true,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
|
ApiStreamEvent::ContentBlockDelta(delta) => match delta.delta {
|
||||||
ContentBlockDelta::TextDelta { text } => {
|
ContentBlockDelta::TextDelta { text } => {
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
write!(stdout, "{text}")
|
write!(out, "{text}")
|
||||||
.and_then(|()| stdout.flush())
|
.and_then(|()| out.flush())
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
events.push(AssistantEvent::TextDelta(text));
|
events.push(AssistantEvent::TextDelta(text));
|
||||||
}
|
}
|
||||||
@@ -2045,13 +2053,9 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
ApiStreamEvent::ContentBlockStop(_) => {
|
ApiStreamEvent::ContentBlockStop(_) => {
|
||||||
if let Some((id, name, input)) = pending_tool.take() {
|
if let Some((id, name, input)) = pending_tool.take() {
|
||||||
// Display tool call now that input is fully accumulated
|
// Display tool call now that input is fully accumulated
|
||||||
writeln!(
|
writeln!(out, "\n{}", format_tool_call_start(&name, &input))
|
||||||
stdout,
|
.and_then(|()| out.flush())
|
||||||
"\n{}",
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
format_tool_call_start(&name, &input)
|
|
||||||
)
|
|
||||||
.and_then(|()| stdout.flush())
|
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2094,11 +2098,67 @@ impl ApiClient for AnthropicRuntimeClient {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
.map_err(|error| RuntimeError::new(error.to_string()))?;
|
||||||
response_to_events(response, &mut stdout)
|
response_to_events(response, out)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn final_assistant_text(summary: &runtime::TurnSummary) -> String {
|
||||||
|
summary
|
||||||
|
.assistant_messages
|
||||||
|
.last()
|
||||||
|
.map(|message| {
|
||||||
|
message
|
||||||
|
.blocks
|
||||||
|
.iter()
|
||||||
|
.filter_map(|block| match block {
|
||||||
|
ContentBlock::Text { text } => Some(text.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("")
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_tool_uses(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
||||||
|
summary
|
||||||
|
.assistant_messages
|
||||||
|
.iter()
|
||||||
|
.flat_map(|message| message.blocks.iter())
|
||||||
|
.filter_map(|block| match block {
|
||||||
|
ContentBlock::ToolUse { id, name, input } => Some(json!({
|
||||||
|
"id": id,
|
||||||
|
"name": name,
|
||||||
|
"input": input,
|
||||||
|
})),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_tool_results(summary: &runtime::TurnSummary) -> Vec<serde_json::Value> {
|
||||||
|
summary
|
||||||
|
.tool_results
|
||||||
|
.iter()
|
||||||
|
.flat_map(|message| message.blocks.iter())
|
||||||
|
.filter_map(|block| match block {
|
||||||
|
ContentBlock::ToolResult {
|
||||||
|
tool_use_id,
|
||||||
|
tool_name,
|
||||||
|
output,
|
||||||
|
is_error,
|
||||||
|
} => Some(json!({
|
||||||
|
"tool_use_id": tool_use_id,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"output": output,
|
||||||
|
"is_error": is_error,
|
||||||
|
})),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn slash_command_completion_candidates() -> Vec<String> {
|
fn slash_command_completion_candidates() -> Vec<String> {
|
||||||
slash_command_specs()
|
slash_command_specs()
|
||||||
.iter()
|
.iter()
|
||||||
@@ -2131,8 +2191,7 @@ fn format_tool_call_start(name: &str, input: &str) -> String {
|
|||||||
let lines = parsed
|
let lines = parsed
|
||||||
.get("content")
|
.get("content")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|c| c.lines().count())
|
.map_or(0, |c| c.lines().count());
|
||||||
.unwrap_or(0);
|
|
||||||
format!("{path} ({lines} lines)")
|
format!("{path} ({lines} lines)")
|
||||||
}
|
}
|
||||||
"edit_file" | "Edit" => {
|
"edit_file" | "Edit" => {
|
||||||
@@ -2185,13 +2244,6 @@ fn summarize_tool_payload(payload: &str) -> String {
|
|||||||
truncate_for_summary(&compact, 96)
|
truncate_for_summary(&compact, 96)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prettify_tool_payload(payload: &str) -> String {
|
|
||||||
match serde_json::from_str::<serde_json::Value>(payload) {
|
|
||||||
Ok(value) => serde_json::to_string_pretty(&value).unwrap_or_else(|_| payload.to_string()),
|
|
||||||
Err(_) => payload.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_for_summary(value: &str, limit: usize) -> String {
|
fn truncate_for_summary(value: &str, limit: usize) -> String {
|
||||||
let mut chars = value.chars();
|
let mut chars = value.chars();
|
||||||
let truncated = chars.by_ref().take(limit).collect::<String>();
|
let truncated = chars.by_ref().take(limit).collect::<String>();
|
||||||
@@ -2204,9 +2256,10 @@ fn truncate_for_summary(value: &str, limit: usize) -> String {
|
|||||||
|
|
||||||
fn push_output_block(
|
fn push_output_block(
|
||||||
block: OutputContentBlock,
|
block: OutputContentBlock,
|
||||||
out: &mut impl Write,
|
out: &mut (impl Write + ?Sized),
|
||||||
events: &mut Vec<AssistantEvent>,
|
events: &mut Vec<AssistantEvent>,
|
||||||
pending_tool: &mut Option<(String, String, String)>,
|
pending_tool: &mut Option<(String, String, String)>,
|
||||||
|
streaming_tool_input: bool,
|
||||||
) -> Result<(), RuntimeError> {
|
) -> Result<(), RuntimeError> {
|
||||||
match block {
|
match block {
|
||||||
OutputContentBlock::Text { text } => {
|
OutputContentBlock::Text { text } => {
|
||||||
@@ -2219,9 +2272,12 @@ fn push_output_block(
|
|||||||
}
|
}
|
||||||
OutputContentBlock::ToolUse { id, name, input } => {
|
OutputContentBlock::ToolUse { id, name, input } => {
|
||||||
// During streaming, the initial content_block_start has an empty input ({}).
|
// During streaming, the initial content_block_start has an empty input ({}).
|
||||||
// The real input arrives via input_json_delta events.
|
// The real input arrives via input_json_delta events. In
|
||||||
// Start with empty string so deltas build the correct JSON.
|
// non-streaming responses, preserve a legitimate empty object.
|
||||||
let initial_input = if input.is_object() && input.as_object().map_or(false, |o| o.is_empty()) {
|
let initial_input = if streaming_tool_input
|
||||||
|
&& input.is_object()
|
||||||
|
&& input.as_object().is_some_and(serde_json::Map::is_empty)
|
||||||
|
{
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} else {
|
||||||
input.to_string()
|
input.to_string()
|
||||||
@@ -2234,13 +2290,13 @@ fn push_output_block(
|
|||||||
|
|
||||||
fn response_to_events(
|
fn response_to_events(
|
||||||
response: MessageResponse,
|
response: MessageResponse,
|
||||||
out: &mut impl Write,
|
out: &mut (impl Write + ?Sized),
|
||||||
) -> Result<Vec<AssistantEvent>, RuntimeError> {
|
) -> Result<Vec<AssistantEvent>, RuntimeError> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let mut pending_tool = None;
|
let mut pending_tool = None;
|
||||||
|
|
||||||
for block in response.content {
|
for block in response.content {
|
||||||
push_output_block(block, out, &mut events, &mut pending_tool)?;
|
push_output_block(block, out, &mut events, &mut pending_tool, false)?;
|
||||||
if let Some((id, name, input)) = pending_tool.take() {
|
if let Some((id, name, input)) = pending_tool.take() {
|
||||||
events.push(AssistantEvent::ToolUse { id, name, input });
|
events.push(AssistantEvent::ToolUse { id, name, input });
|
||||||
}
|
}
|
||||||
@@ -2258,13 +2314,15 @@ fn response_to_events(
|
|||||||
|
|
||||||
struct CliToolExecutor {
|
struct CliToolExecutor {
|
||||||
renderer: TerminalRenderer,
|
renderer: TerminalRenderer,
|
||||||
|
emit_output: bool,
|
||||||
allowed_tools: Option<AllowedToolSet>,
|
allowed_tools: Option<AllowedToolSet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliToolExecutor {
|
impl CliToolExecutor {
|
||||||
fn new(allowed_tools: Option<AllowedToolSet>) -> Self {
|
fn new(allowed_tools: Option<AllowedToolSet>, emit_output: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
renderer: TerminalRenderer::new(),
|
renderer: TerminalRenderer::new(),
|
||||||
|
emit_output,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2285,17 +2343,21 @@ impl ToolExecutor for CliToolExecutor {
|
|||||||
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
|
||||||
match execute_tool(tool_name, &value) {
|
match execute_tool(tool_name, &value) {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
let markdown = format_tool_result(tool_name, &output, false);
|
if self.emit_output {
|
||||||
self.renderer
|
let markdown = format_tool_result(tool_name, &output, false);
|
||||||
.stream_markdown(&markdown, &mut io::stdout())
|
self.renderer
|
||||||
.map_err(|error| ToolError::new(error.to_string()))?;
|
.stream_markdown(&markdown, &mut io::stdout())
|
||||||
|
.map_err(|error| ToolError::new(error.to_string()))?;
|
||||||
|
}
|
||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let markdown = format_tool_result(tool_name, &error, true);
|
if self.emit_output {
|
||||||
self.renderer
|
let markdown = format_tool_result(tool_name, &error, true);
|
||||||
.stream_markdown(&markdown, &mut io::stdout())
|
self.renderer
|
||||||
.map_err(|stream_error| ToolError::new(stream_error.to_string()))?;
|
.stream_markdown(&markdown, &mut io::stdout())
|
||||||
|
.map_err(|stream_error| ToolError::new(stream_error.to_string()))?;
|
||||||
|
}
|
||||||
Err(ToolError::new(error))
|
Err(ToolError::new(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2402,7 +2464,10 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
|
|||||||
out,
|
out,
|
||||||
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
" --permission-mode MODE Set read-only, workspace-write, or danger-full-access"
|
||||||
)?;
|
)?;
|
||||||
writeln!(out, " --dangerously-skip-permissions Skip all permission checks")?;
|
writeln!(
|
||||||
|
out,
|
||||||
|
" --dangerously-skip-permissions Skip all permission checks"
|
||||||
|
)?;
|
||||||
writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
|
writeln!(out, " --allowedTools TOOLS Restrict enabled tools (repeatable; comma-separated aliases supported)")?;
|
||||||
writeln!(
|
writeln!(
|
||||||
out,
|
out,
|
||||||
@@ -2451,11 +2516,13 @@ mod tests {
|
|||||||
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
|
format_model_switch_report, format_permissions_report, format_permissions_switch_report,
|
||||||
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
format_resume_report, format_status_report, format_tool_call_start, format_tool_result,
|
||||||
normalize_permission_mode, parse_args, parse_git_status_metadata, print_help_to,
|
normalize_permission_mode, parse_args, parse_git_status_metadata, print_help_to,
|
||||||
render_config_report, render_memory_report, render_repl_help, resolve_model_alias,
|
push_output_block, render_config_report, render_memory_report, render_repl_help,
|
||||||
resume_supported_slash_commands, status_context, CliAction, CliOutputFormat, SlashCommand,
|
resolve_model_alias, response_to_events, resume_supported_slash_commands, status_context,
|
||||||
StatusUsage, DEFAULT_MODEL,
|
CliAction, CliOutputFormat, SlashCommand, StatusUsage, DEFAULT_MODEL,
|
||||||
};
|
};
|
||||||
use runtime::{ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
use api::{MessageResponse, OutputContentBlock, Usage};
|
||||||
|
use runtime::{AssistantEvent, ContentBlock, ConversationMessage, MessageRole, PermissionMode};
|
||||||
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2465,7 +2532,7 @@ mod tests {
|
|||||||
CliAction::Repl {
|
CliAction::Repl {
|
||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2484,7 +2551,7 @@ mod tests {
|
|||||||
model: DEFAULT_MODEL.to_string(),
|
model: DEFAULT_MODEL.to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2505,7 +2572,7 @@ mod tests {
|
|||||||
model: "claude-opus".to_string(),
|
model: "claude-opus".to_string(),
|
||||||
output_format: CliOutputFormat::Json,
|
output_format: CliOutputFormat::Json,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2525,7 +2592,7 @@ mod tests {
|
|||||||
model: "claude-opus-4-6".to_string(),
|
model: "claude-opus-4-6".to_string(),
|
||||||
output_format: CliOutputFormat::Text,
|
output_format: CliOutputFormat::Text,
|
||||||
allowed_tools: None,
|
allowed_tools: None,
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2534,7 +2601,7 @@ mod tests {
|
|||||||
fn resolves_known_model_aliases() {
|
fn resolves_known_model_aliases() {
|
||||||
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6");
|
assert_eq!(resolve_model_alias("opus"), "claude-opus-4-6");
|
||||||
assert_eq!(resolve_model_alias("sonnet"), "claude-sonnet-4-6");
|
assert_eq!(resolve_model_alias("sonnet"), "claude-sonnet-4-6");
|
||||||
assert_eq!(resolve_model_alias("haiku"), "claude-haiku-3-5-20241022");
|
assert_eq!(resolve_model_alias("haiku"), "claude-haiku-4-5-20251213");
|
||||||
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
assert_eq!(resolve_model_alias("claude-opus"), "claude-opus");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2580,7 +2647,7 @@ mod tests {
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect()
|
.collect()
|
||||||
),
|
),
|
||||||
permission_mode: PermissionMode::WorkspaceWrite,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2986,11 +3053,107 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn tool_rendering_helpers_compact_output() {
|
fn tool_rendering_helpers_compact_output() {
|
||||||
let start = format_tool_call_start("read_file", r#"{"path":"src/main.rs"}"#);
|
let start = format_tool_call_start("read_file", r#"{"path":"src/main.rs"}"#);
|
||||||
assert!(start.contains("Tool call"));
|
assert!(start.contains("read_file"));
|
||||||
assert!(start.contains("src/main.rs"));
|
assert!(start.contains("src/main.rs"));
|
||||||
|
|
||||||
let done = format_tool_result("read_file", r#"{"contents":"hello"}"#, false);
|
let done = format_tool_result("read_file", r#"{"contents":"hello"}"#, false);
|
||||||
assert!(done.contains("Tool `read_file`"));
|
assert!(done.contains("read_file:"));
|
||||||
assert!(done.contains("contents"));
|
assert!(done.contains("contents"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn push_output_block_skips_empty_object_prefix_for_tool_streams() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let mut pending_tool = None;
|
||||||
|
|
||||||
|
push_output_block(
|
||||||
|
OutputContentBlock::ToolUse {
|
||||||
|
id: "tool-1".to_string(),
|
||||||
|
name: "read_file".to_string(),
|
||||||
|
input: json!({}),
|
||||||
|
},
|
||||||
|
&mut out,
|
||||||
|
&mut events,
|
||||||
|
&mut pending_tool,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.expect("tool block should accumulate");
|
||||||
|
|
||||||
|
assert!(events.is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
pending_tool,
|
||||||
|
Some(("tool-1".to_string(), "read_file".to_string(), String::new(),))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_to_events_preserves_empty_object_json_input_outside_streaming() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let events = response_to_events(
|
||||||
|
MessageResponse {
|
||||||
|
id: "msg-1".to_string(),
|
||||||
|
kind: "message".to_string(),
|
||||||
|
model: "claude-opus-4-6".to_string(),
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: vec![OutputContentBlock::ToolUse {
|
||||||
|
id: "tool-1".to_string(),
|
||||||
|
name: "read_file".to_string(),
|
||||||
|
input: json!({}),
|
||||||
|
}],
|
||||||
|
stop_reason: Some("tool_use".to_string()),
|
||||||
|
stop_sequence: None,
|
||||||
|
usage: Usage {
|
||||||
|
input_tokens: 1,
|
||||||
|
output_tokens: 1,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
},
|
||||||
|
request_id: None,
|
||||||
|
},
|
||||||
|
&mut out,
|
||||||
|
)
|
||||||
|
.expect("response conversion should succeed");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
&events[0],
|
||||||
|
AssistantEvent::ToolUse { name, input, .. }
|
||||||
|
if name == "read_file" && input == "{}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_to_events_preserves_non_empty_json_input_outside_streaming() {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let events = response_to_events(
|
||||||
|
MessageResponse {
|
||||||
|
id: "msg-2".to_string(),
|
||||||
|
kind: "message".to_string(),
|
||||||
|
model: "claude-opus-4-6".to_string(),
|
||||||
|
role: "assistant".to_string(),
|
||||||
|
content: vec![OutputContentBlock::ToolUse {
|
||||||
|
id: "tool-2".to_string(),
|
||||||
|
name: "read_file".to_string(),
|
||||||
|
input: json!({ "path": "rust/Cargo.toml" }),
|
||||||
|
}],
|
||||||
|
stop_reason: Some("tool_use".to_string()),
|
||||||
|
stop_sequence: None,
|
||||||
|
usage: Usage {
|
||||||
|
input_tokens: 1,
|
||||||
|
output_tokens: 1,
|
||||||
|
cache_creation_input_tokens: 0,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
},
|
||||||
|
request_id: None,
|
||||||
|
},
|
||||||
|
&mut out,
|
||||||
|
)
|
||||||
|
.expect("response conversion should succeed");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
&events[0],
|
||||||
|
AssistantEvent::ToolUse { name, input, .. }
|
||||||
|
if name == "read_file" && input == "{\"path\":\"rust/Cargo.toml\"}"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user