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