use std::time::Duration;
use log::{info, warn};
use crate::cm_agent::agent_turn::{ToolPolicyEarlyDenyParams, tool_policy_early_deny_message};
use crate::agent::agent_turn::run_command_dedupe::{
RUN_COMMAND_DUPLICATE_SUPPRESSED_MSG, run_command_duplicate_suppress_key,
};
use crate::agent::per_coord::PerCoordinator;
use crate::tool_registry;
use crate::tool_result::parse_legacy_output;
use super::super::emit_tool_result_sse_and_append;
use super::super::run_command_guard::{
classify_run_command_failure_family_from_invocation, parse_run_command_payload,
run_command_cargo_workdir_preflight_error, run_command_ctest_preflight_error,
};
use super::{
SerialEarlyToolPolicyDenyParams, SerialEmitEarlyWithoutDispatchParams,
SerialEmitToolResultParams, SerialTtlAfterDispatchParams, SerialTtlRunCommandEarlyHitParams,
};
pub(super) async fn serial_try_ttl_run_command_cache_hit(
p: SerialTtlRunCommandEarlyHitParams<'_>,
) -> bool {
let ttl_secs = p.cfg.chat_queues_cache.readonly_tool_ttl_cache_secs;
if ttl_secs == 0
|| p.name != "run_command"
|| !crate::readonly_tool_ttl_cache::run_command_invocation_ttl_cache_eligible(p.args)
{
return false;
}
let ws_key = p.effective_working_dir.to_string_lossy();
let Some(cached) = p
.readonly_tool_ttl_cache
.try_get(ws_key.as_ref(), p.name, p.args)
else {
return false;
};
let body = format!("[只读命令短时缓存命中 · TTL≤{ttl_secs}s]\n{cached}");
info!(
target: super::super::LOG_TARGET,
"run_command TTL 缓存命中 args_preview={}",
crate::redact::tool_arguments_preview_for_log(p.args)
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
result: body,
reflection_inject: None,
})
.await;
true
}
fn readonly_tool_ttl_cache_should_invalidate_workspace(
cfg: &crate::config::AgentConfig,
name: &str,
args: &str,
workspace_changed: bool,
) -> bool {
workspace_changed
|| if name == "run_command" {
!crate::readonly_tool_ttl_cache::run_command_invocation_ttl_cache_eligible(args)
} else {
!tool_registry::is_readonly_tool(cfg, name)
}
}
pub(super) async fn serial_emit_early_tool_policy_denials(
p: SerialEarlyToolPolicyDenyParams<'_>,
) -> bool {
if let Some(denied) = tool_policy_early_deny_message(&ToolPolicyEarlyDenyParams {
cfg: p.cfg.as_ref(),
name: p.name,
step_executor_constraint: p.step_executor_constraint,
tools_defs: p.tools_defs_full,
turn_allow: p.turn_allow,
}) {
warn!(target: super::super::LOG_TARGET, "{}", denied);
emit_serial_tool_result(SerialEmitToolResultParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
result: denied,
reflection_inject: None,
})
.await;
return true;
}
false
}
pub(super) fn serial_bookkeep_readonly_tool_ttl_cache_after_tool(
p: SerialTtlAfterDispatchParams<'_>,
) {
let ws_key = p.effective_working_dir.to_string_lossy();
let ttl_secs = p.cfg.chat_queues_cache.readonly_tool_ttl_cache_secs;
let mut ttl_run_command_success_cache: Option<String> = None;
if ttl_secs > 0 && p.name == "run_command" {
let parsed_tool = parse_legacy_output(p.name, p.result);
if parsed_tool.ok
&& crate::readonly_tool_ttl_cache::run_command_invocation_ttl_cache_eligible(p.args)
{
ttl_run_command_success_cache = Some(p.result.to_string());
} else {
p.readonly_tool_ttl_cache
.remove(ws_key.as_ref(), p.name, p.args);
}
}
if readonly_tool_ttl_cache_should_invalidate_workspace(
p.cfg,
p.name,
p.args,
p.workspace_changed,
) {
p.readonly_tool_ttl_cache
.invalidate_workspace(ws_key.as_ref());
}
if let Some(out) = ttl_run_command_success_cache {
p.readonly_tool_ttl_cache.insert(
ws_key.as_ref(),
p.name,
p.args,
out,
Duration::from_secs(ttl_secs),
p.cfg.chat_queues_cache.readonly_tool_ttl_cache_max_entries,
);
}
}
pub(super) async fn emit_serial_tool_result(p: SerialEmitToolResultParams<'_>) {
let SerialEmitToolResultParams {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control,
tool_result_envelope_v1,
name,
args,
id,
result,
reflection_inject,
} = p;
let env = crate::tool_result::ToolEnvelopeContext {
tool_call_id: id,
execution_mode: "serial",
parallel_batch_id: None,
};
emit_tool_result_sse_and_append(
messages,
per_coord,
super::super::EmitToolResultParams {
cfg,
tool_outcome_recorder,
control: control.clone(),
tool_result_envelope_v1,
name,
args,
id,
result,
reflection_inject,
envelope_ctx: Some(env),
},
control.sse_encoder.as_ref(),
)
.await;
}
fn mark_ctest_preflight_failure_signature(per_coord: &mut PerCoordinator, name: &str, args: &str) {
per_coord.mark_tool_failure_signature(name, args, "ctest_dash_c_build_misuse".to_string());
per_coord.mark_tool_failure_family(
name,
"ctest_dash_c_build_misuse",
"ctest_dash_c_build_misuse".to_string(),
);
}
struct SerialRunCommandDupShortCircuitEmitCtx<'a> {
messages: &'a mut Vec<crate::types::Message>,
per_coord: &'a mut PerCoordinator,
cfg: &'a std::sync::Arc<crate::config::AgentConfig>,
tool_outcome_recorder: &'a std::sync::Arc<crate::tool_stats::ToolOutcomeRecorder>,
control: crate::agent::agent_turn::TurnControlSink<'a>,
tool_result_envelope_v1: bool,
name: &'a str,
args: &'a str,
id: &'a str,
}
type SerialRunCommandSuccessDedupeParams<'a> = SerialRunCommandDupShortCircuitEmitCtx<'a>;
async fn serial_emit_run_command_success_dedupe(
p: SerialRunCommandSuccessDedupeParams<'_>,
) -> bool {
let SerialRunCommandSuccessDedupeParams {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control,
tool_result_envelope_v1,
name,
args,
id,
} = p;
let Some(suppress_key) = run_command_duplicate_suppress_key(args) else {
return false;
};
let Some(cached) = per_coord
.cached_successful_run_command_output(&suppress_key)
.map(str::to_string)
else {
return false;
};
info!(
target: super::super::LOG_TARGET,
"run_command 成功去重命中 suppress_key={} args_preview={}",
suppress_key,
crate::redact::tool_arguments_preview_for_log(args)
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control: control.clone(),
tool_result_envelope_v1,
name,
args,
id,
result: format!("{RUN_COMMAND_DUPLICATE_SUPPRESSED_MSG}\n{cached}"),
reflection_inject: None,
})
.await;
true
}
async fn serial_emit_run_command_failure_short_circuits(
p: SerialRunCommandDupShortCircuitEmitCtx<'_>,
) -> bool {
let SerialRunCommandDupShortCircuitEmitCtx {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control,
tool_result_envelope_v1,
name,
args,
id,
} = p;
if let Some(prev_error) = per_coord.repeated_tool_failure_error_marker(name, args) {
let short_circuit = format!(
"错误:检测到同命令重复失败,已短路本次调用(error={prev_error})。请切换策略(例如调整工作目录、改用 --manifest-path、或先做目录/文件探测)。"
);
warn!(
target: super::super::LOG_TARGET,
"run_command 重复失败短路 args_preview={} prev_error={}",
crate::redact::tool_arguments_preview_for_log(args),
prev_error
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control: control.clone(),
tool_result_envelope_v1,
name,
args,
id,
result: short_circuit,
reflection_inject: None,
})
.await;
return true;
}
if let Some((command, command_args)) = parse_run_command_payload(args)
&& let Some(family) = classify_run_command_failure_family_from_invocation(
command.as_str(),
command_args.as_slice(),
)
&& let Some(prev_error) = per_coord.repeated_tool_failure_family_marker(name, family)
{
let short_circuit = format!(
"错误:检测到同类失败已发生(family={family}, prev_error={prev_error}),已短路本次调用。请直接切换策略,避免继续同类试探。"
);
warn!(
target: super::super::LOG_TARGET,
"run_command 同类失败短路 family={} args_preview={} prev_error={}",
family,
crate::redact::tool_arguments_preview_for_log(args),
prev_error
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages,
per_coord,
cfg,
tool_outcome_recorder,
control: control.clone(),
tool_result_envelope_v1,
name,
args,
id,
result: short_circuit,
reflection_inject: None,
})
.await;
return true;
}
false
}
async fn serial_emit_run_command_preflight_errors(
p: &mut SerialEmitEarlyWithoutDispatchParams<'_>,
) -> bool {
if let Some(preflight_error) =
run_command_cargo_workdir_preflight_error(p.name, p.args, p.effective_working_dir)
{
p.per_coord.mark_tool_failure_signature(
p.name,
p.args,
"cargo_manifest_missing".to_string(),
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
result: preflight_error,
reflection_inject: None,
})
.await;
return true;
}
if let Some(preflight_error) = run_command_ctest_preflight_error(p.name, p.args) {
mark_ctest_preflight_failure_signature(p.per_coord, p.name, p.args);
emit_serial_tool_result(SerialEmitToolResultParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
result: preflight_error,
reflection_inject: None,
})
.await;
return true;
}
false
}
async fn serial_emit_run_command_and_readonly_cache_hits(
p: &mut SerialEmitEarlyWithoutDispatchParams<'_>,
) -> bool {
if p.name == "run_command"
&& serial_emit_run_command_success_dedupe(SerialRunCommandSuccessDedupeParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
})
.await
{
return true;
}
if p.name == "run_command"
&& serial_emit_run_command_failure_short_circuits(SerialRunCommandDupShortCircuitEmitCtx {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
})
.await
{
return true;
}
if serial_try_ttl_run_command_cache_hit(SerialTtlRunCommandEarlyHitParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
effective_working_dir: p.effective_working_dir,
name: p.name,
args: p.args,
id: p.id,
readonly_tool_ttl_cache: p.readonly_tool_ttl_cache,
})
.await
{
return true;
}
let is_readonly = tool_registry::is_readonly_tool(p.cfg.as_ref(), p.name);
let cache_key = (p.name.to_string(), p.args.to_string());
if is_readonly && let Some(cached) = p.readonly_cache.get(&cache_key) {
let cached = cached.clone();
info!(
target: super::super::LOG_TARGET,
"工具结果命中缓存(只读去重) tool={} args_preview={}",
p.name,
crate::redact::tool_arguments_preview_for_log(p.args)
);
emit_serial_tool_result(SerialEmitToolResultParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
result: cached,
reflection_inject: None,
})
.await;
return true;
}
false
}
pub(super) async fn serial_emit_early_without_dispatch(
mut p: SerialEmitEarlyWithoutDispatchParams<'_>,
) -> bool {
if serial_emit_run_command_preflight_errors(&mut p).await {
return true;
}
if serial_emit_early_tool_policy_denials(SerialEarlyToolPolicyDenyParams {
messages: p.messages,
per_coord: p.per_coord,
cfg: p.cfg,
tool_outcome_recorder: p.tool_outcome_recorder,
control: p.control.clone(),
tool_result_envelope_v1: p.tool_result_envelope_v1,
name: p.name,
args: p.args,
id: p.id,
step_executor_constraint: p.step_executor_constraint,
tools_defs_full: p.tools_defs_full,
turn_allow: p.turn_allow,
})
.await
{
return true;
}
serial_emit_run_command_and_readonly_cache_hits(&mut p).await
}