use rmcp::model::{CallToolResult, ContentBlock};
use rmcp::ErrorData as McpError;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::Serialize;
use serde_json::{json, Map, Value};
use crate::errors::WinxError;
use crate::runtime::BashCommandRuntimeResult;
use crate::state::turn::TurnState;
use crate::tools::bash_command::BashCommandState;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub(super) enum ToolResultStatus {
Completed,
CompletedWithIssues,
Running,
AwaitingInput,
AwaitingApproval,
NeedsRead,
NeedsInitialize,
Conflict,
RecoveryExhausted,
NotFound,
Denied,
InvalidInput,
Failed,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct ToolNextAction {
pub tool: String,
pub instruction: String,
#[schemars(schema_with = "open_object_schema")]
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Value>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct RequiredRead {
pub path: String,
pub ranges: Vec<String>,
}
#[derive(Clone, Debug, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub(super) struct ToolResultEnvelope {
pub status: ToolResultStatus,
pub tool: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
pub retryable: bool,
pub retry_same_call: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub retry_after_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_action: Option<ToolNextAction>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub required_reads: Vec<RequiredRead>,
#[schemars(schema_with = "open_object_schema")]
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
fn open_object_schema(_: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({ "type": "object" })
}
#[allow(dead_code)] #[derive(Clone, Debug, JsonSchema)]
pub(super) struct CodeMapToolResultEnvelope {
#[schemars(flatten)]
pub result: ToolResultEnvelope,
pub truncated: Option<bool>,
pub next_cursor: Option<String>,
pub snapshot_hash: Option<String>,
pub files_scanned: Option<usize>,
pub payload_bytes: Option<usize>,
pub source_kind: Option<String>,
pub canonical: Option<bool>,
pub temporary_helper_budget: Option<crate::types::CodeMapTemporaryHelperBudget>,
pub mode: Option<String>,
pub files_shown: Option<usize>,
pub files: Option<Vec<crate::types::OutlineFile>>,
pub file_extension: Option<String>,
pub language_supported: Option<bool>,
pub fallback: Option<crate::types::CodeMapFallback>,
pub name: Option<String>,
pub definitions: Option<usize>,
pub references: Option<usize>,
pub hits: Option<Vec<crate::types::ReferenceHit>>,
}
pub(super) fn tool_failure(
tool: &str,
error: &WinxError,
arguments: Option<&Value>,
) -> Result<CallToolResult, McpError> {
if matches!(error, WinxError::SerializationError(_) | WinxError::BashStateLockError(_)) {
return Err(McpError::internal_error(format!("{tool} failed: {error}"), None));
}
let text = match (tool, error) {
(
"Initialize",
WinxError::WorkspaceBindingMismatch { requested_workspace, bound_workspace, .. },
) => format!(
"Initialize rejected: this conversation is already bound to {}. Do not call \
Initialize again for {}. Keep the existing thread_id/workspace_root pair; an \
allowed target outside that workspace can be accessed by its absolute path without \
rebinding. If the user truly intends a different project, start a new conversation \
or client session.",
bound_workspace.display(),
requested_workspace.display()
),
("Initialize", WinxError::WorkspaceChangeRequiresNewSession { workspace_root }) => {
format!(
"Initialize rejected: an existing remote conversation cannot change its project \
identity in place to {}. Keep using the current binding for allowed external \
paths, or start a new conversation/client session for that project. Do not \
repeat this Initialize call.",
workspace_root.display()
)
}
("EditFiles", error) if error.is_search_match_conflict() => {
format!(
"{tool} failed: {error}\n\nNo file was changed by this call. Winx invalidated \
the prior read permit for the conflicting target. Execute the exact ReadFiles \
nextAction, then make one corrected EditFiles retry with mode=line_patch using \
its returned revision and visible coordinates. Do not retry SEARCH or use \
BashCommand, sed, or Python for the ordinary file edit."
)
}
("FileWriteOrEdit" | "MultiFileEdit", error) if error.is_search_match_conflict() => {
format!(
"{tool} failed: {error}\n\nNo file was changed by this call. Winx invalidated \
the prior read permit for the conflicting target. Call the exact ReadFiles \
nextAction, rebuild SEARCH from the returned current text, and do not retry this \
edit unchanged. A shell read does not refresh the edit guard."
)
}
_ => format!("{tool} failed: {error}"),
};
let envelope = error_envelope(tool, error, arguments, text.clone());
let mut result = CallToolResult::error(vec![ContentBlock::text(text)]);
result.structured_content = serde_json::to_value(envelope).ok();
Ok(result)
}
pub(super) fn decorate_success(tool: &str, arguments: Option<&Value>, result: &mut CallToolResult) {
if result.is_error == Some(true) || tool == "BashCommand" {
return;
}
if result.structured_content.as_ref().is_some_and(|structured| {
structured.get("tool").and_then(Value::as_str) == Some(tool)
&& structured.get("status").and_then(Value::as_str).is_some()
}) {
return;
}
let existing = result.structured_content.take();
let output = content_text(result).filter(|_| {
!existing
.as_ref()
.and_then(Value::as_object)
.is_some_and(|navigation| code_map_symbols_are_structured(tool, navigation))
});
let text = output.clone().unwrap_or_default();
let mut data = safe_success_data(tool, arguments, &text, false);
if let Some(existing) = existing.as_ref() {
if matches!(
tool,
"Initialize"
| "ReadFiles"
| "FileWriteOrEdit"
| "MultiFileEdit"
| "UndoEdit"
| "ApplyPatch"
| "EditFiles"
) {
if let Value::Object(metadata) = existing {
for (key, value) in metadata {
if tool == "Initialize"
&& matches!(key.as_str(), "thread_id" | "workspace_root")
{
data.insert(key.clone(), value.clone());
} else {
data.entry(key.clone()).or_insert_with(|| value.clone());
}
}
} else {
data.insert("result".to_string(), existing.clone());
}
} else if tool != "CodeMap" {
data.insert("result".to_string(), existing.clone());
}
}
let envelope = ToolResultEnvelope {
status: ToolResultStatus::Completed,
tool: tool.to_string(),
message: success_message(tool, ToolResultStatus::Completed),
output,
error_code: None,
retryable: false,
retry_same_call: false,
retry_after_ms: None,
next_action: None,
required_reads: Vec::new(),
data: (!data.is_empty()).then_some(Value::Object(data)),
};
let Ok(mut value) = serde_json::to_value(envelope) else {
result.structured_content = existing;
return;
};
if tool == "CodeMap" {
if let (Value::Object(target), Some(Value::Object(source))) = (&mut value, existing) {
for (key, item) in source {
target.entry(key).or_insert(item);
}
}
}
result.structured_content = Some(value);
}
pub(super) fn attach_prepared_edit_context(
result: &mut CallToolResult,
prepared: &crate::tools::edit_files::PreparedEditContext,
) {
let Some(Value::Object(envelope)) = result.structured_content.as_mut() else { return };
let data = envelope.entry("data").or_insert_with(|| Value::Object(Map::new()));
let Some(data) = data.as_object_mut() else { return };
let target_paths = prepared.target_paths();
data.insert("file_count".to_string(), json!(target_paths.len()));
data.insert("file_paths".to_string(), json!(target_paths));
if let Some(path) = target_paths.first().filter(|_| target_paths.len() == 1) {
data.insert("file_path".to_string(), Value::String(path.clone()));
}
data.insert(
"operation".to_string(),
Value::String(prepared.command.operation().as_str().to_string()),
);
data.insert(
"edit_modes".to_string(),
Value::Array(
prepared
.command
.modes()
.into_iter()
.map(|mode| Value::String(mode.as_str().to_string()))
.collect(),
),
);
}
fn edit_verification_presentation(
tool: &str,
edit_text: &str,
verification_text: &str,
verification_error: bool,
active: bool,
nested_status: &str,
exit_code: Option<i64>,
) -> (String, String) {
let status_label = if verification_error {
"completed_with_issues"
} else if active {
nested_status
} else {
"completed"
};
let message = if verification_error {
format!(
"{tool} applied the edit, but verification failed. The edit remains applied; do not \
repeat the original edit. Diagnose the verification output and make only corrective \
changes."
)
} else if active {
format!("{tool} applied the edit; verification is still {status_label}.")
} else {
format!("{tool} applied the edit and verification completed.")
};
let summary = if verification_error {
exit_code.map_or_else(
|| "EDIT APPLIED. Verification failed; do not repeat the original edit.".to_string(),
|code| {
format!(
"EDIT APPLIED. Verification failed with exit code {code}; do not repeat the \
original edit."
)
},
)
} else if active {
format!("Verification is still {status_label}.")
} else {
exit_code.map_or_else(
|| "Verification completed.".to_string(),
|code| format!("Verification completed with exit code {code}."),
)
};
let combined_text = if verification_error && verification_text.trim().is_empty() {
format!("{summary}\n\n{edit_text}")
} else if verification_error {
format!("{summary}\n\n{edit_text}\n\n{verification_text}")
} else if verification_text.trim().is_empty() {
format!("{edit_text}\n\n{summary}")
} else {
format!("{edit_text}\n\n{summary}\n{verification_text}")
};
(message, combined_text)
}
pub(super) struct VerificationRecovery {
pub id: String,
pub tool: &'static str,
pub arguments: Value,
}
pub(super) fn edit_verification_result(
tool: &str,
arguments: Option<&Value>,
edit_text: &str,
verification: CallToolResult,
recovery: &VerificationRecovery,
) -> CallToolResult {
let verification_text = result_text(&verification);
let verification_is_error = verification.is_error == Some(true);
let nested = verification.structured_content.unwrap_or_else(|| {
json!({
"status": if verification_is_error { "failed" } else { "completed" },
"tool": "BashCommand",
"message": "Verification returned no structured result."
})
});
let nested_status = nested.get("status").and_then(Value::as_str).unwrap_or("failed");
let exit_code =
nested.get("data").and_then(|data| data.get("exit_code")).and_then(Value::as_i64);
let nonzero_exit = exit_code.is_some_and(|code| code != 0);
let verification_error = verification_is_error || nonzero_exit;
let active = matches!(nested_status, "running" | "awaiting_input" | "awaiting_approval");
let outer_status = if verification_error {
ToolResultStatus::CompletedWithIssues
} else if active {
match nested_status {
"running" => ToolResultStatus::Running,
"awaiting_input" => ToolResultStatus::AwaitingInput,
"awaiting_approval" => ToolResultStatus::AwaitingApproval,
_ => ToolResultStatus::Completed,
}
} else {
ToolResultStatus::Completed
};
let (message, combined_text) = edit_verification_presentation(
tool,
edit_text,
&verification_text,
verification_error,
active,
nested_status,
exit_code,
);
let mut data = safe_success_data(tool, arguments, &combined_text, false);
data.insert("edit_applied".to_string(), Value::Bool(true));
data.insert("original_edit_retryable".to_string(), Value::Bool(false));
data.insert("follow_up_required".to_string(), Value::Bool(verification_error || active));
data.insert(
"verification_status".to_string(),
Value::String(if verification_error { "failed" } else { nested_status }.to_string()),
);
if !active {
data.insert("verification_passed".to_string(), Value::Bool(!verification_error));
}
data.insert("verification".to_string(), nested.clone());
data.insert("verification_id".to_string(), Value::String(recovery.id.clone()));
if let Some(exit_code) = exit_code {
data.insert("verification_exit_code".to_string(), json!(exit_code));
}
let mut envelope = json!({
"status": outer_status,
"tool": tool,
"message": message,
"retryable": if verification_error {
false
} else {
nested.get("retryable").and_then(Value::as_bool).unwrap_or(false)
},
"retrySameCall": false,
"requiredReads": [],
"data": data,
});
if verification_error {
let nested_code =
nested.get("errorCode").and_then(Value::as_str).unwrap_or("execution_failed");
envelope["errorCode"] = Value::String(if nonzero_exit {
"verification_failed".to_string()
} else {
format!("verification_{nested_code}")
});
}
for key in ["retryAfterMs", "nextAction"] {
if let Some(value) = nested.get(key) {
envelope[key] = value.clone();
}
}
if verification_error || active {
envelope["nextAction"] = json!({
"tool": recovery.tool,
"instruction": if active {
"The edit is already applied and verification is still active. Poll these exact receipt-bound verification arguments; never issue a generic status_check or repeat the original edit."
} else {
"The edit is already applied. Diagnose the output and make only corrective changes. After that, execute these exact receipt-bound verification arguments; never repeat the original edit."
},
"arguments": recovery.arguments
});
}
let mut result = CallToolResult::success(vec![ContentBlock::text(combined_text)]);
result.structured_content = Some(envelope);
result
}
pub(super) fn verify_edit_result(
arguments: Option<&Value>,
verification_id: &str,
verification: CallToolResult,
) -> CallToolResult {
let verification_text = result_text(&verification);
let verification_is_error = verification.is_error == Some(true);
let nested = verification.structured_content.unwrap_or_else(|| {
json!({
"status": if verification_is_error { "failed" } else { "completed" },
"tool": "BashCommand",
"message": "Verification returned no structured result."
})
});
let nested_status = nested.get("status").and_then(Value::as_str).unwrap_or("failed");
let exit_code =
nested.get("data").and_then(|data| data.get("exit_code")).and_then(Value::as_i64);
let failed = verification_is_error || exit_code.is_some_and(|code| code != 0);
let active = matches!(nested_status, "running" | "awaiting_input" | "awaiting_approval");
let status = if failed {
ToolResultStatus::Failed
} else if active {
match nested_status {
"running" => ToolResultStatus::Running,
"awaiting_input" => ToolResultStatus::AwaitingInput,
"awaiting_approval" => ToolResultStatus::AwaitingApproval,
_ => ToolResultStatus::Completed,
}
} else {
ToolResultStatus::Completed
};
let message = if failed {
"VerifyEdit failed. The original edit remains applied; fix the reported issue before rerunning this verification."
} else if active {
"VerifyEdit is still running."
} else {
"VerifyEdit passed. The original edit remains committed."
};
let rendered = if failed {
format!(
"VERIFICATION FAILED. Do not repeat the original edit. Make corrective changes first.\n\n{verification_text}"
)
} else {
verification_text
};
let mut data = safe_success_data("VerifyEdit", arguments, &rendered, false);
data.insert("verification_id".to_string(), Value::String(verification_id.to_string()));
data.insert("verification_passed".to_string(), Value::Bool(!failed && !active));
data.insert("original_edit_retryable".to_string(), Value::Bool(false));
data.insert("verification".to_string(), nested.clone());
if let Some(exit_code) = exit_code {
data.insert("verification_exit_code".to_string(), json!(exit_code));
}
let mut envelope = json!({
"status": status,
"tool": "VerifyEdit",
"message": message,
"retryable": failed,
"retrySameCall": false,
"requiredReads": [],
"data": data,
});
if failed {
envelope["errorCode"] = Value::String("verification_failed".to_string());
envelope["nextAction"] = json!({
"tool": "VerifyEdit",
"instruction": "Do not retry unchanged. Correct the diagnosed code or configuration first, then rerun the same verification receipt.",
"arguments": arguments
});
} else if active {
if let Some(value) = nested.get("retryAfterMs") {
envelope["retryAfterMs"] = value.clone();
}
envelope["nextAction"] = json!({
"tool": "VerifyEdit",
"instruction": "Poll this exact legacy verification receipt; do not use the nested Bash status action and do not repeat the edit.",
"arguments": arguments
});
}
let mut result = if failed {
CallToolResult::error(vec![ContentBlock::text(rendered)])
} else {
CallToolResult::success(vec![ContentBlock::text(rendered)])
};
result.structured_content = Some(envelope);
result
}
pub(super) fn bash_success_result(
arguments: Option<&Value>,
outcome: BashCommandRuntimeResult,
compact_output: bool,
) -> Result<CallToolResult, McpError> {
let status = bash_success_status(&outcome.result.state);
let output_truncated = outcome.output_truncated;
let next_action = bash_next_action(status, arguments, &outcome.result.state);
let compact_rendered = compact_output && outcome.compact_output.is_some();
let rendered_output = if compact_output {
outcome.compact_output.unwrap_or(outcome.result.output)
} else {
outcome.result.output
};
let mut data = safe_success_data("BashCommand", arguments, &rendered_output, output_truncated);
if compact_rendered {
data.insert("output_format".to_string(), Value::String("compact".to_string()));
}
data.insert("cwd".to_string(), Value::String(outcome.result.state.cwd.display().to_string()));
if let Some(value) = outcome.result.state.exit_code {
data.insert("exit_code".to_string(), json!(value));
}
if let Some(value) = outcome.result.state.background_id.as_ref() {
data.insert("bg_command_id".to_string(), Value::String(value.clone()));
}
if let Some(value) = outcome.result.state.running_for_seconds {
data.insert("running_for_seconds".to_string(), json!(value));
data.insert("running_for".to_string(), Value::String(format!("{value} seconds")));
}
if let Some(value) = outcome.result.state.turn_state {
data.insert("turn_state".to_string(), Value::String(value.as_str().to_string()));
}
if let Some(value) = outcome.dropped_output_file.as_ref() {
data.insert(
"dropped_output_file".to_string(),
Value::String(value.to_string_lossy().into_owned()),
);
}
if let Some(generation) = outcome.command_generation {
data.insert("command_generation".to_string(), json!(generation));
}
if let Some(background_id) = outcome.result.state.background_id.as_ref() {
data.insert(
"execution_id".to_string(),
Value::String(format!("background:{background_id}")),
);
} else if let Some(token) = outcome.execution_token.as_ref() {
data.insert(
"execution_id".to_string(),
Value::String(format!(
"{}:{}:{}",
token.guardian_epoch, token.session_epoch, token.generation
)),
);
}
let envelope = ToolResultEnvelope {
status,
tool: "BashCommand".to_string(),
message: success_message("BashCommand", status),
output: Some(rendered_output.clone()),
error_code: None,
retryable: false,
retry_same_call: false,
retry_after_ms: (status == ToolResultStatus::Running).then_some(1_000),
next_action,
required_reads: Vec::new(),
data: (!data.is_empty()).then_some(Value::Object(data)),
};
let structured_content = serde_json::to_value(envelope).map_err(|error| {
McpError::internal_error(
format!("failed to serialize typed BashCommand result: {error}"),
None,
)
})?;
let mut result = CallToolResult::success(vec![ContentBlock::text(rendered_output)]);
result.structured_content = Some(structured_content);
Ok(result)
}
pub(super) fn attach_temporary_artifact_usage(
result: &mut CallToolResult,
arguments: Option<&Value>,
usage: &crate::utils::agent_temp::TemporaryArtifactUsage,
) {
let Some(Value::Object(envelope)) = result.structured_content.as_mut() else { return };
let data = envelope.entry("data").or_insert_with(|| Value::Object(Map::new())).as_object_mut();
let Some(data) = data else { return };
if let Ok(value) = serde_json::to_value(usage) {
data.insert("temporary_artifact_budget".to_string(), value);
}
if !usage.over_budget {
return;
}
data.insert("temporary_artifact_cleanup_required".to_string(), Value::Bool(true));
let background_id = data.get("bg_command_id").and_then(Value::as_str).map(str::to_string);
let running = envelope.get("status").and_then(Value::as_str) == Some("running");
let automatic_cleanup = if usage.stale_pruned_files > 0 {
format!(
"Winx already reclaimed {} TTL-expired helper(s) / {} bytes; fresh artifacts were \
preserved.",
usage.stale_pruned_files, usage.stale_pruned_bytes
)
} else {
"No eligible TTL-expired helper was removed; fresh artifacts were preserved.".to_string()
};
let instruction = if running {
format!(
"The running process has exceeded the managed temporary-artifact budget. Interrupt it, \
then inspect and remove obsolete files beneath $WINX_TEMP_DIR before starting another \
command. {automatic_cleanup}"
)
} else {
format!(
"Inspect and remove obsolete files beneath $WINX_TEMP_DIR with a cleanup-only \
BashCommand. {automatic_cleanup} Ordinary Command actions remain blocked until this \
session is back under budget."
)
};
envelope.insert(
"message".to_string(),
Value::String(if running {
"BashCommand is running, but its temporary-artifact budget is exceeded.".to_string()
} else {
"BashCommand completed, but its temporary-artifact budget is exceeded.".to_string()
}),
);
let next_arguments = running.then(|| {
let mut action = json!({
"action_json": {
"type": "send_specials",
"send_specials": ["Ctrl-c"],
"submit": false
},
"wait_policy": "return_early"
});
copy_session_binding(arguments, &mut action);
if let Some(background_id) = background_id {
action["action_json"]["bg_command_id"] = Value::String(background_id);
}
action
});
let next_action =
ToolNextAction { tool: "BashCommand".to_string(), instruction, arguments: next_arguments };
if let Ok(value) = serde_json::to_value(next_action) {
envelope.insert("nextAction".to_string(), value);
}
let thread_id = string_argument(arguments, "thread_id").unwrap_or_default();
result.content.push(ContentBlock::text(format!(
"Winx guard: temporary_artifact_dir={} now contains {} files / {} bytes (limits: {} \
files / {} bytes; largest file {} bytes, limit {}). Stale reclamation removed {} files / \
{} bytes while preserving fresh artifacts. Inspect and remove remaining obsolete helpers \
with a cleanup-only BashCommand using thread_id={thread_id}.",
usage.temporary_artifact_dir.display(),
usage.session_files,
usage.session_bytes,
usage.max_session_files,
usage.max_session_bytes,
usage.largest_file_bytes,
usage.max_file_bytes,
usage.stale_pruned_files,
usage.stale_pruned_bytes,
)));
}
pub(super) fn result_status(result: &CallToolResult) -> String {
result
.structured_content
.as_ref()
.and_then(|value| value.get("status"))
.and_then(Value::as_str)
.unwrap_or(if result.is_error == Some(true) { "failed" } else { "completed" })
.to_string()
}
pub(super) fn result_size_bytes(result: &CallToolResult) -> usize {
let content = result
.content
.iter()
.map(|block| {
block
.as_text()
.map_or(0, |text| text.text.len())
.saturating_add(block.as_image().map_or(0, |image| image.data.len()))
})
.fold(0usize, usize::saturating_add);
content.saturating_add(
result
.structured_content
.as_ref()
.and_then(|value| serde_json::to_vec(value).ok())
.map_or(0, |value| value.len()),
)
}
pub(super) fn enforce_next_action_policy(
result: &mut CallToolResult,
policy: crate::tool_policy::ToolPolicy,
) {
let Some(Value::Object(envelope)) = result.structured_content.as_mut() else { return };
let Some(next_tool) =
envelope.get("nextAction").and_then(|action| action.get("tool")).and_then(Value::as_str)
else {
return;
};
if policy.allows_call(next_tool) {
return;
}
let edit_applied = envelope
.get("data")
.and_then(|data| data.get("edit_applied"))
.and_then(Value::as_bool)
.unwrap_or(false);
envelope.remove("nextAction");
envelope.insert("errorCode".to_string(), Value::String("policy_blocked".to_string()));
envelope.insert("retryable".to_string(), Value::Bool(false));
envelope.insert("requiredReads".to_string(), Value::Array(Vec::new()));
if !edit_applied {
envelope.insert("status".to_string(), json!(ToolResultStatus::Denied));
result.is_error = Some(true);
}
let data = envelope.entry("data").or_insert_with(|| Value::Object(Map::new()));
if let Value::Object(data) = data {
data.insert("follow_up_blocked_by_policy".to_string(), Value::Bool(true));
}
let suffix = " Required recovery is unavailable in this request's tool policy; no unauthorized action was emitted.";
let message = envelope
.get("message")
.and_then(Value::as_str)
.unwrap_or("Tool recovery is blocked by policy.")
.to_string();
envelope.insert("message".to_string(), Value::String(format!("{message}{suffix}")));
result.content.insert(0, ContentBlock::text(format!("POLICY BLOCKED.{suffix}")));
mirror_text_output(result);
}
fn result_text(result: &CallToolResult) -> String {
content_text(result).unwrap_or_default()
}
fn content_text(result: &CallToolResult) -> Option<String> {
let mut blocks = result
.content
.iter()
.filter_map(|content| content.as_text().map(|text| text.text.as_str()))
.peekable();
blocks.peek()?;
Some(blocks.collect::<Vec<_>>().join("\n"))
}
pub(super) fn mirror_text_output(result: &mut CallToolResult) {
let text = content_text(result);
let Some(Value::Object(envelope)) = result.structured_content.as_mut() else { return };
let tool = envelope.get("tool").and_then(Value::as_str).unwrap_or_default();
if code_map_symbols_are_structured(tool, envelope) {
envelope.remove("output");
return;
}
match text {
Some(text) => {
let text = crate::utils::redact::redact(&text).into_owned();
envelope.insert("output".to_string(), Value::String(text));
}
None => {
envelope.remove("output");
}
}
}
fn code_map_symbols_are_structured(tool: &str, structured: &Map<String, Value>) -> bool {
tool == "CodeMap"
&& ["files", "hits"].iter().any(|key| {
structured.get(*key).and_then(Value::as_array).is_some_and(|items| !items.is_empty())
})
}
pub(super) fn finalize_tool_result(result: &mut CallToolResult) {
for content in &mut result.content {
if let ContentBlock::Text(text) = content {
if let std::borrow::Cow::Owned(scrubbed) = crate::utils::redact::redact(&text.text) {
text.text = scrubbed;
}
}
}
if let Some(structured) = result.structured_content.as_mut() {
crate::utils::redact::redact_json(structured);
}
mirror_text_output(result);
}
fn bash_success_status(state: &BashCommandState) -> ToolResultStatus {
if !state.is_running() {
return ToolResultStatus::Completed;
}
match state.turn_state {
Some(TurnState::AwaitingApproval) => ToolResultStatus::AwaitingApproval,
Some(TurnState::AwaitingInput) => ToolResultStatus::AwaitingInput,
Some(TurnState::Busy | TurnState::Unknown) | None => ToolResultStatus::Running,
}
}
fn success_message(tool: &str, status: ToolResultStatus) -> String {
match status {
ToolResultStatus::Running => format!("{tool} is still running."),
ToolResultStatus::AwaitingInput => format!("{tool} is waiting for input."),
ToolResultStatus::AwaitingApproval => format!("{tool} is waiting for approval."),
_ => format!("{tool} completed."),
}
}
fn bash_next_action(
status: ToolResultStatus,
arguments: Option<&Value>,
bash_state: &BashCommandState,
) -> Option<ToolNextAction> {
match status {
ToolResultStatus::Running => {
let mut action = json!({
"action_json": {
"type": "status_check",
"status_check": true
}
});
copy_session_binding(arguments, &mut action);
if let Some(background_id) = bash_state.background_id.as_ref() {
action["action_json"]["bg_command_id"] = Value::String(background_id.clone());
}
Some(ToolNextAction {
tool: "BashCommand".to_string(),
instruction: "Wait for retry_after_ms, then check status. Do not repeat the original command."
.to_string(),
arguments: Some(action),
})
}
ToolResultStatus::AwaitingInput => Some(ToolNextAction {
tool: "BashCommand".to_string(),
instruction: "Inspect the returned screen, then use send_text or send_specials with the required input."
.to_string(),
arguments: None,
}),
ToolResultStatus::AwaitingApproval => Some(ToolNextAction {
tool: "BashCommand".to_string(),
instruction: "Do not approve automatically. Ask the user when approval changes permissions, data, or system state."
.to_string(),
arguments: None,
}),
_ => None,
}
}
fn safe_success_data(
tool: &str,
arguments: Option<&Value>,
text: &str,
output_truncated: bool,
) -> Map<String, Value> {
let mut data = Map::new();
data.insert("output_bytes".to_string(), json!(text.len()));
data.insert("output_truncated".to_string(), json!(output_truncated));
for key in [
"thread_id",
"operation",
"path",
"name",
"id",
"mode_name",
"any_workspace_path",
"workspace_root",
] {
if let Some(value) = string_argument(arguments, key) {
data.insert(key.to_string(), Value::String(value));
}
}
if tool == "ReadImage" {
if let Some(file_path) = string_argument(arguments, "file_path") {
data.insert("file_path".to_string(), Value::String(file_path));
}
}
if tool == "ReadFiles" {
if let Some(files) = arguments
.and_then(Value::as_object)
.and_then(|arguments| arguments.get("file_paths"))
.and_then(Value::as_array)
{
data.insert(
"file_paths".to_string(),
Value::Array(
files.iter().filter_map(Value::as_str).map(|value| json!(value)).collect(),
),
);
}
}
if tool == "BashCommand" {
if let Some(action) = bash_action(arguments) {
data.insert("action".to_string(), Value::String(action));
}
if let (Some(thread_id), Some(workspace_root)) =
(string_argument(arguments, "thread_id"), string_argument(arguments, "workspace_root"))
{
let temporary_artifact = crate::utils::agent_temp::session_info(
std::path::Path::new(&workspace_root),
&thread_id,
);
data.insert(
"temporary_artifact_dir".to_string(),
Value::String(temporary_artifact.directory.to_string_lossy().into_owned()),
);
data.insert(
"temporary_artifact_env".to_string(),
Value::String("WINX_TEMP_DIR".into()),
);
}
}
data
}
#[allow(clippy::too_many_lines)] fn error_envelope(
tool: &str,
error: &WinxError,
arguments: Option<&Value>,
text: String,
) -> ToolResultEnvelope {
let classified_error = error.edit_source();
let mut status = ToolResultStatus::Failed;
let mut error_code = "execution_failed".to_string();
let mut retryable = false;
let mut retry_after_ms = None;
let mut next_action = None;
let mut required_reads = Vec::new();
match classified_error {
WinxError::BashStateNotInitialized => {
status = ToolResultStatus::NeedsInitialize;
error_code = "not_initialized".to_string();
retryable = true;
next_action = Some(missing_session_initialize_action(tool, arguments));
}
WinxError::WorkspaceBindingRequired { .. } => {
status = ToolResultStatus::NeedsInitialize;
error_code = "workspace_binding_required".to_string();
retryable = true;
next_action = Some(ToolNextAction {
tool: "Initialize".to_string(),
instruction: "Reuse the exact thread_id/workspace_root pair returned by Initialize for the intended workspace. If that pair is unavailable, initialize the intended workspace once and use its returned pair. The rejected operation did not run; do not reset an existing shell."
.to_string(),
arguments: None,
});
}
WinxError::WorkspaceThreadMismatch { workspace_root, .. } => {
status = ToolResultStatus::Conflict;
error_code = "workspace_thread_mismatch".to_string();
retryable = true;
next_action = Some(initialize_workspace_action(workspace_root));
}
WinxError::WorkspaceBindingMismatch { .. } if tool == "Initialize" => {
status = ToolResultStatus::Conflict;
error_code = "initialize_workspace_already_bound".to_string();
retryable = false;
}
WinxError::WorkspaceBindingMismatch { requested_workspace, .. } => {
status = ToolResultStatus::Conflict;
error_code = "workspace_binding_mismatch".to_string();
retryable = true;
next_action = Some(initialize_workspace_action(requested_workspace));
}
WinxError::WorkspaceChangeRequiresNewSession { .. } => {
status = ToolResultStatus::Conflict;
error_code = "workspace_change_requires_new_session".to_string();
retryable = false;
}
WinxError::CommandAlreadyRunning { .. } => {
status = ToolResultStatus::Running;
error_code = "command_already_running".to_string();
retryable = true;
retry_after_ms = Some(1_000);
let mut action = status_check_action(arguments);
action.instruction = "The requested command did not start. This status_check observes \
the existing foreground execution. For independent work, submit a new command \
with is_background=true and an explicit working directory; the new shell does \
not inherit foreground variables or functions. Do not interrupt or reset an \
unrelated execution."
.to_string();
next_action = Some(action);
}
WinxError::TemporaryArtifactPolicy { temporary_artifact_dir, .. } => {
status = ToolResultStatus::InvalidInput;
error_code = "temporary_artifact_policy".to_string();
retryable = true;
next_action = Some(if tool == "CodeMap" {
ToolNextAction {
tool: "ReadFiles".to_string(),
instruction: "Use the canonical source or a targeted range from an existing \
file. For plain-text search use rg via BashCommand. Do not create \
or map another carrier."
.to_string(),
arguments: None,
}
} else {
ToolNextAction {
tool: tool.to_string(),
instruction: format!(
"Correct the helper path or content using temporary_artifact_dir `{}`; use \
a short descriptive path and do not repeat the rejected call unchanged.",
temporary_artifact_dir.display()
),
arguments: None,
}
});
}
WinxError::TemporaryArtifactBudgetExceeded { temporary_artifact_dir, .. } => {
status = ToolResultStatus::InvalidInput;
error_code = "temporary_artifact_budget_exceeded".to_string();
retryable = true;
next_action = Some(ToolNextAction {
tool: "BashCommand".to_string(),
instruction: format!(
"Inspect and remove obsolete files beneath `{}` using only inspection/cleanup \
commands. Ordinary commands remain blocked until the active session is back \
under budget; do not repeat the rejected command unchanged.",
temporary_artifact_dir.display()
),
arguments: None,
});
}
WinxError::InvalidWaitPolicyForAction { .. } => {
status = ToolResultStatus::InvalidInput;
error_code = "wait_policy_incompatible_with_action".to_string();
retryable = true;
let mut corrected = arguments.cloned();
if let Some(Value::Object(arguments)) = corrected.as_mut() {
arguments
.insert("wait_policy".to_string(), Value::String("return_early".to_string()));
}
next_action = Some(ToolNextAction {
tool: "BashCommand".to_string(),
instruction: "Retry the action with wait_policy=return_early. Reserve \
until_complete for a finite foreground Command action only."
.to_string(),
arguments: corrected,
});
}
WinxError::DerivedCodeMapBudget { .. } => {
status = ToolResultStatus::InvalidInput;
error_code = "derived_code_map_budget_exhausted".to_string();
retryable = true;
next_action = Some(ToolNextAction {
tool: "ReadFiles".to_string(),
instruction: "Stop creating or mapping temporary carriers. Reuse prior results, \
run CodeMap on canonical source, or read only the exact canonical \
ranges needed; use rg via BashCommand for plain-text search."
.to_string(),
arguments: None,
});
}
WinxError::PathSecurityError { .. } | WinxError::CommandNotAllowed(_) => {
status = ToolResultStatus::Denied;
error_code = "operation_denied".to_string();
}
WinxError::WorkspacePathError(_) | WinxError::BackgroundSessionNotFound(_) => {
status = ToolResultStatus::NotFound;
error_code = "target_not_found".to_string();
}
WinxError::ThreadIdMismatch(_) => {
status = ToolResultStatus::Conflict;
error_code = "thread_id_mismatch".to_string();
}
WinxError::ParameterValidationError { .. }
| WinxError::MissingParameterError { .. }
| WinxError::NullValueError { .. }
| WinxError::ArgumentParseError(_)
| WinxError::JsonParseError(_)
| WinxError::DeserializationError(_)
| WinxError::InvalidInput(_)
| WinxError::ParseError(_)
| WinxError::SearchReplaceSyntaxError(_)
| WinxError::SearchReplaceSyntaxErrorDetailed { .. }
| WinxError::EmptyInteractiveInput { .. }
| WinxError::InteractiveTargetNotRunning(_)
| WinxError::NoActiveCommand(_) => {
status = ToolResultStatus::InvalidInput;
error_code = "invalid_tool_input".to_string();
retryable = true;
}
WinxError::RecoverableSuggestionError { .. } => {
retryable = true;
error_code = "recoverable_failure".to_string();
}
WinxError::CommandTimeout { timeout_seconds, .. } => {
error_code = "command_timeout".to_string();
retryable = true;
retry_after_ms = Some(timeout_seconds.saturating_mul(1_000));
}
_ => {}
}
if let Some(plan) = super::recovery::classify(tool, error, arguments) {
status = plan.status;
error_code = plan.error_code.to_string();
retryable = plan.retryable;
retry_after_ms = plan.retry_after_ms;
next_action = plan.next_action;
required_reads = plan.required_reads;
}
let mut data = Map::new();
if let Some(thread_id) = string_argument(arguments, "thread_id") {
data.insert("thread_id".to_string(), Value::String(thread_id));
}
if let Some(workspace_root) = string_argument(arguments, "workspace_root") {
data.insert("workspace_root".to_string(), Value::String(workspace_root));
}
if let WinxError::WorkspaceBindingMismatch { requested_workspace, bound_workspace, .. } =
classified_error
{
data.insert(
"bound_workspace".to_string(),
Value::String(bound_workspace.to_string_lossy().into_owned()),
);
data.insert(
"requested_workspace".to_string(),
Value::String(requested_workspace.to_string_lossy().into_owned()),
);
if tool == "Initialize" {
data.insert("continue_with_bound_session".to_string(), Value::Bool(true));
data.insert("external_targets_require_reinitialize".to_string(), Value::Bool(false));
}
}
if tool != "Initialize"
&& matches!(
classified_error,
WinxError::WorkspaceBindingRequired { .. }
| WinxError::WorkspaceThreadMismatch { .. }
| WinxError::WorkspaceBindingMismatch { .. }
)
{
data.insert("operation_executed".to_string(), Value::Bool(false));
data.insert("binding_changed".to_string(), Value::Bool(false));
data.insert(
"recovery_strategy".to_string(),
Value::String("reuse_valid_pair_or_initialize".to_string()),
);
}
if let WinxError::TemporaryArtifactPolicy { path, temporary_artifact_dir, .. } =
classified_error
{
data.insert(
"rejected_path".to_string(),
Value::String(path.to_string_lossy().into_owned()),
);
data.insert(
"temporary_artifact_dir".to_string(),
Value::String(temporary_artifact_dir.to_string_lossy().into_owned()),
);
}
if let WinxError::TemporaryArtifactBudgetExceeded {
temporary_artifact_dir,
total_bytes,
max_total_bytes,
session_bytes,
max_session_bytes,
session_files,
max_session_files,
largest_file_bytes,
max_file_bytes,
} = classified_error
{
data.insert(
"temporary_artifact_dir".to_string(),
Value::String(temporary_artifact_dir.to_string_lossy().into_owned()),
);
data.insert("total_bytes".to_string(), json!(total_bytes));
data.insert("max_total_bytes".to_string(), json!(max_total_bytes));
data.insert("session_bytes".to_string(), json!(session_bytes));
data.insert("max_session_bytes".to_string(), json!(max_session_bytes));
data.insert("session_files".to_string(), json!(session_files));
data.insert("max_session_files".to_string(), json!(max_session_files));
data.insert("largest_file_bytes".to_string(), json!(largest_file_bytes));
data.insert("max_file_bytes".to_string(), json!(max_file_bytes));
data.insert("temporary_artifact_cleanup_required".to_string(), Value::Bool(true));
}
if let WinxError::InvalidWaitPolicyForAction { wait_policy, action } = classified_error {
data.insert("wait_policy".to_string(), Value::String(wait_policy.clone()));
data.insert("action".to_string(), Value::String(action.clone()));
}
if let WinxError::DerivedCodeMapBudget {
path,
temporary_artifact_dir,
calls_used,
calls_limit,
unique_files_used,
unique_files_limit,
..
} = classified_error
{
data.insert(
"rejected_path".to_string(),
Value::String(path.to_string_lossy().into_owned()),
);
data.insert(
"temporary_artifact_dir".to_string(),
Value::String(temporary_artifact_dir.to_string_lossy().into_owned()),
);
data.insert("calls_used".to_string(), json!(calls_used));
data.insert("calls_limit".to_string(), json!(calls_limit));
data.insert("unique_files_used".to_string(), json!(unique_files_used));
data.insert("unique_files_limit".to_string(), json!(unique_files_limit));
}
if error.is_search_match_conflict() {
data.insert("edit_applied".to_string(), Value::Bool(false));
data.insert("fresh_read_required".to_string(), Value::Bool(true));
data.insert("read_permit_invalidated".to_string(), Value::Bool(true));
if tool == "EditFiles" {
data.insert(
"recommended_edit_mode".to_string(),
Value::String("line_patch".to_string()),
);
data.insert("ordinary_shell_edit_fallback".to_string(), Value::Bool(false));
}
}
if matches!(classified_error, WinxError::CommandAlreadyRunning { .. }) {
data.insert("command_started".to_string(), Value::Bool(false));
data.insert("shell_busy".to_string(), Value::Bool(true));
}
ToolResultEnvelope {
status,
tool: tool.to_string(),
output: Some(text.clone()),
message: text,
error_code: Some(error_code),
retryable,
retry_same_call: false,
retry_after_ms,
next_action,
required_reads,
data: (!data.is_empty()).then_some(Value::Object(data)),
}
}
fn status_check_action(arguments: Option<&Value>) -> ToolNextAction {
let mut value = json!({
"action_json": {
"type": "status_check",
"status_check": true
}
});
copy_session_binding(arguments, &mut value);
ToolNextAction {
tool: "BashCommand".to_string(),
instruction: "Check the running command instead of submitting the original command again."
.to_string(),
arguments: Some(value),
}
}
fn initialize_workspace_action(workspace_root: &std::path::Path) -> ToolNextAction {
ToolNextAction {
tool: "Initialize".to_string(),
instruction: "Reuse the exact thread_id/workspace_root pair already returned by Initialize for the intended workspace. If that pair is unavailable, call Initialize once with these arguments and use the pair it returns. Keep the existing binding; do not carry the rejected thread_id into this fallback or reset its shell."
.to_string(),
arguments: Some(json!({
"type": "first_call",
"any_workspace_path": workspace_root.to_string_lossy(),
"initial_files_to_read": [],
"mode_name": "wcgw",
"thread_id": ""
})),
}
}
fn missing_session_initialize_action(tool: &str, arguments: Option<&Value>) -> ToolNextAction {
let corrected_arguments = if tool == "Initialize" {
arguments.and_then(Value::as_object).map(|arguments| {
let mut corrected = arguments.clone();
corrected.insert("type".to_string(), Value::String("first_call".to_string()));
Value::Object(corrected)
})
} else {
string_argument(arguments, "workspace_root").map(|workspace_root| {
let mut corrected = json!({
"type": "first_call",
"any_workspace_path": workspace_root,
"initial_files_to_read": [],
"mode_name": "wcgw"
});
if let Some(thread_id) = string_argument(arguments, "thread_id") {
corrected["thread_id"] = Value::String(thread_id);
}
corrected
})
};
ToolNextAction {
tool: "Initialize".to_string(),
instruction: "Call this corrected first_call exactly once for the intended workspace, preserve the returned thread_id/workspace_root pair, then retry the interrupted operation. Do not retry the prior reset, mode change, or stateful tool first."
.to_string(),
arguments: corrected_arguments,
}
}
fn copy_session_binding(arguments: Option<&Value>, target: &mut Value) {
for key in ["thread_id", "workspace_root"] {
if let Some(value) = string_argument(arguments, key) {
target[key] = Value::String(value);
}
}
}
fn bash_action(arguments: Option<&Value>) -> Option<String> {
let arguments = arguments?.as_object()?;
let fallback = Value::Object(arguments.clone());
let action = arguments.get("action_json").unwrap_or(&fallback);
match action {
Value::Object(action) => {
action.get("type").and_then(Value::as_str).map(ToString::to_string).or_else(|| {
[
"command",
"status_check",
"send_text",
"send_specials",
"send_ascii",
"screen",
"wait_for_turn",
]
.into_iter()
.find(|kind| action.contains_key(*kind))
.map(ToString::to_string)
})
}
Value::String(_) => Some("command".to_string()),
_ => None,
}
}
fn string_argument(arguments: Option<&Value>, key: &str) -> Option<String> {
arguments?
.as_object()?
.get(key)?
.as_str()
.filter(|value| !value.is_empty())
.map(ToString::to_string)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
fn exited_bash_result(output: &str) -> BashCommandRuntimeResult {
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: output.to_string(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(0),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: None,
command_generation: Some(1),
execution_token: None,
generation_bound_actions: false,
dropped_output_file: None,
output_truncated: false,
action_context: None,
workflow: None,
}
}
#[test]
fn bash_success_mirrors_rendered_output_for_structured_only_clients() {
let result = bash_success_result(
Some(&json!({
"thread_id": "thread",
"workspace_root": "/workspace",
"action_json": {"type": "command", "command": "printf WINX_OK"}
})),
exited_bash_result("WINX_OK"),
false,
)
.expect("typed BashCommand result");
let text = result.content[0].as_text().expect("text block").text.clone();
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["status"], "completed");
assert_eq!(structured["output"], "WINX_OK");
assert_eq!(structured["output"], text);
assert_eq!(structured["data"]["output_bytes"], 7);
}
#[test]
fn empty_command_output_is_mirrored_as_an_empty_string() {
let result = bash_success_result(None, exited_bash_result(""), false)
.expect("typed BashCommand result");
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["output"], "");
}
#[test]
fn decorate_success_mirrors_joined_text_and_omits_output_for_images() {
let mut result = CallToolResult::success(vec![
ContentBlock::text("first"),
ContentBlock::text("second"),
]);
decorate_success("ReadFiles", Some(&json!({"thread_id": "thread"})), &mut result);
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["output"], "first\nsecond");
assert_eq!(structured["data"]["output_bytes"], "first\nsecond".len());
let mut image = CallToolResult::success(vec![ContentBlock::image("AAAA", "image/png")]);
image.structured_content = Some(json!({"content_fingerprint": "abc"}));
decorate_success("ReadImage", None, &mut image);
let structured = image.structured_content.expect("structured image result");
assert_eq!(structured["status"], "completed");
assert!(structured.get("output").is_none(), "{structured}");
assert_eq!(structured["data"]["result"]["content_fingerprint"], "abc");
}
#[test]
fn initialize_reports_the_bound_session_identity_over_argument_echoes() {
let mut result = CallToolResult::success(vec![ContentBlock::text(
"Use thread_id=ws_bound for all winx tool calls.",
)]);
result.structured_content = Some(json!({
"thread_id": "ws_bound",
"workspace_root": "/canonical/workspace",
"initialize_transition": "created"
}));
decorate_success(
"Initialize",
Some(&json!({
"thread_id": " client-thread ",
"workspace_root": "/requested/workspace",
"any_workspace_path": "/requested/workspace"
})),
&mut result,
);
let structured = result.structured_content.expect("structured Initialize result");
assert_eq!(structured["data"]["thread_id"], "ws_bound");
assert_eq!(structured["data"]["workspace_root"], "/canonical/workspace");
assert_eq!(structured["data"]["any_workspace_path"], "/requested/workspace");
assert_eq!(structured["data"]["initialize_transition"], "created");
assert_eq!(structured["output"], "Use thread_id=ws_bound for all winx tool calls.");
}
#[test]
fn tool_failure_mirrors_its_text_into_output() {
let result = tool_failure(
"BashCommand",
&WinxError::BashStateNotInitialized,
Some(&json!({"thread_id": "thread"})),
)
.expect("tool-level error");
let text = result.content[0].as_text().expect("text block").text.clone();
let structured = result.structured_content.expect("structured error");
assert_eq!(structured["status"], "needs_initialize");
assert_eq!(structured["output"], text);
assert_eq!(structured["message"], text);
}
#[test]
fn code_map_omits_output_only_when_its_symbols_are_structured() {
let mut symbols =
CallToolResult::success(vec![ContentBlock::text("src/lib.rs\n 1 fn main")]);
symbols.structured_content = Some(json!({
"mode": "file",
"files": [{"file": "src/lib.rs", "symbols": [{"name": "main", "kind": "fn", "line": 1}]}],
"payload_bytes": 1
}));
decorate_success("CodeMap", None, &mut symbols);
let structured = symbols.structured_content.as_ref().expect("structured CodeMap result");
assert_eq!(structured["status"], "completed");
assert!(structured.get("output").is_none(), "{structured}");
assert_eq!(structured["mode"], "file");
finalize_tool_result(&mut symbols);
assert!(symbols.structured_content.expect("structured")["output"].is_null());
let diagnostic = "Could not outline src/broken.rs: permission denied";
let mut unreadable = CallToolResult::success(vec![ContentBlock::text(diagnostic)]);
unreadable.structured_content =
Some(json!({"mode": "file", "files": [], "payload_bytes": 1}));
decorate_success("CodeMap", None, &mut unreadable);
finalize_tool_result(&mut unreadable);
let structured = unreadable.structured_content.expect("structured CodeMap diagnostic");
assert_eq!(structured["output"], diagnostic, "{structured}");
let mut failure = tool_failure("CodeMap", &WinxError::BashStateNotInitialized, None)
.expect("tool-level error");
finalize_tool_result(&mut failure);
let structured = failure.structured_content.expect("structured CodeMap error");
assert_eq!(structured["output"], structured["message"], "{structured}");
}
#[test]
fn finalize_redacts_text_and_mirror_identically() {
let token = format!("ghp_{}", "a".repeat(40));
let mut result =
CallToolResult::success(vec![ContentBlock::text(format!("token={token} ok"))]);
result.structured_content = Some(json!({
"status": "completed",
"tool": "BashCommand",
"output": format!("token={token} ok"),
"data": {"note": format!("saw {token}")}
}));
finalize_tool_result(&mut result);
let text = result_text(&result);
assert_eq!(text, "token=[REDACTED:github-token] ok");
let structured = result.structured_content.expect("structured result");
assert_eq!(structured["output"], text);
assert_eq!(structured["data"]["note"], "saw [REDACTED:github-token]");
assert!(!serde_json::to_string(&structured).expect("json").contains(&token));
}
#[test]
fn mirror_is_never_less_redacted_than_the_text_blocks() {
let mut result = CallToolResult::success(vec![
ContentBlock::text("-----BEGIN PRIVATE KEY-----\nsecret-head"),
ContentBlock::text("secret-tail\n-----END PRIVATE KEY-----"),
]);
result.structured_content = Some(json!({"status": "completed", "tool": "BashCommand"}));
finalize_tool_result(&mut result);
let output = result.structured_content.expect("structured")["output"]
.as_str()
.expect("mirrored output")
.to_string();
assert!(!output.contains("secret-head"), "{output}");
assert!(output.contains("[REDACTED:"), "{output}");
}
#[test]
fn mirror_text_output_tracks_content_rewrites() {
let mut result = CallToolResult::success(vec![ContentBlock::text("before")]);
result.structured_content =
Some(json!({"status": "completed", "tool": "ReadFiles", "output": "before"}));
result.content = vec![ContentBlock::text("after"), ContentBlock::text("more")];
mirror_text_output(&mut result);
assert_eq!(
result.structured_content.as_ref().expect("structured")["output"],
"after\nmore"
);
result.content = vec![ContentBlock::image("AAAA", "image/png")];
mirror_text_output(&mut result);
assert!(result.structured_content.as_ref().expect("structured").get("output").is_none());
let mut plain = CallToolResult::success(vec![ContentBlock::text("no envelope")]);
mirror_text_output(&mut plain);
assert!(plain.structured_content.is_none());
}
#[test]
fn code_map_navigation_payload_is_not_duplicated_inside_data_result() {
let navigation = json!({
"mode": "repo",
"files_shown": 1,
"files": [{"file": "src/lib.rs", "symbols": []}],
"truncated": false,
"files_scanned": 1,
"payload_bytes": 128
});
let mut result = CallToolResult::success(vec![ContentBlock::text("src/lib.rs\n")]);
result.structured_content = Some(navigation);
decorate_success("CodeMap", None, &mut result);
let structured = result.structured_content.expect("structured CodeMap result");
assert_eq!(structured["files"][0]["file"], "src/lib.rs");
assert!(structured["data"].get("result").is_none(), "{structured}");
}
#[test]
fn initialize_metadata_is_flattened_into_the_shared_data_envelope() {
let mut result = CallToolResult::success(vec![ContentBlock::text("attached")]);
result.structured_content = Some(json!({
"initialize_transition": "attached_existing",
"initialize_reused": true,
"initialize_response_mode": "compact",
"context_bytes": 0,
"workspace_root": "/workspace"
}));
decorate_success(
"Initialize",
Some(&json!({"thread_id":"thread","mode_name":"wcgw"})),
&mut result,
);
let structured = result.structured_content.expect("structured Initialize result");
assert_eq!(structured["status"], "completed");
assert_eq!(structured["data"]["initialize_transition"], "attached_existing");
assert_eq!(structured["data"]["initialize_reused"], true);
assert_eq!(structured["data"]["initialize_response_mode"], "compact");
assert_eq!(structured["data"]["workspace_root"], "/workspace");
assert!(structured["data"].get("result").is_none());
}
#[test]
fn missing_session_recovery_supplies_an_exact_first_call() {
let initialize = tool_failure(
"Initialize",
&WinxError::BashStateNotInitialized,
Some(&json!({
"type": "reset_shell",
"any_workspace_path": "/workspace",
"initial_files_to_read": ["README.md"],
"mode_name": "code_writer",
"thread_id": "thread",
"code_writer_config": {"allowed_commands": ["cargo"], "allowed_globs": ["**/*.rs"]}
})),
)
.expect("Initialize recovery result");
let structured = initialize.structured_content.expect("structured Initialize recovery");
let arguments = &structured["nextAction"]["arguments"];
assert_eq!(structured["status"], "needs_initialize");
assert_eq!(structured["errorCode"], "not_initialized");
assert_eq!(arguments["type"], "first_call");
assert_eq!(arguments["any_workspace_path"], "/workspace");
assert_eq!(arguments["thread_id"], "thread");
assert_eq!(arguments["mode_name"], "code_writer");
assert_eq!(arguments["initial_files_to_read"], json!(["README.md"]));
let stateful = tool_failure(
"ReadFiles",
&WinxError::BashStateNotInitialized,
Some(&json!({
"file_paths": ["/workspace/README.md"],
"thread_id": "thread",
"workspace_root": "/workspace"
})),
)
.expect("stateful recovery result");
let structured = stateful.structured_content.expect("structured stateful recovery");
let arguments = &structured["nextAction"]["arguments"];
assert_eq!(arguments["type"], "first_call");
assert_eq!(arguments["any_workspace_path"], "/workspace");
assert_eq!(arguments["thread_id"], "thread");
assert_eq!(arguments["mode_name"], "wcgw");
}
#[test]
fn unread_file_error_provides_exact_read_call() {
let error = WinxError::FileReadRequired {
path: "/workspace/README.md".into(),
reason: crate::errors::ReadRequirement::InsufficientCoverage,
ranges: vec!["20-40".into(), "90-".into()],
message: "Read more of the file before overwriting. Unread line ranges: 20-40, 90-"
.to_string(),
};
let result = tool_failure(
"FileWriteOrEdit",
&error,
Some(&json!({
"thread_id":"thread",
"workspace_root":"/workspace",
"file_path":"/workspace/README.md"
})),
)
.expect("tool-level error");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured error");
assert_eq!(structured["status"], "needs_read");
assert_eq!(structured["retrySameCall"], false);
assert_eq!(
structured["nextAction"]["arguments"]["file_paths"],
json!(["/workspace/README.md:20-40", "/workspace/README.md:90-"])
);
assert_eq!(structured["nextAction"]["arguments"]["workspace_root"], "/workspace");
}
#[test]
fn temporary_artifact_policy_returns_a_correctable_structured_result() {
let error = WinxError::TemporaryArtifactPolicy {
path: "/workspace/.winx_tmp/carrier.py".into(),
temporary_artifact_dir: "/workspace/.winx/tmp/session-deadbeefdeadbeef".into(),
message: "use the managed session directory".to_string(),
};
let result = tool_failure(
"FileWriteOrEdit",
&error,
Some(&json!({
"thread_id":"thread",
"workspace_root":"/workspace",
"file_path":"/workspace/.winx_tmp/carrier.py"
})),
)
.expect("tool result");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured policy result");
assert_eq!(structured["status"], "invalid_input");
assert_eq!(structured["errorCode"], "temporary_artifact_policy");
assert_eq!(structured["retryable"], true);
assert_eq!(structured["retrySameCall"], false);
assert_eq!(
structured["data"]["temporary_artifact_dir"],
"/workspace/.winx/tmp/session-deadbeefdeadbeef"
);
assert_eq!(structured["nextAction"]["tool"], "FileWriteOrEdit");
}
#[test]
fn post_bash_temp_overflow_preserves_execution_and_requires_cleanup() {
let mut result = CallToolResult::success(vec![ContentBlock::text("command output")]);
result.structured_content = Some(json!({
"status": "completed",
"tool": "BashCommand",
"message": "BashCommand completed.",
"retryable": false,
"retrySameCall": false,
"requiredReads": [],
"data": {}
}));
let usage = crate::utils::agent_temp::TemporaryArtifactUsage {
deletion_deferred: "runtime_protection_unknown",
temporary_artifact_dir: "/workspace/.winx/tmp/session-deadbeefdeadbeef".into(),
total_bytes: 129,
max_total_bytes: 1_024,
session_bytes: 129,
max_session_bytes: 128,
session_files: 129,
max_session_files: 128,
largest_file_bytes: 2,
max_file_bytes: 64,
stale_pruned_files: 0,
stale_pruned_bytes: 0,
over_budget: true,
};
attach_temporary_artifact_usage(
&mut result,
Some(&json!({"thread_id":"thread","workspace_root":"/workspace"})),
&usage,
);
assert_ne!(result.is_error, Some(true));
assert_eq!(result.content.len(), 2);
let structured = result.structured_content.expect("structured Bash result");
assert_eq!(structured["status"], "completed");
assert_eq!(structured["data"]["temporary_artifact_cleanup_required"], true);
assert_eq!(structured["data"]["temporary_artifact_budget"]["session_files"], 129);
assert_eq!(structured["nextAction"]["tool"], "BashCommand");
assert!(structured["nextAction"].get("arguments").is_none());
}
#[test]
fn running_temp_overflow_supplies_a_concrete_interrupt_action() {
let mut result = CallToolResult::success(vec![ContentBlock::text("still running")]);
result.structured_content = Some(json!({
"status": "running",
"tool": "BashCommand",
"message": "BashCommand is still running.",
"retryable": false,
"retrySameCall": false,
"requiredReads": [],
"data": {"bg_command_id": "bg-7"}
}));
let usage = crate::utils::agent_temp::TemporaryArtifactUsage {
deletion_deferred: "runtime_protection_unknown",
temporary_artifact_dir: "/workspace/.winx/tmp/session-deadbeefdeadbeef".into(),
total_bytes: 129,
max_total_bytes: 1_024,
session_bytes: 129,
max_session_bytes: 128,
session_files: 129,
max_session_files: 128,
largest_file_bytes: 2,
max_file_bytes: 64,
stale_pruned_files: 0,
stale_pruned_bytes: 0,
over_budget: true,
};
attach_temporary_artifact_usage(
&mut result,
Some(&json!({"thread_id":"thread","workspace_root":"/workspace"})),
&usage,
);
let structured = result.structured_content.expect("structured Bash result");
let arguments = &structured["nextAction"]["arguments"];
assert_eq!(structured["status"], "running");
assert_eq!(arguments["action_json"]["type"], "send_specials");
assert_eq!(arguments["action_json"]["send_specials"][0], "Ctrl-c");
assert_eq!(arguments["action_json"]["bg_command_id"], "bg-7");
assert_eq!(arguments["wait_policy"], "return_early");
assert_eq!(arguments["thread_id"], "thread");
assert_eq!(arguments["workspace_root"], "/workspace");
}
#[test]
fn incompatible_wait_policy_returns_corrected_arguments() {
let error = WinxError::InvalidWaitPolicyForAction {
wait_policy: "until_complete".to_string(),
action: "status_check".to_string(),
};
let result = tool_failure(
"BashCommand",
&error,
Some(&json!({
"thread_id":"thread",
"workspace_root":"/workspace",
"wait_policy":"until_complete",
"action_json":{"type":"status_check","status_check":true}
})),
)
.expect("recoverable tool result");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured wait-policy result");
assert_eq!(structured["status"], "invalid_input");
assert_eq!(structured["errorCode"], "wait_policy_incompatible_with_action");
assert_eq!(structured["nextAction"]["arguments"]["wait_policy"], "return_early");
assert_eq!(structured["data"]["action"], "status_check");
}
#[test]
fn derived_code_map_budget_redirects_to_canonical_source_tools() {
let error = WinxError::DerivedCodeMapBudget {
path: "/workspace/.winx/tmp/session-deadbeefdeadbeef/carrier.py".into(),
temporary_artifact_dir: "/workspace/.winx/tmp/session-deadbeefdeadbeef".into(),
calls_used: 64,
calls_limit: 64,
unique_files_used: 24,
unique_files_limit: 24,
message: "budget exhausted".to_string(),
};
let result = tool_failure(
"CodeMap",
&error,
Some(&json!({
"operation": "outline",
"path": "/workspace/.winx/tmp/session-deadbeefdeadbeef/carrier.py",
"thread_id": "thread",
"workspace_root": "/workspace"
})),
)
.expect("tool-level budget result");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured budget error");
assert_eq!(structured["status"], "invalid_input");
assert_eq!(structured["errorCode"], "derived_code_map_budget_exhausted");
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["nextAction"]["tool"], "ReadFiles");
assert_eq!(structured["data"]["calls_used"], 64);
assert_eq!(structured["data"]["unique_files_limit"], 24);
}
#[test]
fn running_command_success_points_to_status_check() {
let result = bash_success_result(
Some(&json!({
"thread_id":"thread",
"workspace_root":"/workspace",
"action_json":{"type":"command","command":"test"}
})),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: "build output with arbitrary text".to_string(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Running,
background_id: None,
running_for_seconds: Some(1),
exit_code: None,
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: Some("build output with arbitrary text".to_string()),
command_generation: Some(1),
execution_token: Some(crate::runtime::ShellExecutionToken {
guardian_epoch: "guardian".to_string(),
session_epoch: "session".to_string(),
generation: 1,
}),
generation_bound_actions: true,
dropped_output_file: None,
output_truncated: false,
workflow: None,
action_context: None,
},
false,
)
.expect("typed BashCommand result");
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["status"], "running");
assert_eq!(structured["nextAction"]["tool"], "BashCommand");
assert_eq!(structured["nextAction"]["arguments"]["action_json"]["type"], "status_check");
assert_eq!(structured["nextAction"]["arguments"]["workspace_root"], "/workspace");
assert_eq!(structured["data"]["command_generation"], 1);
assert_eq!(structured["data"]["execution_id"], "guardian:session:1");
}
#[test]
fn workspace_thread_mismatch_reports_unchanged_binding_and_safe_fallback() {
let error = WinxError::WorkspaceThreadMismatch {
thread_id: "rejected-thread".to_string(),
workspace_root: "/intended".into(),
};
let result = tool_failure(
"ReadFiles",
&error,
Some(&json!({
"thread_id": "rejected-thread",
"workspace_root": "/intended",
"file_paths": ["/intended/file.txt"]
})),
)
.expect("tool-level coherence error");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured coherence error");
assert_eq!(structured["status"], "conflict");
assert_eq!(structured["errorCode"], "workspace_thread_mismatch");
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["data"]["operation_executed"], false);
assert_eq!(structured["data"]["binding_changed"], false);
assert_eq!(structured["data"]["recovery_strategy"], "reuse_valid_pair_or_initialize");
assert!(structured["data"].get("bound_workspace").is_none());
let next = &structured["nextAction"];
assert_eq!(next["tool"], "Initialize");
assert_eq!(next["arguments"]["type"], "first_call");
assert_eq!(next["arguments"]["any_workspace_path"], "/intended");
assert_eq!(next["arguments"]["thread_id"], "");
assert!(!next["arguments"].to_string().contains("rejected-thread"));
}
#[test]
fn workspace_mismatch_has_a_safe_structured_recovery() {
let error = WinxError::WorkspaceBindingMismatch {
thread_id: "wrong-thread".to_string(),
requested_workspace: "/intended".into(),
bound_workspace: "/other".into(),
};
let result = tool_failure(
"BashCommand",
&error,
Some(&json!({
"thread_id": "wrong-thread",
"workspace_root": "/intended"
})),
)
.expect("tool-level coherence error");
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured coherence error");
assert_eq!(structured["status"], "conflict");
assert_eq!(structured["errorCode"], "workspace_binding_mismatch");
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["data"]["bound_workspace"], "/other");
assert_eq!(structured["data"]["operation_executed"], false);
assert_eq!(structured["data"]["binding_changed"], false);
assert_eq!(structured["data"]["recovery_strategy"], "reuse_valid_pair_or_initialize");
assert_eq!(structured["nextAction"]["tool"], "Initialize");
let arguments = &structured["nextAction"]["arguments"];
assert_eq!(arguments["type"], "first_call");
assert_eq!(arguments["any_workspace_path"], "/intended");
assert_eq!(arguments["thread_id"], "");
assert!(!arguments.to_string().contains("wrong-thread"));
assert!(!arguments.to_string().contains("/other"));
}
#[test]
fn workspace_binding_required_reports_operation_not_executed() {
let result = tool_failure(
"ReadFiles",
&WinxError::WorkspaceBindingRequired { thread_id: "known-thread".to_string() },
Some(&json!({"thread_id": "known-thread"})),
)
.expect("tool-level missing binding error");
let structured = result.structured_content.expect("structured binding error");
assert_eq!(structured["status"], "needs_initialize");
assert_eq!(structured["errorCode"], "workspace_binding_required");
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["data"]["operation_executed"], false);
assert_eq!(structured["data"]["binding_changed"], false);
assert_eq!(structured["data"]["recovery_strategy"], "reuse_valid_pair_or_initialize");
assert_eq!(structured["nextAction"]["tool"], "Initialize");
}
#[test]
fn unified_search_conflict_points_to_line_patch_without_shell_fallback() {
let error = WinxError::IndexedEditError {
index: 1,
path: "/workspace/src/lib.rs".into(),
mode: "search_replace".to_string(),
source: Box::new(WinxError::SearchBlockNotFound("stale block".to_string())),
};
let arguments = json!({
"files": [{
"file_path": "/workspace/src/lib.rs",
"mode": "search_replace",
"content": "stale block"
}],
"thread_id": "thread",
"workspace_root": "/workspace"
});
let result =
tool_failure("EditFiles", &error, Some(&arguments)).expect("tool-level edit conflict");
assert_eq!(result.is_error, Some(true));
let text = result_text(&result);
assert!(text.contains("mode=line_patch"), "{text}");
assert!(text.contains("Do not retry SEARCH"), "{text}");
let structured = result.structured_content.expect("structured edit conflict");
assert_eq!(structured["status"], "conflict");
assert_eq!(structured["errorCode"], "search_block_not_found");
assert_eq!(structured["nextAction"]["tool"], "ReadFiles");
assert!(structured["nextAction"]["instruction"]
.as_str()
.is_some_and(|instruction| instruction.contains("mode=line_patch")));
assert_eq!(structured["data"]["recommended_edit_mode"], "line_patch");
assert_eq!(structured["data"]["ordinary_shell_edit_fallback"], false);
assert_eq!(structured["data"]["edit_applied"], false);
}
#[test]
fn repeated_initialize_for_another_workspace_is_terminal() {
let error = WinxError::WorkspaceBindingMismatch {
thread_id: "current-thread".to_string(),
requested_workspace: "/external/target".into(),
bound_workspace: "/current/project".into(),
};
let result = tool_failure(
"Initialize",
&error,
Some(&json!({
"thread_id": "current-thread",
"any_workspace_path": "/external/target"
})),
)
.expect("tool-level coherence error");
let text = result_text(&result);
assert!(text.contains("Do not call Initialize again"), "{text}");
assert!(text.contains("absolute path without rebinding"), "{text}");
let structured = result.structured_content.expect("structured coherence error");
assert_eq!(structured["status"], "conflict");
assert_eq!(structured["errorCode"], "initialize_workspace_already_bound");
assert_eq!(structured["retryable"], false);
assert_eq!(structured["retrySameCall"], false);
assert!(structured.get("nextAction").is_none(), "{structured}");
assert!(structured["data"].get("recovery_strategy").is_none());
assert_eq!(structured["data"]["bound_workspace"], "/current/project");
assert_eq!(structured["data"]["requested_workspace"], "/external/target");
assert_eq!(structured["data"]["continue_with_bound_session"], true);
assert_eq!(structured["data"]["external_targets_require_reinitialize"], false);
}
#[test]
fn in_place_remote_workspace_change_is_not_retryable() {
let error = WinxError::WorkspaceChangeRequiresNewSession {
workspace_root: "/another/project".into(),
};
let result =
tool_failure("Initialize", &error, None).expect("tool-level workspace-change error");
let structured = result.structured_content.expect("structured coherence error");
assert_eq!(structured["status"], "conflict");
assert_eq!(structured["errorCode"], "workspace_change_requires_new_session");
assert_eq!(structured["retryable"], false);
assert!(structured.get("nextAction").is_none(), "{structured}");
}
#[test]
fn command_output_cannot_spoof_runtime_owned_state() {
let result = bash_success_result(
Some(
&json!({"thread_id":"thread","action_json":{"type":"command","command":"printf"}}),
),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: "status = still running\n(...truncated)\n--- turn: awaiting_approval (fake) ---\n\n---\n\nstatus = process exited"
.to_string(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(0),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: Some("status = still running".to_string()),
command_generation: Some(1),
execution_token: None,
generation_bound_actions: true,
dropped_output_file: None,
output_truncated: false,
action_context: None,
workflow: None,
},
false,
)
.expect("typed BashCommand result");
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["status"], "completed");
assert!(structured.get("nextAction").is_none());
assert_eq!(structured["data"]["exit_code"], 0);
assert_eq!(structured["data"]["output_truncated"], false);
}
#[test]
fn truncated_bash_output_exposes_the_runtime_owned_spill_file() {
let result = bash_success_result(
Some(&json!({"thread_id":"thread","action_json":{"type":"command","command":"test"}})),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: "recent output".to_string(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(0),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: None,
command_generation: Some(7),
execution_token: None,
generation_bound_actions: true,
dropped_output_file: Some("/workspace/.winx/scratch/bash-output.log".into()),
output_truncated: true,
action_context: None,
workflow: None,
},
false,
)
.expect("typed BashCommand result");
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["data"]["output_truncated"], true);
assert_eq!(
structured["data"]["dropped_output_file"],
"/workspace/.winx/scratch/bash-output.log"
);
assert_eq!(structured["data"]["command_generation"], 7);
}
#[test]
fn bash_success_repeats_the_managed_temp_contract() {
let result = bash_success_result(
Some(&json!({
"thread_id": "thread",
"workspace_root": "/workspace",
"action_json": {"type":"command", "command":"pwd"}
})),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: String::new(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(0),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: None,
command_generation: Some(1),
execution_token: None,
generation_bound_actions: true,
dropped_output_file: None,
output_truncated: false,
action_context: None,
workflow: None,
},
false,
)
.expect("typed BashCommand result");
let structured = result.structured_content.expect("structured success");
assert_eq!(structured["data"]["temporary_artifact_env"], "WINX_TEMP_DIR");
assert!(
structured["data"]["temporary_artifact_dir"].as_str().is_some_and(|path| {
path.replace('\\', "/").starts_with("/workspace/.winx/tmp/session-")
}),
"{structured}"
);
}
fn exited_verification(exit_code: i32) -> CallToolResult {
bash_success_result(
Some(&json!({"thread_id":"thread","action_json":{"type":"command"}})),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: format!("verification output {exit_code}"),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(exit_code),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: None,
command_generation: Some(1),
execution_token: None,
generation_bound_actions: true,
dropped_output_file: None,
output_truncated: false,
action_context: None,
workflow: None,
},
false,
)
.expect("typed verification result")
}
fn verification_recovery() -> VerificationRecovery {
VerificationRecovery {
id: "verify_receipt".to_string(),
tool: "VerifyEdit",
arguments: json!({
"verification_id": "verify_receipt",
"command": "cargo test",
"thread_id": "thread",
"workspace_root": "/workspace"
}),
}
}
#[test]
fn edit_verification_surfaces_success_without_losing_edit_metadata() {
let arguments = json!({"thread_id":"thread","file_path":"/workspace/lib.rs"});
let mut result = edit_verification_result(
"FileWriteOrEdit",
Some(&arguments),
"Successfully edited /workspace/lib.rs",
exited_verification(0),
&verification_recovery(),
);
decorate_success("FileWriteOrEdit", Some(&arguments), &mut result);
assert_ne!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured verification");
assert_eq!(structured["status"], "completed");
assert_eq!(structured["data"]["edit_applied"], true);
assert_eq!(structured["data"]["verification_exit_code"], 0);
assert!(
structured["data"].get("file_path").is_none(),
"edit paths are attached later from PreparedEditContext, never raw wire arguments"
);
}
#[test]
fn failed_edit_verification_is_completed_with_issues_and_never_retries_the_edit() {
let result = edit_verification_result(
"MultiFileEdit",
Some(&json!({"thread_id":"thread","files":[]})),
"MultiFileEdit applied all edits",
exited_verification(7),
&verification_recovery(),
);
assert_ne!(result.is_error, Some(true));
let text = result.content[0].as_text().expect("text result");
assert!(text.text.starts_with("EDIT APPLIED."));
assert!(text.text.contains("do not repeat the original edit"));
let structured = result.structured_content.expect("structured verification");
assert_eq!(structured["status"], "completed_with_issues");
assert_eq!(structured["errorCode"], "verification_failed");
assert_eq!(structured["retryable"], false);
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["nextAction"]["tool"], "VerifyEdit");
assert_eq!(structured["nextAction"]["arguments"]["verification_id"], "verify_receipt");
assert_eq!(structured["data"]["edit_applied"], true);
assert_eq!(structured["data"]["original_edit_retryable"], false);
assert_eq!(structured["data"]["follow_up_required"], true);
assert_eq!(structured["data"]["verification_status"], "failed");
assert_eq!(structured["data"]["verification_passed"], false);
assert_eq!(structured["data"]["verification_exit_code"], 7);
}
#[test]
fn standalone_verification_failure_is_retryable_only_after_correction() {
let arguments = json!({
"verification_id": "verify_receipt",
"command": "cargo test",
"thread_id": "thread",
"workspace_root": "/workspace"
});
let result = verify_edit_result(Some(&arguments), "verify_receipt", exited_verification(2));
assert_eq!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured verification");
assert_eq!(structured["status"], "failed");
assert_eq!(structured["errorCode"], "verification_failed");
assert_eq!(structured["retryable"], true);
assert_eq!(structured["retrySameCall"], false);
assert_eq!(structured["nextAction"]["tool"], "VerifyEdit");
assert_eq!(structured["nextAction"]["arguments"], arguments);
assert_eq!(structured["data"]["original_edit_retryable"], false);
}
#[test]
fn active_legacy_verification_keeps_exact_verify_edit_recovery_surface() {
let arguments = json!({
"verification_id": "verify_legacy",
"command": "cargo test",
"wait_for_seconds": 15.0,
"thread_id": "thread",
"workspace_root": "/workspace"
});
let mut nested = CallToolResult::success(vec![ContentBlock::text("still running")]);
nested.structured_content = Some(json!({
"status": "running",
"tool": "BashCommand",
"retryAfterMs": 1000,
"nextAction": {
"tool": "BashCommand",
"arguments": {
"action_json": {
"type": "verify",
"verification_id": "verify_canonical",
"command": "cargo test"
}
}
},
"data": {}
}));
let result = verify_edit_result(Some(&arguments), "verify_legacy", nested);
let structured = result.structured_content.expect("structured verification");
assert_eq!(structured["status"], "running");
assert_eq!(structured["nextAction"]["tool"], "VerifyEdit");
assert_eq!(structured["nextAction"]["arguments"], arguments);
assert!(!structured["nextAction"].to_string().contains("verify_canonical"));
}
#[test]
fn active_initial_legacy_edit_keeps_verify_edit_recovery_surface() {
let edit_arguments = json!({
"file_path": "/workspace/file.rs",
"thread_id": "thread",
"workspace_root": "/workspace"
});
let verify_arguments = json!({
"verification_id": "verify_legacy",
"command": "cargo test",
"wait_for_seconds": 15.0,
"thread_id": "thread",
"workspace_root": "/workspace"
});
for tool in ["FileWriteOrEdit", "MultiFileEdit"] {
let mut nested = CallToolResult::success(vec![ContentBlock::text("still running")]);
nested.structured_content = Some(json!({
"status": "running",
"tool": "BashCommand",
"retryAfterMs": 1000,
"nextAction": {
"tool": "BashCommand",
"arguments": {"action_json": {"type": "status_check"}}
},
"data": {}
}));
let result = edit_verification_result(
tool,
Some(&edit_arguments),
"edit committed",
nested,
&VerificationRecovery {
id: "verify_legacy".to_string(),
tool: "VerifyEdit",
arguments: verify_arguments.clone(),
},
);
let structured = result.structured_content.expect("structured verification");
assert_eq!(structured["status"], "running");
assert_eq!(structured["nextAction"]["tool"], "VerifyEdit");
assert_eq!(structured["nextAction"]["arguments"], verify_arguments);
}
}
#[test]
fn nested_edit_errors_keep_syntax_and_temp_policy_classification() {
let syntax = WinxError::IndexedEditError {
index: 1,
path: "/workspace/file.rs".into(),
mode: "search_replace".to_string(),
source: Box::new(WinxError::SearchReplaceSyntaxError("bad markers".to_string())),
};
let syntax_result = tool_failure("EditFiles", &syntax, None).expect("syntax result");
assert_eq!(
syntax_result.structured_content.as_ref().expect("structured")["errorCode"],
"invalid_tool_input"
);
let temporary = WinxError::EditContextError {
path: "/workspace/.winx_tmp/helper.rs".into(),
source: Box::new(WinxError::TemporaryArtifactPolicy {
path: "/workspace/.winx_tmp/helper.rs".into(),
temporary_artifact_dir: "/workspace/.winx/tmp/session-id".into(),
message: "managed path required".to_string(),
}),
};
let temporary_result =
tool_failure("FileWriteOrEdit", &temporary, None).expect("temp policy result");
assert_eq!(
temporary_result.structured_content.as_ref().expect("structured")["errorCode"],
"temporary_artifact_policy"
);
}
#[test]
fn effective_policy_removes_unadvertised_read_recovery() {
let error = WinxError::FileReadRequired {
path: "/workspace/file.rs".into(),
reason: crate::errors::ReadRequirement::NeverRead,
ranges: Vec::new(),
message: "fresh read required".to_string(),
};
let mut result = tool_failure(
"FileWriteOrEdit",
&error,
Some(&json!({"thread_id":"thread","workspace_root":"/workspace"})),
)
.expect("recovery result");
let policy = crate::tool_policy::ToolPolicy::from_allowed_tools(["FileWriteOrEdit"])
.expect("custom policy");
enforce_next_action_policy(&mut result, policy);
let text = result_text(&result);
let structured = result.structured_content.expect("structured result");
assert_eq!(structured["status"], "denied");
assert_eq!(structured["errorCode"], "policy_blocked");
assert!(structured.get("nextAction").is_none());
assert_eq!(structured["requiredReads"], json!([]));
assert!(text.starts_with("POLICY BLOCKED."), "{text}");
assert_eq!(structured["output"], text, "policy banner must be mirrored");
}
#[test]
fn effective_policy_blocks_hidden_verification_without_hiding_committed_edit() {
let mut result = edit_verification_result(
"FileWriteOrEdit",
Some(&json!({"thread_id":"thread","workspace_root":"/workspace"})),
"edit committed",
exited_verification(1),
&verification_recovery(),
);
let policy =
crate::tool_policy::ToolPolicy::from_allowed_tools(["FileWriteOrEdit", "BashCommand"])
.expect("custom policy");
enforce_next_action_policy(&mut result, policy);
assert_ne!(result.is_error, Some(true));
let structured = result.structured_content.expect("structured result");
assert_eq!(structured["status"], "completed_with_issues");
assert_eq!(structured["errorCode"], "policy_blocked");
assert_eq!(structured["data"]["edit_applied"], true);
assert!(structured.get("nextAction").is_none());
}
#[test]
fn compact_negotiation_falls_back_to_legacy_runtime_output() {
let legacy = "child output\n\n---\n\nstatus = process exited\ncwd = /workspace";
let result = bash_success_result(
Some(&json!({"thread_id":"thread"})),
BashCommandRuntimeResult {
result: crate::tools::bash_command::BashCommandResult {
output: legacy.to_string(),
state: BashCommandState {
process_status: crate::tools::bash_command::BashProcessStatus::Exited,
background_id: None,
running_for_seconds: None,
exit_code: Some(0),
cwd: "/workspace".into(),
turn_state: None,
},
},
compact_output: None,
command_generation: None,
execution_token: None,
generation_bound_actions: false,
dropped_output_file: None,
output_truncated: false,
action_context: None,
workflow: None,
},
true,
)
.expect("legacy guardian result");
let text = result.content[0].as_text().expect("text output");
assert_eq!(text.text, legacy);
assert!(result
.structured_content
.as_ref()
.and_then(|value| value.get("data"))
.and_then(|value| value.get("output_format"))
.is_none());
}
fn background_usage_result(id: &str, retained_completion: bool) -> Value {
let mut outcome =
BashCommandRuntimeResult::legacy(crate::tools::bash_command::BashCommandResult {
output: String::new(),
state: BashCommandState {
process_status: if retained_completion {
crate::tools::bash_command::BashProcessStatus::Exited
} else {
crate::tools::bash_command::BashProcessStatus::Running
},
background_id: Some(id.to_string()),
running_for_seconds: None,
exit_code: retained_completion.then_some(0),
cwd: "/workspace".into(),
turn_state: None,
},
});
if !retained_completion {
outcome.command_generation = Some(1);
outcome.execution_token = Some(crate::runtime::ShellExecutionToken {
guardian_epoch: "guardian".to_string(),
session_epoch: "session".to_string(),
generation: 1,
});
}
bash_success_result(None, outcome, false)
.expect("background response")
.structured_content
.expect("structured background response")
}
#[test]
fn independent_background_commands_have_distinct_execution_ids() {
let first = background_usage_result("0000000000000001", false);
let second = background_usage_result("0000000000000002", false);
assert!(first["data"]["execution_id"].as_str().is_some());
assert!(second["data"]["execution_id"].as_str().is_some());
assert_ne!(first["data"]["execution_id"], second["data"]["execution_id"]);
}
#[test]
fn background_execution_id_survives_retained_completion() {
let started = background_usage_result("0000000000000001", false);
let completed = background_usage_result("0000000000000001", true);
assert!(completed["data"]["execution_id"].as_str().is_some());
assert_eq!(started["data"]["execution_id"], completed["data"]["execution_id"]);
assert_eq!(completed["data"]["exit_code"], 0);
}
#[test]
fn busy_shell_marks_the_rejected_command_as_not_started() {
let result = tool_failure(
"BashCommand",
&WinxError::CommandAlreadyRunning {
current_command: "existing-job".to_string(),
duration_seconds: 2.0,
},
Some(&json!({
"thread_id": "thread",
"workspace_root": "/workspace",
"action_json": {"type": "command", "command": "independent-job"}
})),
)
.expect("busy response");
let structured = result.structured_content.expect("structured busy response");
assert_eq!(structured["errorCode"], "command_already_running");
assert_eq!(structured["data"]["command_started"], false);
assert_eq!(structured["data"]["shell_busy"], true);
assert!(structured["data"].get("execution_id").is_none());
assert_eq!(structured["nextAction"]["arguments"]["action_json"]["type"], "status_check");
assert_eq!(structured["retrySameCall"], false);
}
}