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 streak: None,
104 };
105 let feedback_with_streak =
106 |kind: &str, content: String, streak: Option<usize>| AgentEvent::FeedbackInjected {
107 session_id: sid(),
108 kind: kind.to_string(),
109 content,
110 streak,
111 };
112 let feedback_content = |fallback: String| {
113 first_non_empty_string(payload, &["content", "message", "text"]).unwrap_or(fallback)
114 };
115 let event = match event_type {
116 "typed_checkpoint" => AgentEvent::TypedCheckpoint {
117 session_id: sid(),
118 checkpoint: payload.clone(),
119 },
120 "loop_stuck" => AgentEvent::LoopStuckSignal {
121 session_id: sid(),
122 payload: payload.clone(),
123 },
124 "reserved_terminal_verify" => AgentEvent::ReservedTerminalVerify {
125 session_id: sid(),
126 payload: payload.clone(),
127 },
128 "agent_loop_stall_warning" => AgentEvent::AgentLoopStallWarning {
129 session_id: sid(),
130 warning: payload.clone(),
131 },
132 "cache_hit" => AgentEvent::CacheHit {
133 session_id: sid(),
134 key: obj_string(payload, "key"),
135 backend: obj_string(payload, "backend"),
136 namespace: obj_string(payload, "namespace"),
137 payload: payload.clone(),
138 },
139 "cache_miss" => AgentEvent::CacheMiss {
140 session_id: sid(),
141 key: obj_string(payload, "key"),
142 backend: obj_string(payload, "backend"),
143 namespace: obj_string(payload, "namespace"),
144 payload: payload.clone(),
145 },
146 "agent_scratchpad_reorganization" => {
147 let mut details = payload.clone();
148 if let Some(object) = details.as_object_mut() {
149 object.remove("iteration");
150 object.remove("status");
151 }
152 AgentEvent::AgentScratchpadReorganization {
153 session_id: sid(),
154 iteration: obj_usize(payload, "iteration"),
155 status: obj_string(payload, "status"),
156 details,
157 }
158 }
159 "stance_armed"
163 | "stance_write_access_granted"
164 | "stance_write_access_denied"
165 | "stance_disarmed" => {
166 let allowed_tools = payload
167 .get("allowed_tools")
168 .and_then(Value::as_array)
169 .map(|values| {
170 values
171 .iter()
172 .filter_map(|value| value.as_str().map(str::to_string))
173 .collect()
174 })
175 .unwrap_or_default();
176 AgentEvent::StanceTransition {
177 session_id: sid(),
178 phase: event_type
179 .strip_prefix("stance_")
180 .unwrap_or(event_type)
181 .to_string(),
182 escape_tool: obj_string(payload, "escape_tool"),
183 allowed_tools,
184 justification: obj_string(payload, "justification"),
185 consent: obj_string(payload, "consent"),
186 reason: obj_string(payload, "reason"),
187 }
188 }
189 "completion_confirmation_nudge" => feedback(
193 "completion_confirmation_nudge",
194 feedback_content(obj_string(payload, "visible_text_prefix")),
195 ),
196 "fenced_call_attempt_nudge" => feedback(
197 "fenced_call_attempt_nudge",
198 feedback_content(obj_string(payload, "fence")),
199 ),
200 "missing_tool_call_nudge" => feedback(
201 "missing_tool_call_nudge",
202 feedback_content(obj_string(payload, "tool")),
203 ),
204 "no_progress_streak_nudge" => feedback_with_streak(
205 "no_progress_streak_nudge",
206 feedback_content(obj_usize(payload, "turns_since_progress").to_string()),
207 feedback_streak(payload),
208 ),
209 "tool_parse_error_feedback" => feedback(
210 "tool_parse_error_feedback",
211 feedback_content(obj_string(payload, "error_summary")),
212 ),
213 "tool_call_blank_name_dropped" => feedback(
214 "tool_call_blank_name_dropped",
215 feedback_content(obj_usize(payload, "dropped_count").to_string()),
216 ),
217 "llm_auto_continue" => feedback(
218 "llm_auto_continue",
219 feedback_content(format!(
220 "{}->{} (attempt {}/{})",
221 obj_usize(payload, "previous_max_tokens"),
222 obj_usize(payload, "raised_max_tokens"),
223 obj_usize(payload, "attempt"),
224 obj_usize(payload, "max_continuations"),
225 )),
226 ),
227 "context_overflow_recovery" => feedback(
228 "context_overflow_recovery",
229 feedback_content(format!(
230 "attempt {}/{} archived {} messages",
231 obj_usize(payload, "attempt"),
232 obj_usize(payload, "max_recoveries"),
233 obj_usize(payload, "archived_messages"),
234 )),
235 ),
236 _ => return None,
237 };
238 Some(event)
239}
240
241fn first_non_empty_string(payload: &Value, keys: &[&str]) -> Option<String> {
242 keys.iter().find_map(|key| {
243 payload
244 .get(*key)
245 .and_then(Value::as_str)
246 .filter(|value| !value.trim().is_empty())
247 .map(str::to_string)
248 })
249}
250
251fn feedback_streak(payload: &Value) -> Option<usize> {
252 let streak = obj_usize(payload, "streak").max(obj_usize(payload, "turns_since_progress"));
253 (streak > 0).then_some(streak)
254}
255
256fn from_host_generic(
260 session_id: &str,
261 event_type: &str,
262 payload: &Value,
263) -> Result<AgentEvent, VmError> {
264 if !HOST_DESERIALIZE_EVENT_TYPES.contains(&event_type) {
265 return Err(VmError::Runtime(format!(
266 "{HOST_AGENT_EMIT_EVENT}: unsupported event type `{event_type}`"
267 )));
268 }
269 let mut obj = match payload {
270 Value::Object(map) => map.clone(),
271 _ => Map::new(),
272 };
273 apply_host_payload_defaults(event_type, &mut obj)?;
274 obj.insert("type".to_string(), Value::String(event_type.to_string()));
275 obj.insert(
276 "session_id".to_string(),
277 Value::String(session_id.to_string()),
278 );
279 let mut event: AgentEvent = serde_json::from_value(Value::Object(obj)).map_err(|error| {
280 VmError::Runtime(format!(
281 "{HOST_AGENT_EMIT_EVENT}: invalid `{event_type}` payload: {error}"
282 ))
283 })?;
284 if let AgentEvent::ToolCall { audit, .. } | AgentEvent::ToolCallUpdate { audit, .. } =
287 &mut event
288 {
289 *audit = crate::orchestration::current_mutation_session();
290 }
291 Ok(event)
292}
293
294fn apply_host_payload_defaults(
299 event_type: &str,
300 obj: &mut Map<String, Value>,
301) -> Result<(), VmError> {
302 match event_type {
303 "tool_call" => {
304 obj.remove("audit"); set_default(obj, "status", Value::String("pending".to_string()));
306 set_default(obj, "raw_input", Value::Null);
307 }
308 "tool_call_update" => {
309 obj.remove("audit"); set_default(obj, "status", Value::String("in_progress".to_string()));
311 normalize_executor(obj)?;
312 }
313 "iteration_end" => set_default(obj, "iteration_info", Value::Null),
314 "progress_reported" => {
315 set_default(obj, "entries", Value::Array(Vec::new()));
316 set_default(obj, "replace", Value::Bool(true));
317 set_default(obj, "metadata", Value::Object(Map::new()));
318 }
319 "tool_search_query" => set_default(obj, "query", Value::Null),
320 "tool_search_result" => set_default(obj, "promoted", Value::Array(Vec::new())),
321 "skill_narrow" => {
322 set_default(obj, "removed_tools", Value::Array(Vec::new()));
323 set_default(obj, "remaining_tools", Value::Array(Vec::new()));
324 }
325 "tool_call_audit" => set_default(obj, "audit", Value::Null),
326 _ => {}
327 }
328 Ok(())
329}
330
331fn normalize_executor(obj: &mut Map<String, Value>) -> Result<(), VmError> {
337 let raw = match obj.get("executor") {
338 Some(Value::String(value)) => value.clone(),
339 _ => return Ok(()),
340 };
341 let kind = match raw.trim() {
342 "" => {
343 obj.remove("executor");
344 return Ok(());
345 }
346 "harn" | "harn_builtin" => "harn_builtin",
347 "host" | "host_bridge" => "host_bridge",
348 "provider" | "provider_native" => "provider_native",
349 other => {
350 return Err(VmError::Runtime(format!(
351 "{HOST_AGENT_EMIT_EVENT}: invalid tool executor `{other}`"
352 )));
353 }
354 };
355 let mut executor = Map::new();
356 executor.insert("kind".to_string(), Value::String(kind.to_string()));
357 obj.insert("executor".to_string(), Value::Object(executor));
358 Ok(())
359}
360
361fn set_default(obj: &mut Map<String, Value>, key: &str, value: Value) {
362 obj.entry(key).or_insert(value);
363}
364
365fn obj_string(payload: &Value, key: &str) -> String {
366 payload
367 .get(key)
368 .and_then(Value::as_str)
369 .unwrap_or("")
370 .to_string()
371}
372
373fn obj_usize(payload: &Value, key: &str) -> usize {
374 payload.get(key).and_then(Value::as_u64).unwrap_or(0) as usize
375}