1use serde_json::{Map, Value};
39
40use crate::value::VmError;
41
42use super::AgentEvent;
43
44const HOST_AGENT_EMIT_EVENT: &str = "__host_agent_emit_event";
45
46const HOST_DESERIALIZE_EVENT_TYPES: &[&str] = &[
51 "tool_call",
52 "tool_call_update",
53 "iteration_start",
54 "iteration_end",
55 "judge_decision",
56 "step_judge_decision",
57 "structural_validator_decision",
58 "scope_classifier_verdict",
59 "input_guardrail_verdict",
60 "missing_tool_call_verdict",
61 "budget_exhausted",
62 "budget_circuit_breaker",
63 "progress_reported",
64 "tool_search_query",
65 "tool_search_result",
66 "skill_narrow",
67 "loop_control_decision",
68 "capability_gap",
69 "tool_format_override",
70 "tool_call_audit",
71 "loop_checkpoint",
72];
73
74impl AgentEvent {
75 pub fn from_host_payload(
81 session_id: &str,
82 event_type: &str,
83 payload: &Value,
84 ) -> Result<AgentEvent, VmError> {
85 if let Some(event) = from_host_special(session_id, event_type, payload) {
86 return Ok(event);
87 }
88 from_host_generic(session_id, event_type, payload)
89 }
90}
91
92fn from_host_special(session_id: &str, event_type: &str, payload: &Value) -> Option<AgentEvent> {
98 let sid = || session_id.to_string();
99 let feedback = |kind: &str, content: String| AgentEvent::FeedbackInjected {
100 session_id: sid(),
101 kind: kind.to_string(),
102 content,
103 };
104 let event = match event_type {
105 "typed_checkpoint" => AgentEvent::TypedCheckpoint {
106 session_id: sid(),
107 checkpoint: payload.clone(),
108 },
109 "loop_stuck" => AgentEvent::LoopStuckSignal {
110 session_id: sid(),
111 payload: payload.clone(),
112 },
113 "reserved_terminal_verify" => AgentEvent::ReservedTerminalVerify {
114 session_id: sid(),
115 payload: payload.clone(),
116 },
117 "agent_loop_stall_warning" => AgentEvent::AgentLoopStallWarning {
118 session_id: sid(),
119 warning: payload.clone(),
120 },
121 "cache_hit" => AgentEvent::CacheHit {
122 session_id: sid(),
123 key: obj_string(payload, "key"),
124 backend: obj_string(payload, "backend"),
125 namespace: obj_string(payload, "namespace"),
126 payload: payload.clone(),
127 },
128 "cache_miss" => AgentEvent::CacheMiss {
129 session_id: sid(),
130 key: obj_string(payload, "key"),
131 backend: obj_string(payload, "backend"),
132 namespace: obj_string(payload, "namespace"),
133 payload: payload.clone(),
134 },
135 "agent_scratchpad_reorganization" => {
136 let mut details = payload.clone();
137 if let Some(object) = details.as_object_mut() {
138 object.remove("iteration");
139 object.remove("status");
140 }
141 AgentEvent::AgentScratchpadReorganization {
142 session_id: sid(),
143 iteration: obj_usize(payload, "iteration"),
144 status: obj_string(payload, "status"),
145 details,
146 }
147 }
148 "stance_armed"
152 | "stance_write_access_granted"
153 | "stance_write_access_denied"
154 | "stance_disarmed" => {
155 let allowed_tools = payload
156 .get("allowed_tools")
157 .and_then(Value::as_array)
158 .map(|values| {
159 values
160 .iter()
161 .filter_map(|value| value.as_str().map(str::to_string))
162 .collect()
163 })
164 .unwrap_or_default();
165 AgentEvent::StanceTransition {
166 session_id: sid(),
167 phase: event_type
168 .strip_prefix("stance_")
169 .unwrap_or(event_type)
170 .to_string(),
171 escape_tool: obj_string(payload, "escape_tool"),
172 allowed_tools,
173 justification: obj_string(payload, "justification"),
174 consent: obj_string(payload, "consent"),
175 reason: obj_string(payload, "reason"),
176 }
177 }
178 "completion_confirmation_nudge" => feedback(
182 "completion_confirmation_nudge",
183 obj_string(payload, "visible_text_prefix"),
184 ),
185 "fenced_call_attempt_nudge" => {
186 feedback("fenced_call_attempt_nudge", obj_string(payload, "fence"))
187 }
188 "missing_tool_call_nudge" => {
189 feedback("missing_tool_call_nudge", obj_string(payload, "tool"))
190 }
191 "no_progress_streak_nudge" => feedback(
192 "no_progress_streak_nudge",
193 obj_usize(payload, "turns_since_progress").to_string(),
194 ),
195 "tool_parse_error_feedback" => feedback(
196 "tool_parse_error_feedback",
197 obj_string(payload, "error_summary"),
198 ),
199 "tool_call_blank_name_dropped" => feedback(
200 "tool_call_blank_name_dropped",
201 obj_usize(payload, "dropped_count").to_string(),
202 ),
203 "llm_auto_continue" => feedback(
204 "llm_auto_continue",
205 format!(
206 "{}->{} (attempt {}/{})",
207 obj_usize(payload, "previous_max_tokens"),
208 obj_usize(payload, "raised_max_tokens"),
209 obj_usize(payload, "attempt"),
210 obj_usize(payload, "max_continuations"),
211 ),
212 ),
213 "context_overflow_recovery" => feedback(
214 "context_overflow_recovery",
215 format!(
216 "attempt {}/{} archived {} messages",
217 obj_usize(payload, "attempt"),
218 obj_usize(payload, "max_recoveries"),
219 obj_usize(payload, "archived_messages"),
220 ),
221 ),
222 _ => return None,
223 };
224 Some(event)
225}
226
227fn from_host_generic(
231 session_id: &str,
232 event_type: &str,
233 payload: &Value,
234) -> Result<AgentEvent, VmError> {
235 if !HOST_DESERIALIZE_EVENT_TYPES.contains(&event_type) {
236 return Err(VmError::Runtime(format!(
237 "{HOST_AGENT_EMIT_EVENT}: unsupported event type `{event_type}`"
238 )));
239 }
240 let mut obj = match payload {
241 Value::Object(map) => map.clone(),
242 _ => Map::new(),
243 };
244 apply_host_payload_defaults(event_type, &mut obj)?;
245 obj.insert("type".to_string(), Value::String(event_type.to_string()));
246 obj.insert(
247 "session_id".to_string(),
248 Value::String(session_id.to_string()),
249 );
250 let mut event: AgentEvent = serde_json::from_value(Value::Object(obj)).map_err(|error| {
251 VmError::Runtime(format!(
252 "{HOST_AGENT_EMIT_EVENT}: invalid `{event_type}` payload: {error}"
253 ))
254 })?;
255 if let AgentEvent::ToolCall { audit, .. } | AgentEvent::ToolCallUpdate { audit, .. } =
258 &mut event
259 {
260 *audit = crate::orchestration::current_mutation_session();
261 }
262 Ok(event)
263}
264
265fn apply_host_payload_defaults(
270 event_type: &str,
271 obj: &mut Map<String, Value>,
272) -> Result<(), VmError> {
273 match event_type {
274 "tool_call" => {
275 obj.remove("audit"); set_default(obj, "status", Value::String("pending".to_string()));
277 set_default(obj, "raw_input", Value::Null);
278 }
279 "tool_call_update" => {
280 obj.remove("audit"); set_default(obj, "status", Value::String("in_progress".to_string()));
282 normalize_executor(obj)?;
283 }
284 "iteration_end" => set_default(obj, "iteration_info", Value::Null),
285 "progress_reported" => {
286 set_default(obj, "entries", Value::Array(Vec::new()));
287 set_default(obj, "replace", Value::Bool(true));
288 set_default(obj, "metadata", Value::Object(Map::new()));
289 }
290 "tool_search_query" => set_default(obj, "query", Value::Null),
291 "tool_search_result" => set_default(obj, "promoted", Value::Array(Vec::new())),
292 "skill_narrow" => {
293 set_default(obj, "removed_tools", Value::Array(Vec::new()));
294 set_default(obj, "remaining_tools", Value::Array(Vec::new()));
295 }
296 "tool_call_audit" => set_default(obj, "audit", Value::Null),
297 _ => {}
298 }
299 Ok(())
300}
301
302fn normalize_executor(obj: &mut Map<String, Value>) -> Result<(), VmError> {
308 let raw = match obj.get("executor") {
309 Some(Value::String(value)) => value.clone(),
310 _ => return Ok(()),
311 };
312 let kind = match raw.trim() {
313 "" => {
314 obj.remove("executor");
315 return Ok(());
316 }
317 "harn" | "harn_builtin" => "harn_builtin",
318 "host" | "host_bridge" => "host_bridge",
319 "provider" | "provider_native" => "provider_native",
320 other => {
321 return Err(VmError::Runtime(format!(
322 "{HOST_AGENT_EMIT_EVENT}: invalid tool executor `{other}`"
323 )));
324 }
325 };
326 let mut executor = Map::new();
327 executor.insert("kind".to_string(), Value::String(kind.to_string()));
328 obj.insert("executor".to_string(), Value::Object(executor));
329 Ok(())
330}
331
332fn set_default(obj: &mut Map<String, Value>, key: &str, value: Value) {
333 obj.entry(key).or_insert(value);
334}
335
336fn obj_string(payload: &Value, key: &str) -> String {
337 payload
338 .get(key)
339 .and_then(Value::as_str)
340 .unwrap_or("")
341 .to_string()
342}
343
344fn obj_usize(payload: &Value, key: &str) -> usize {
345 payload.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
346}