use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::agent::AgentInner;
use crate::types::*;
const EMIT_LISTENER_TIMEOUT: Duration = Duration::from_secs(30);
pub(super) fn apply_turn_update(inner: &Arc<AgentInner>, update: AgentLoopTurnUpdate) {
let mut state = inner.state.lock();
if let Some(ctx) = update.context {
state.messages = ctx.messages;
state.system_prompt = ctx.system_prompt;
state.tools = ctx.tools;
}
if let Some(model) = update.model {
state.model = Some(model);
}
if let Some(level) = update.thinking_level {
state.thinking_level = Some(level);
}
}
pub(super) fn snapshot_context(inner: &Arc<AgentInner>) -> AgentContext {
let g = inner.state.lock();
AgentContext {
system_prompt: g.system_prompt.clone(),
messages: g.messages.clone(),
tools: g.tools.clone(),
}
}
pub(super) async fn emit(inner: &Arc<AgentInner>, event: LoopEvent, cancel: &CancellationToken) {
let sync_cbs = inner.sync_callbacks.lock().clone();
for cb in &sync_cbs {
let cb = Arc::clone(cb);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cb(&event);
}));
}
let await_listeners = inner.await_listeners.lock().clone();
for listener in await_listeners {
let listener_token = cancel.clone();
if tokio::time::timeout(
EMIT_LISTENER_TIMEOUT,
listener(event.clone(), listener_token),
)
.await
.is_err()
{
tracing::warn!(
"loop listener exceeded {}s and was dropped",
EMIT_LISTENER_TIMEOUT.as_secs()
);
}
}
let _ = inner.broadcast_tx.send(event);
}
pub(super) async fn finalize(inner: &Arc<AgentInner>, cancel: CancellationToken) {
let messages = inner.state.lock().messages.clone();
emit(inner, LoopEvent::RunEnded { messages }, &cancel).await;
}
pub(super) fn compute_args_hash(args: &serde_json::Value) -> String {
use sha2::{Digest, Sha256};
let canonical = canonicalize(args);
let bytes = serde_json::to_vec(&canonical)
.unwrap_or_else(|_| b"<args canonicalization failed>".to_vec());
let digest = Sha256::digest(&bytes);
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
use std::fmt::Write;
let _ = write!(out, "{byte:02x}");
}
out
}
fn canonicalize(value: &serde_json::Value) -> serde_json::Value {
use std::collections::BTreeMap;
match value {
serde_json::Value::Object(map) => {
let sorted: BTreeMap<String, serde_json::Value> = map
.iter()
.map(|(k, v)| (k.clone(), canonicalize(v)))
.collect();
serde_json::to_value(sorted).unwrap_or(serde_json::Value::Null)
}
serde_json::Value::Array(items) => {
serde_json::Value::Array(items.iter().map(canonicalize).collect())
}
other => other.clone(),
}
}
pub(super) fn default_prompt_payload(
tool_name: &str,
args: &serde_json::Value,
) -> serde_json::Value {
const MAX_KEYS: usize = 32;
const MAX_KEY_LEN: usize = 64;
let keys: Vec<String> = match args {
serde_json::Value::Object(map) => {
let mut ks: Vec<String> = map
.keys()
.take(MAX_KEYS)
.map(|k| {
if k.chars().count() <= MAX_KEY_LEN {
k.clone()
} else {
let mut t: String = k.chars().take(MAX_KEY_LEN).collect();
t.push('…');
t
}
})
.collect();
ks.sort();
ks
}
_ => Vec::new(),
};
serde_json::json!({
"tool_name": tool_name,
"args_keys": keys,
"args_hash": compute_args_hash(args),
})
}
#[cfg(test)]
tests_bridge_macro::tests_bridge!("agent/run_loop/utils");