use serde_json::{Map, Value};
use crate::value::VmError;
use super::AgentEvent;
const HOST_AGENT_EMIT_EVENT: &str = "__host_agent_emit_event";
const HOST_DESERIALIZE_EVENT_TYPES: &[&str] = &[
"tool_call",
"tool_call_update",
"iteration_start",
"iteration_end",
"judge_decision",
"step_judge_decision",
"structural_validator_decision",
"scope_classifier_verdict",
"missing_tool_call_verdict",
"budget_exhausted",
"budget_circuit_breaker",
"progress_reported",
"tool_search_query",
"tool_search_result",
"skill_narrow",
"loop_control_decision",
"capability_gap",
"tool_format_override",
"tool_call_audit",
"loop_checkpoint",
];
impl AgentEvent {
pub fn from_host_payload(
session_id: &str,
event_type: &str,
payload: &Value,
) -> Result<AgentEvent, VmError> {
if let Some(event) = from_host_special(session_id, event_type, payload) {
return Ok(event);
}
from_host_generic(session_id, event_type, payload)
}
}
fn from_host_special(session_id: &str, event_type: &str, payload: &Value) -> Option<AgentEvent> {
let sid = || session_id.to_string();
let feedback = |kind: &str, content: String| AgentEvent::FeedbackInjected {
session_id: sid(),
kind: kind.to_string(),
content,
};
let event = match event_type {
"typed_checkpoint" => AgentEvent::TypedCheckpoint {
session_id: sid(),
checkpoint: payload.clone(),
},
"loop_stuck" => AgentEvent::LoopStuckSignal {
session_id: sid(),
payload: payload.clone(),
},
"reserved_terminal_verify" => AgentEvent::ReservedTerminalVerify {
session_id: sid(),
payload: payload.clone(),
},
"agent_loop_stall_warning" => AgentEvent::AgentLoopStallWarning {
session_id: sid(),
warning: payload.clone(),
},
"cache_hit" => AgentEvent::CacheHit {
session_id: sid(),
key: obj_string(payload, "key"),
backend: obj_string(payload, "backend"),
namespace: obj_string(payload, "namespace"),
payload: payload.clone(),
},
"cache_miss" => AgentEvent::CacheMiss {
session_id: sid(),
key: obj_string(payload, "key"),
backend: obj_string(payload, "backend"),
namespace: obj_string(payload, "namespace"),
payload: payload.clone(),
},
"agent_scratchpad_reorganization" => {
let mut details = payload.clone();
if let Some(object) = details.as_object_mut() {
object.remove("iteration");
object.remove("status");
}
AgentEvent::AgentScratchpadReorganization {
session_id: sid(),
iteration: obj_usize(payload, "iteration"),
status: obj_string(payload, "status"),
details,
}
}
"stance_armed"
| "stance_write_access_granted"
| "stance_write_access_denied"
| "stance_disarmed" => {
let allowed_tools = payload
.get("allowed_tools")
.and_then(Value::as_array)
.map(|values| {
values
.iter()
.filter_map(|value| value.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
AgentEvent::StanceTransition {
session_id: sid(),
phase: event_type
.strip_prefix("stance_")
.unwrap_or(event_type)
.to_string(),
escape_tool: obj_string(payload, "escape_tool"),
allowed_tools,
justification: obj_string(payload, "justification"),
consent: obj_string(payload, "consent"),
reason: obj_string(payload, "reason"),
}
}
"completion_confirmation_nudge" => feedback(
"completion_confirmation_nudge",
obj_string(payload, "visible_text_prefix"),
),
"fenced_call_attempt_nudge" => {
feedback("fenced_call_attempt_nudge", obj_string(payload, "fence"))
}
"missing_tool_call_nudge" => {
feedback("missing_tool_call_nudge", obj_string(payload, "tool"))
}
"no_progress_streak_nudge" => feedback(
"no_progress_streak_nudge",
obj_usize(payload, "turns_since_progress").to_string(),
),
"tool_parse_error_feedback" => feedback(
"tool_parse_error_feedback",
obj_string(payload, "error_summary"),
),
"tool_call_blank_name_dropped" => feedback(
"tool_call_blank_name_dropped",
obj_usize(payload, "dropped_count").to_string(),
),
"llm_auto_continue" => feedback(
"llm_auto_continue",
format!(
"{}->{} (attempt {}/{})",
obj_usize(payload, "previous_max_tokens"),
obj_usize(payload, "raised_max_tokens"),
obj_usize(payload, "attempt"),
obj_usize(payload, "max_continuations"),
),
),
"context_overflow_recovery" => feedback(
"context_overflow_recovery",
format!(
"attempt {}/{} archived {} messages",
obj_usize(payload, "attempt"),
obj_usize(payload, "max_recoveries"),
obj_usize(payload, "archived_messages"),
),
),
_ => return None,
};
Some(event)
}
fn from_host_generic(
session_id: &str,
event_type: &str,
payload: &Value,
) -> Result<AgentEvent, VmError> {
if !HOST_DESERIALIZE_EVENT_TYPES.contains(&event_type) {
return Err(VmError::Runtime(format!(
"{HOST_AGENT_EMIT_EVENT}: unsupported event type `{event_type}`"
)));
}
let mut obj = match payload {
Value::Object(map) => map.clone(),
_ => Map::new(),
};
apply_host_payload_defaults(event_type, &mut obj)?;
obj.insert("type".to_string(), Value::String(event_type.to_string()));
obj.insert(
"session_id".to_string(),
Value::String(session_id.to_string()),
);
let mut event: AgentEvent = serde_json::from_value(Value::Object(obj)).map_err(|error| {
VmError::Runtime(format!(
"{HOST_AGENT_EMIT_EVENT}: invalid `{event_type}` payload: {error}"
))
})?;
if let AgentEvent::ToolCall { audit, .. } | AgentEvent::ToolCallUpdate { audit, .. } =
&mut event
{
*audit = crate::orchestration::current_mutation_session();
}
Ok(event)
}
fn apply_host_payload_defaults(
event_type: &str,
obj: &mut Map<String, Value>,
) -> Result<(), VmError> {
match event_type {
"tool_call" => {
obj.remove("audit"); set_default(obj, "status", Value::String("pending".to_string()));
set_default(obj, "raw_input", Value::Null);
}
"tool_call_update" => {
obj.remove("audit"); set_default(obj, "status", Value::String("in_progress".to_string()));
normalize_executor(obj)?;
}
"iteration_end" => set_default(obj, "iteration_info", Value::Null),
"progress_reported" => {
set_default(obj, "entries", Value::Array(Vec::new()));
set_default(obj, "replace", Value::Bool(true));
set_default(obj, "metadata", Value::Object(Map::new()));
}
"tool_search_query" => set_default(obj, "query", Value::Null),
"tool_search_result" => set_default(obj, "promoted", Value::Array(Vec::new())),
"skill_narrow" => {
set_default(obj, "removed_tools", Value::Array(Vec::new()));
set_default(obj, "remaining_tools", Value::Array(Vec::new()));
}
"tool_call_audit" => set_default(obj, "audit", Value::Null),
_ => {}
}
Ok(())
}
fn normalize_executor(obj: &mut Map<String, Value>) -> Result<(), VmError> {
let raw = match obj.get("executor") {
Some(Value::String(value)) => value.clone(),
_ => return Ok(()),
};
let kind = match raw.trim() {
"" => {
obj.remove("executor");
return Ok(());
}
"harn" | "harn_builtin" => "harn_builtin",
"host" | "host_bridge" => "host_bridge",
"provider" | "provider_native" => "provider_native",
other => {
return Err(VmError::Runtime(format!(
"{HOST_AGENT_EMIT_EVENT}: invalid tool executor `{other}`"
)));
}
};
let mut executor = Map::new();
executor.insert("kind".to_string(), Value::String(kind.to_string()));
obj.insert("executor".to_string(), Value::Object(executor));
Ok(())
}
fn set_default(obj: &mut Map<String, Value>, key: &str, value: Value) {
obj.entry(key).or_insert(value);
}
fn obj_string(payload: &Value, key: &str) -> String {
payload
.get(key)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}
fn obj_usize(payload: &Value, key: &str) -> usize {
payload.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
}