1use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
8use std::future::Future;
9use std::io::Write;
10use std::path::{Path, PathBuf};
11use std::pin::Pin;
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::Arc;
14use std::time::Duration;
15
16use tokio::io::AsyncBufReadExt;
17use tokio::sync::{oneshot, Mutex, Notify};
18
19use harn_parser::diagnostic_codes::Code;
20
21use crate::orchestration::MutationSessionRecord;
22use crate::value::{ErrorCategory, VmClosure, VmError, VmValue};
23use crate::visible_text::VisibleTextState;
24use crate::vm::Vm;
25
26const DEFAULT_TIMEOUT: Duration = Duration::from_mins(5);
28
29fn bridge_call_timeout(method: &str) -> Option<Duration> {
30 match method {
31 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION => None,
35 _ => Some(DEFAULT_TIMEOUT),
36 }
37}
38
39async fn wait_for_bridge_call_timeout(method: &str) -> Duration {
40 if let Some(timeout) = bridge_call_timeout(method) {
41 tokio::time::sleep(timeout).await;
42 timeout
43 } else {
44 std::future::pending::<Duration>().await
45 }
46}
47
48pub type HostBridgeWriter = Arc<dyn Fn(&str) -> Result<(), String> + Send + Sync>;
49
50fn stdout_writer(stdout_lock: Arc<std::sync::Mutex<()>>) -> HostBridgeWriter {
51 Arc::new(move |line: &str| {
52 let _guard = stdout_lock.lock().unwrap_or_else(|e| e.into_inner());
53 let mut stdout = std::io::stdout().lock();
54 stdout
55 .write_all(line.as_bytes())
56 .map_err(|e| format!("Bridge write error: {e}"))?;
57 stdout
58 .write_all(b"\n")
59 .map_err(|e| format!("Bridge write error: {e}"))?;
60 stdout
61 .flush()
62 .map_err(|e| format!("Bridge flush error: {e}"))?;
63 Ok(())
64 })
65}
66
67pub struct HostBridge {
74 next_id: AtomicU64,
75 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
77 cancelled: Arc<AtomicBool>,
79 cancel_notify: Arc<Notify>,
81 disconnected: Arc<AtomicBool>,
84 writer: HostBridgeWriter,
86 session_id: std::sync::Mutex<String>,
88 script_name: std::sync::Mutex<String>,
90 queued_transcript_injections: HostBridgeInjectionState,
92 resume_requested: Arc<AtomicBool>,
94 skills_reload_requested: Arc<AtomicBool>,
99 daemon_idle: Arc<AtomicBool>,
101 prompt_outcome: std::sync::Mutex<Option<(String, crate::agent_events::AgentTerminalOutcome)>>,
104 visible_call_states: std::sync::Mutex<HashMap<String, VisibleTextState>>,
106 visible_call_streams: std::sync::Mutex<HashMap<String, bool>>,
108 in_process: Option<InProcessHost>,
110}
111
112struct InProcessHost {
113 module_path: PathBuf,
114 exported_functions: BTreeMap<String, Arc<VmClosure>>,
115 vm: Vm,
116}
117
118impl InProcessHost {
119 fn dispatch<'a>(
125 &'a self,
126 method: &'a str,
127 params: serde_json::Value,
128 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, VmError>> + Send + 'a>> {
129 Box::pin(async move {
130 match method {
131 "builtin_call" => {
132 let name = params
133 .get("name")
134 .and_then(|value| value.as_str())
135 .unwrap_or_default();
136 let args = params
137 .get("args")
138 .and_then(|value| value.as_array())
139 .cloned()
140 .unwrap_or_default()
141 .into_iter()
142 .map(|value| json_result_to_vm_value(&value))
143 .collect::<Vec<_>>();
144 self.invoke_export(name, &args).await
145 }
146 "host/tools/list" => self
147 .invoke_optional_export("host_tools_list", &[])
148 .await
149 .map(|value| value.unwrap_or_else(|| serde_json::json!({ "tools": [] }))),
150 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION => {
151 self.request_permission(params).await
152 }
153 other => Err(VmError::Runtime(format!(
154 "playground host backend does not implement bridge method '{other}'"
155 ))),
156 }
157 })
158 }
159
160 async fn invoke_export(
161 &self,
162 name: &str,
163 args: &[VmValue],
164 ) -> Result<serde_json::Value, VmError> {
165 let Some(closure) = self.exported_functions.get(name) else {
166 return Err(VmError::Runtime(format!(
167 "Playground host is missing capability '{name}'. Define `pub fn {name}(...)` in {}",
168 self.module_path.display()
169 )));
170 };
171
172 let mut vm = self.vm.child_vm_for_host();
173 let result = vm.call_closure_pub(closure, args).await?;
174 Ok(crate::llm::vm_value_to_json(&result))
175 }
176
177 async fn invoke_optional_export(
178 &self,
179 name: &str,
180 args: &[VmValue],
181 ) -> Result<Option<serde_json::Value>, VmError> {
182 if !self.exported_functions.contains_key(name) {
183 return Ok(None);
184 }
185 self.invoke_export(name, args).await.map(Some)
186 }
187
188 async fn request_permission(
189 &self,
190 params: serde_json::Value,
191 ) -> Result<serde_json::Value, VmError> {
192 let Some(closure) = self.exported_functions.get("request_permission") else {
195 return Ok(crate::llm::acp_permission::allow_response());
196 };
197
198 let tool_call = params.get("toolCall");
199 let tool_name = tool_call
200 .and_then(|tool_call| tool_call.pointer("/_meta/harn/toolName"))
201 .or_else(|| tool_call.and_then(|tool_call| tool_call.get("toolName")))
202 .or_else(|| tool_call.and_then(|tool_call| tool_call.get("title")))
203 .and_then(|value| value.as_str())
204 .unwrap_or_default();
205 let tool_args = tool_call
206 .and_then(|tool_call| tool_call.get("rawInput"))
207 .map(json_result_to_vm_value)
208 .unwrap_or(VmValue::Nil);
209 let full_payload = json_result_to_vm_value(¶ms);
210
211 let arg_count = closure.func.params.len();
212 let args = if arg_count >= 3 {
213 vec![
214 VmValue::String(arcstr::ArcStr::from(tool_name.to_string())),
215 tool_args,
216 full_payload,
217 ]
218 } else if arg_count == 2 {
219 vec![
220 VmValue::String(arcstr::ArcStr::from(tool_name.to_string())),
221 tool_args,
222 ]
223 } else if arg_count == 1 {
224 vec![full_payload]
225 } else {
226 Vec::new()
227 };
228
229 let mut vm = self.vm.child_vm_for_host();
230 let result = vm.call_closure_pub(closure, &args).await?;
231 let payload = match result {
236 VmValue::Bool(granted) => {
237 if granted {
238 crate::llm::acp_permission::allow_response()
239 } else {
240 crate::llm::acp_permission::reject_response(None)
241 }
242 }
243 VmValue::String(reason) if !reason.is_empty() => {
244 crate::llm::acp_permission::reject_response(Some(reason.to_string()))
245 }
246 other => {
247 let json = crate::llm::vm_value_to_json(&other);
248 if let Some(granted) = json.get("granted").and_then(|value| value.as_bool()) {
249 if granted {
250 crate::llm::acp_permission::allow_response()
251 } else {
252 crate::llm::acp_permission::reject_response(
253 json.get("reason")
254 .and_then(|value| value.as_str())
255 .map(str::to_string),
256 )
257 }
258 } else if json.get("outcome").is_some() {
259 json
261 } else if other.is_truthy() {
262 crate::llm::acp_permission::allow_response()
263 } else {
264 crate::llm::acp_permission::reject_response(None)
265 }
266 }
267 };
268 Ok(payload)
269 }
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
281pub enum QueuedUserMessageMode {
282 InterruptImmediate,
283 FinishStep,
284 AuditOnly,
285}
286
287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
288pub enum DeliveryCheckpoint {
289 InterruptImmediate,
290 AfterCurrentOperation,
291 EndOfInteraction,
292}
293
294impl QueuedUserMessageMode {
295 fn from_str(value: &str) -> Self {
296 match value {
297 "interrupt_immediate" | "interrupt" => Self::InterruptImmediate,
298 "finish_step" | "after_current_operation" | "steer" => Self::FinishStep,
302 "queue" => Self::AuditOnly,
304 _ => Self::AuditOnly,
309 }
310 }
311
312 fn as_str(self) -> &'static str {
313 match self {
314 Self::InterruptImmediate => "interrupt_immediate",
315 Self::FinishStep => "finish_step",
316 Self::AuditOnly => "audit_only",
317 }
318 }
319}
320
321#[derive(Clone, Debug, PartialEq, Eq)]
322pub struct QueuedUserMessage {
323 pub message_id: String,
324 pub content: String,
325 pub transcript_content: serde_json::Value,
326 pub mode: QueuedUserMessageMode,
327}
328
329#[derive(Clone, Debug, PartialEq, Eq)]
330pub struct QueuedReminder {
331 pub reminder: crate::llm::helpers::SystemReminder,
332 pub mode: QueuedUserMessageMode,
333}
334
335#[derive(Clone, Debug, PartialEq, Eq)]
336pub enum QueuedTranscriptInjection {
337 User(QueuedUserMessage),
338 Reminder(QueuedReminder),
339}
340
341#[derive(Debug, Default)]
342struct QueuedTranscriptInjections {
343 queue: VecDeque<QueuedTranscriptInjection>,
344 revoked_user_message_ids: HashSet<String>,
345 delivered_user_message_ids: HashSet<String>,
346 revoked_reminder_ids: HashSet<String>,
347 delivered_reminder_ids: HashSet<String>,
348}
349
350#[derive(Clone, Debug, Default)]
351pub struct HostBridgeInjectionState {
352 inner: Arc<Mutex<QueuedTranscriptInjections>>,
353}
354
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356pub enum PendingUserMessageMutationResult {
357 Mutated,
358 AlreadyRevoked,
359 AlreadyDelivered,
360 UnknownMessageId,
361}
362
363impl QueuedTranscriptInjection {
364 fn mode(&self) -> QueuedUserMessageMode {
365 match self {
366 Self::User(message) => message.mode,
367 Self::Reminder(reminder) => reminder.mode,
368 }
369 }
370
371 fn pending_json(&self, position: usize) -> serde_json::Value {
372 match self {
373 Self::User(message) => serde_json::json!({
374 "kind": "user",
375 "id": message.message_id,
376 "messageId": message.message_id,
377 "mode": message.mode.as_str(),
378 "position": position,
379 "content": message.transcript_content,
380 }),
381 Self::Reminder(reminder) => serde_json::json!({
382 "kind": "reminder",
383 "id": reminder.reminder.id,
384 "reminderId": reminder.reminder.id,
385 "mode": reminder.mode.as_str(),
386 "position": position,
387 "body": reminder.reminder.body,
388 "tags": reminder.reminder.tags,
389 "dedupeKey": reminder.reminder.dedupe_key,
390 "ttlTurns": reminder.reminder.ttl_turns,
391 "preserveOnCompact": reminder.reminder.preserve_on_compact,
392 "propagate": reminder.reminder.propagate.as_str(),
393 "roleHint": reminder.reminder.role_hint.as_str(),
394 "source": reminder.reminder.source.as_str(),
395 "firedAtTurn": reminder.reminder.fired_at_turn,
396 "originatingAgentId": reminder.reminder.originating_agent_id,
397 }),
398 }
399 }
400}
401
402#[derive(Clone, Copy, Debug, PartialEq, Eq)]
403pub enum PendingReminderMutationResult {
404 Mutated,
405 AlreadyRevoked,
406 AlreadyDelivered,
407 UnknownReminderId,
408}
409
410fn new_inject_message_id() -> String {
411 format!("msg_inj_{}", uuid::Uuid::now_v7().simple())
412}
413
414impl HostBridgeInjectionState {
415 pub fn new() -> Self {
416 Self::default()
417 }
418
419 pub async fn push_pending_user_message(
420 &self,
421 content: String,
422 transcript_content: serde_json::Value,
423 mode: &str,
424 ) -> String {
425 let message_id = new_inject_message_id();
426 self.inner
427 .lock()
428 .await
429 .queue
430 .push_back(QueuedTranscriptInjection::User(QueuedUserMessage {
431 message_id: message_id.clone(),
432 content,
433 transcript_content,
434 mode: QueuedUserMessageMode::from_str(mode),
435 }));
436 message_id
437 }
438
439 pub async fn revoke_pending_user_message(
440 &self,
441 message_id: &str,
442 ) -> PendingUserMessageMutationResult {
443 let mut state = self.inner.lock().await;
444 let mut retained = VecDeque::new();
445 let mut revoked = false;
446 while let Some(injection) = state.queue.pop_front() {
447 match &injection {
448 QueuedTranscriptInjection::User(message) if message.message_id == message_id => {
449 revoked = true;
450 }
451 _ => retained.push_back(injection),
452 }
453 }
454 state.queue = retained;
455 if revoked {
456 state
457 .revoked_user_message_ids
458 .insert(message_id.to_string());
459 return PendingUserMessageMutationResult::Mutated;
460 }
461 if state.revoked_user_message_ids.contains(message_id) {
462 PendingUserMessageMutationResult::AlreadyRevoked
463 } else if state.delivered_user_message_ids.contains(message_id) {
464 PendingUserMessageMutationResult::AlreadyDelivered
465 } else {
466 PendingUserMessageMutationResult::UnknownMessageId
467 }
468 }
469
470 pub async fn revoke_pending_reminder(
471 &self,
472 reminder_id: &str,
473 ) -> PendingReminderMutationResult {
474 let mut state = self.inner.lock().await;
475 let mut retained = VecDeque::new();
476 let mut revoked = false;
477 while let Some(injection) = state.queue.pop_front() {
478 match &injection {
479 QueuedTranscriptInjection::Reminder(reminder)
480 if reminder.reminder.id == reminder_id =>
481 {
482 revoked = true;
483 }
484 _ => retained.push_back(injection),
485 }
486 }
487 state.queue = retained;
488 if revoked {
489 state.revoked_reminder_ids.insert(reminder_id.to_string());
490 return PendingReminderMutationResult::Mutated;
491 }
492 if state.revoked_reminder_ids.contains(reminder_id) {
493 PendingReminderMutationResult::AlreadyRevoked
494 } else if state.delivered_reminder_ids.contains(reminder_id) {
495 PendingReminderMutationResult::AlreadyDelivered
496 } else {
497 PendingReminderMutationResult::UnknownReminderId
498 }
499 }
500
501 pub async fn replace_pending_user_message(
502 &self,
503 message_id: &str,
504 content: String,
505 transcript_content: serde_json::Value,
506 ) -> PendingUserMessageMutationResult {
507 let mut state = self.inner.lock().await;
508 for injection in &mut state.queue {
509 if let QueuedTranscriptInjection::User(message) = injection {
510 if message.message_id == message_id {
511 message.content = content;
512 message.transcript_content = transcript_content;
513 return PendingUserMessageMutationResult::Mutated;
514 }
515 }
516 }
517 if state.revoked_user_message_ids.contains(message_id) {
518 PendingUserMessageMutationResult::AlreadyRevoked
519 } else if state.delivered_user_message_ids.contains(message_id) {
520 PendingUserMessageMutationResult::AlreadyDelivered
521 } else {
522 PendingUserMessageMutationResult::UnknownMessageId
523 }
524 }
525
526 async fn push_session_reminder(&self, reminder: QueuedReminder) {
527 self.inner
528 .lock()
529 .await
530 .queue
531 .push_back(QueuedTranscriptInjection::Reminder(reminder));
532 }
533
534 pub async fn pending_injections_json(&self) -> serde_json::Value {
535 let state = self.inner.lock().await;
536 let injections = state
537 .queue
538 .iter()
539 .enumerate()
540 .map(|(position, injection)| injection.pending_json(position))
541 .collect::<Vec<_>>();
542 serde_json::json!({
543 "pendingCount": injections.len(),
544 "injections": injections,
545 })
546 }
547}
548
549fn reminder_unknown_option_error(message: impl AsRef<str>) -> String {
550 format!(
551 "{}: {}",
552 Code::ReminderUnknownOption.as_str(),
553 message.as_ref()
554 )
555}
556
557fn session_remind_shape_error(message: impl AsRef<str>) -> String {
558 format!(
559 "{}: {}",
560 Code::ReminderInvalidShape.as_str(),
561 message.as_ref()
562 )
563}
564
565fn reminder_unknown_propagate_error(message: impl AsRef<str>) -> String {
566 format!(
567 "{}: {}",
568 Code::ReminderUnknownPropagate.as_str(),
569 message.as_ref()
570 )
571}
572
573fn string_field(
574 map: &serde_json::Map<String, serde_json::Value>,
575 key: &str,
576 required: bool,
577) -> Result<Option<String>, String> {
578 match map.get(key) {
579 None | Some(serde_json::Value::Null) if required => Err(session_remind_shape_error(
580 format!("`{key}` must be a non-empty string"),
581 )),
582 None | Some(serde_json::Value::Null) => Ok(None),
583 Some(serde_json::Value::String(value)) if required && value.trim().is_empty() => Err(
584 session_remind_shape_error(format!("`{key}` must be a non-empty string")),
585 ),
586 Some(serde_json::Value::String(value)) => {
587 let trimmed = value.trim();
588 if trimmed.is_empty() {
589 Ok(None)
590 } else {
591 Ok(Some(trimmed.to_string()))
592 }
593 }
594 Some(other) => Err(session_remind_shape_error(format!(
595 "`{key}` must be a string, got {other}"
596 ))),
597 }
598}
599
600fn bool_field(
601 map: &serde_json::Map<String, serde_json::Value>,
602 key: &str,
603) -> Result<Option<bool>, String> {
604 match map.get(key) {
605 None | Some(serde_json::Value::Null) => Ok(None),
606 Some(serde_json::Value::Bool(value)) => Ok(Some(*value)),
607 Some(other) => Err(session_remind_shape_error(format!(
608 "`{key}` must be a bool, got {other}"
609 ))),
610 }
611}
612
613fn int_field(
614 map: &serde_json::Map<String, serde_json::Value>,
615 key: &str,
616) -> Result<Option<i64>, String> {
617 match map.get(key) {
618 None | Some(serde_json::Value::Null) => Ok(None),
619 Some(serde_json::Value::Number(value)) => {
620 let Some(value) = value.as_i64() else {
621 return Err(session_remind_shape_error(format!(
622 "`{key}` must be an integer"
623 )));
624 };
625 Ok(Some(value))
626 }
627 Some(other) => Err(session_remind_shape_error(format!(
628 "`{key}` must be an int, got {other}"
629 ))),
630 }
631}
632
633fn tags_field(map: &serde_json::Map<String, serde_json::Value>) -> Result<Vec<String>, String> {
634 let Some(value) = map.get("tags") else {
635 return Ok(Vec::new());
636 };
637 if value.is_null() {
638 return Ok(Vec::new());
639 }
640 let Some(values) = value.as_array() else {
641 return Err(session_remind_shape_error("`tags` must be a list"));
642 };
643 let mut tags = Vec::new();
644 for value in values {
645 let Some(tag) = value.as_str() else {
646 return Err(session_remind_shape_error(format!(
647 "`tags` entries must be strings, got {value}"
648 )));
649 };
650 let tag = tag.trim();
651 if tag.is_empty() {
652 return Err(session_remind_shape_error(
653 "`tags` entries must be non-empty strings",
654 ));
655 }
656 if !tags.iter().any(|existing| existing == tag) {
657 tags.push(tag.to_string());
658 }
659 }
660 Ok(tags)
661}
662
663fn session_remind_payload_from_value(
664 value: &serde_json::Value,
665) -> Result<crate::llm::helpers::SystemReminder, String> {
666 let Some(map) = value.as_object() else {
667 return Err(session_remind_shape_error(
668 "session/remind payload must be a reminder object",
669 ));
670 };
671 const ALLOWED: &[&str] = &[
672 "_meta",
673 "body",
674 "dedupe_key",
675 "fired_at_turn",
676 "id",
677 "preserve_on_compact",
678 "propagate",
679 "role_hint",
680 "source",
681 "tags",
682 "ttl_turns",
683 ];
684 let unknown = map
685 .keys()
686 .filter(|key| !ALLOWED.contains(&key.as_str()))
687 .map(String::as_str)
688 .collect::<Vec<_>>();
689 if !unknown.is_empty() {
690 if unknown.contains(&"content") {
691 return Err(session_remind_shape_error(
692 "session/remind expects reminder `body`, not user-message `content`",
693 ));
694 }
695 return Err(reminder_unknown_option_error(format!(
696 "unknown reminder option(s): {}",
697 unknown.join(", ")
698 )));
699 }
700 if let Some(meta) = map.get("_meta") {
701 if !meta.is_null() && !meta.is_object() {
702 return Err(session_remind_shape_error("`_meta` must be an object"));
703 }
704 }
705 let ttl_turns = int_field(map, "ttl_turns")?;
706 if let Some(value) = ttl_turns {
707 if value <= 0 {
708 return Err(session_remind_shape_error("`ttl_turns` must be > 0"));
709 }
710 }
711 let fired_at_turn = int_field(map, "fired_at_turn")?.unwrap_or(0);
712 if fired_at_turn < 0 {
713 return Err(session_remind_shape_error(
714 "`fired_at_turn` must be >= 0 when provided",
715 ));
716 }
717 match string_field(map, "source", false)?.as_deref() {
718 None | Some("bridge") => {}
719 Some(_) => {
720 return Err(session_remind_shape_error(
721 "`source` for session/remind must be bridge when provided",
722 ))
723 }
724 }
725 let propagate = match string_field(map, "propagate", false)?.as_deref() {
726 None => crate::llm::helpers::ReminderPropagate::Session,
727 Some("all") => crate::llm::helpers::ReminderPropagate::All,
728 Some("session") => crate::llm::helpers::ReminderPropagate::Session,
729 Some("none") => crate::llm::helpers::ReminderPropagate::None,
730 Some(_) => {
731 return Err(reminder_unknown_propagate_error(
732 "`propagate` must be one of all, session, or none",
733 ))
734 }
735 };
736 let role_hint = match string_field(map, "role_hint", false)?.as_deref() {
737 None => crate::llm::helpers::ReminderRoleHint::System,
738 Some("system") => crate::llm::helpers::ReminderRoleHint::System,
739 Some("developer") => crate::llm::helpers::ReminderRoleHint::Developer,
740 Some("user_block") => crate::llm::helpers::ReminderRoleHint::UserBlock,
741 Some("ephemeral_cache") => crate::llm::helpers::ReminderRoleHint::EphemeralCache,
742 Some(_) => {
743 return Err(session_remind_shape_error(
744 "`role_hint` must be one of system, developer, user_block, or ephemeral_cache",
745 ))
746 }
747 };
748 Ok(crate::llm::helpers::SystemReminder {
749 id: string_field(map, "id", false)?.unwrap_or_else(|| uuid::Uuid::now_v7().to_string()),
750 tags: tags_field(map)?,
751 dedupe_key: string_field(map, "dedupe_key", false)?,
752 ttl_turns,
753 preserve_on_compact: bool_field(map, "preserve_on_compact")?.unwrap_or(false),
754 propagate,
755 role_hint,
756 source: crate::llm::helpers::ReminderSource::Bridge,
757 body: string_field(map, "body", true)?.unwrap_or_default(),
758 fired_at_turn,
759 originating_agent_id: None,
760 })
761}
762
763fn handle_cancel_tool_call_notification(params: &serde_json::Value) {
773 let session_id = params
774 .get("sessionId")
775 .or_else(|| params.get("session_id"))
776 .and_then(|value| value.as_str())
777 .unwrap_or_default();
778 let call_id = params
779 .get("toolCallId")
780 .or_else(|| params.get("tool_call_id"))
781 .or_else(|| params.get("callId"))
782 .or_else(|| params.get("call_id"))
783 .and_then(|value| value.as_str())
784 .unwrap_or_default();
785 if call_id.is_empty() {
786 return;
787 }
788 let reason = params
789 .get("reason")
790 .and_then(|value| value.as_str())
791 .unwrap_or("host cancelled in-flight tool call")
792 .to_string();
793 let inject_reminder = params
794 .get("injectReminder")
795 .or_else(|| params.get("inject_reminder"))
796 .and_then(|value| value.as_bool())
797 .unwrap_or(true);
798 crate::tool_call_cancellations::cancel(session_id, call_id, reason, inject_reminder);
799}
800
801fn queued_session_remind_from_params(params: &serde_json::Value) -> Result<QueuedReminder, String> {
802 let mode = QueuedUserMessageMode::from_str(
803 params
804 .get("mode")
805 .and_then(|value| value.as_str())
806 .unwrap_or("audit_only"),
807 );
808 let reminder_value = if let Some(reminder) = params.get("reminder") {
809 reminder.clone()
810 } else {
811 let Some(params) = params.as_object() else {
812 return Err(session_remind_shape_error(
813 "session/remind params must be an object",
814 ));
815 };
816 let mut reminder = params.clone();
817 reminder.remove("mode");
818 reminder.remove("sessionId");
819 reminder.remove("session_id");
820 serde_json::Value::Object(reminder)
821 };
822 Ok(QueuedReminder {
823 reminder: session_remind_payload_from_value(&reminder_value)?,
824 mode,
825 })
826}
827
828#[allow(clippy::new_without_default)]
830impl HostBridge {
831 pub fn new() -> Self {
836 let pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>> =
837 Arc::new(Mutex::new(HashMap::new()));
838 let cancelled = Arc::new(AtomicBool::new(false));
839 let cancel_notify = Arc::new(Notify::new());
840 let disconnected = Arc::new(AtomicBool::new(false));
841 let queued_transcript_injections = HostBridgeInjectionState::default();
842 let resume_requested = Arc::new(AtomicBool::new(false));
843 let skills_reload_requested = Arc::new(AtomicBool::new(false));
844 let daemon_idle = Arc::new(AtomicBool::new(false));
845
846 let pending_clone = pending.clone();
848 let cancelled_clone = cancelled.clone();
849 let cancel_notify_clone = cancel_notify.clone();
850 let disconnected_clone = disconnected.clone();
851 let queued_clone = queued_transcript_injections.clone();
852 let resume_clone = resume_requested.clone();
853 let skills_reload_clone = skills_reload_requested.clone();
854 tokio::task::spawn_local(async move {
855 let stdin = tokio::io::stdin();
856 let reader = tokio::io::BufReader::new(stdin);
857 let mut lines = reader.lines();
858
859 while let Ok(Some(line)) = lines.next_line().await {
860 let line = line.trim().to_string();
861 if line.is_empty() {
862 continue;
863 }
864
865 let msg: serde_json::Value = match serde_json::from_str(&line) {
866 Ok(v) => v,
867 Err(_) => continue,
868 };
869
870 if msg.get("id").is_none() {
872 if let Some(method) = msg["method"].as_str() {
873 if method == "cancel" {
874 cancelled_clone.store(true, Ordering::SeqCst);
875 cancel_notify_clone.notify_waiters();
876 } else if method == "agent/resume" {
877 resume_clone.store(true, Ordering::SeqCst);
878 } else if method == "skills/update" {
879 skills_reload_clone.store(true, Ordering::SeqCst);
880 } else if method == "session/remind" {
881 let params = &msg["params"];
882 if let Ok(reminder) = queued_session_remind_from_params(params) {
883 queued_clone.push_session_reminder(reminder).await;
884 }
885 } else if method == "session/cancel_tool_call" {
886 handle_cancel_tool_call_notification(&msg["params"]);
887 }
888 }
889 continue;
890 }
891
892 if let Some(id) = msg["id"].as_u64() {
893 let mut pending = pending_clone.lock().await;
894 if let Some(sender) = pending.remove(&id) {
895 let _ = sender.send(msg);
896 }
897 }
898 }
899
900 disconnected_clone.store(true, Ordering::SeqCst);
904 let mut pending = pending_clone.lock().await;
905 pending.clear();
906 });
907
908 Self {
909 next_id: AtomicU64::new(1),
910 pending,
911 cancelled,
912 cancel_notify,
913 disconnected,
914 writer: stdout_writer(Arc::new(std::sync::Mutex::new(()))),
915 session_id: std::sync::Mutex::new(String::new()),
916 script_name: std::sync::Mutex::new(String::new()),
917 queued_transcript_injections,
918 resume_requested,
919 skills_reload_requested,
920 daemon_idle,
921 prompt_outcome: std::sync::Mutex::new(None),
922 visible_call_states: std::sync::Mutex::new(HashMap::new()),
923 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
924 in_process: None,
925 }
926 }
927
928 pub fn from_parts(
934 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
935 cancelled: Arc<AtomicBool>,
936 stdout_lock: Arc<std::sync::Mutex<()>>,
937 start_id: u64,
938 ) -> Self {
939 Self::from_parts_with_writer(pending, cancelled, stdout_writer(stdout_lock), start_id)
940 }
941
942 pub fn from_parts_with_writer(
943 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
944 cancelled: Arc<AtomicBool>,
945 writer: HostBridgeWriter,
946 start_id: u64,
947 ) -> Self {
948 Self::from_parts_with_writer_and_cancel_notify(
949 pending,
950 cancelled,
951 Arc::new(Notify::new()),
952 writer,
953 start_id,
954 )
955 }
956
957 pub fn from_parts_with_writer_and_cancel_notify(
958 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
959 cancelled: Arc<AtomicBool>,
960 cancel_notify: Arc<Notify>,
961 writer: HostBridgeWriter,
962 start_id: u64,
963 ) -> Self {
964 Self::from_parts_with_writer_cancel_notify_and_injection_state(
965 pending,
966 cancelled,
967 cancel_notify,
968 writer,
969 start_id,
970 None,
971 )
972 }
973
974 pub fn from_parts_with_writer_cancel_notify_and_injection_state(
975 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
976 cancelled: Arc<AtomicBool>,
977 cancel_notify: Arc<Notify>,
978 writer: HostBridgeWriter,
979 start_id: u64,
980 injection_state: Option<HostBridgeInjectionState>,
981 ) -> Self {
982 Self {
983 next_id: AtomicU64::new(start_id),
984 pending,
985 cancelled,
986 cancel_notify,
987 disconnected: Arc::new(AtomicBool::new(false)),
988 writer,
989 session_id: std::sync::Mutex::new(String::new()),
990 script_name: std::sync::Mutex::new(String::new()),
991 queued_transcript_injections: injection_state.unwrap_or_default(),
992 resume_requested: Arc::new(AtomicBool::new(false)),
993 skills_reload_requested: Arc::new(AtomicBool::new(false)),
994 daemon_idle: Arc::new(AtomicBool::new(false)),
995 prompt_outcome: std::sync::Mutex::new(None),
996 visible_call_states: std::sync::Mutex::new(HashMap::new()),
997 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
998 in_process: None,
999 }
1000 }
1001
1002 pub async fn from_harn_module(mut vm: Vm, module_path: &Path) -> Result<Self, VmError> {
1005 let exported_functions = vm.load_module_exports(module_path).await?;
1006 Ok(Self {
1007 next_id: AtomicU64::new(1),
1008 pending: Arc::new(Mutex::new(HashMap::new())),
1009 cancelled: Arc::new(AtomicBool::new(false)),
1010 cancel_notify: Arc::new(Notify::new()),
1011 disconnected: Arc::new(AtomicBool::new(false)),
1012 writer: stdout_writer(Arc::new(std::sync::Mutex::new(()))),
1013 session_id: std::sync::Mutex::new(String::new()),
1014 script_name: std::sync::Mutex::new(String::new()),
1015 queued_transcript_injections: HostBridgeInjectionState::default(),
1016 resume_requested: Arc::new(AtomicBool::new(false)),
1017 skills_reload_requested: Arc::new(AtomicBool::new(false)),
1018 daemon_idle: Arc::new(AtomicBool::new(false)),
1019 prompt_outcome: std::sync::Mutex::new(None),
1020 visible_call_states: std::sync::Mutex::new(HashMap::new()),
1021 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
1022 in_process: Some(InProcessHost {
1023 module_path: module_path.to_path_buf(),
1024 exported_functions,
1025 vm,
1026 }),
1027 })
1028 }
1029
1030 pub fn set_session_id(&self, id: &str) {
1032 *self.session_id.lock().unwrap_or_else(|e| e.into_inner()) = id.to_string();
1033 }
1034
1035 pub fn set_script_name(&self, name: &str) {
1037 *self.script_name.lock().unwrap_or_else(|e| e.into_inner()) = name.to_string();
1038 }
1039
1040 fn get_script_name(&self) -> String {
1042 self.script_name
1043 .lock()
1044 .unwrap_or_else(|e| e.into_inner())
1045 .clone()
1046 }
1047
1048 pub fn get_session_id(&self) -> String {
1050 self.session_id
1051 .lock()
1052 .unwrap_or_else(|e| e.into_inner())
1053 .clone()
1054 }
1055
1056 fn write_line(&self, line: &str) -> Result<(), VmError> {
1058 (self.writer)(line).map_err(VmError::Runtime)
1059 }
1060
1061 pub async fn call(
1066 &self,
1067 method: &str,
1068 params: serde_json::Value,
1069 ) -> Result<serde_json::Value, VmError> {
1070 if let Some(in_process) = &self.in_process {
1071 return in_process.dispatch(method, params).await;
1072 }
1073
1074 if self.is_cancelled() {
1075 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1076 }
1077
1078 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
1079 let cancel_wait = self.cancel_notify.notified();
1080 tokio::pin!(cancel_wait);
1081 cancel_wait.as_mut().enable();
1085
1086 let request = crate::jsonrpc::request(id, method, params);
1087
1088 let (tx, rx) = oneshot::channel();
1089 {
1090 let mut pending = self.pending.lock().await;
1091 if self.disconnected.load(Ordering::SeqCst) {
1092 return Err(VmError::Runtime(
1093 "Bridge: host connection is already closed".into(),
1094 ));
1095 }
1096 pending.insert(id, tx);
1097 }
1098
1099 let line = serde_json::to_string(&request)
1100 .map_err(|e| VmError::Runtime(format!("Bridge serialization error: {e}")))?;
1101 if let Err(e) = self.write_line(&line) {
1102 let mut pending = self.pending.lock().await;
1103 pending.remove(&id);
1104 return Err(e);
1105 }
1106
1107 if self.is_cancelled() {
1108 let mut pending = self.pending.lock().await;
1109 pending.remove(&id);
1110 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1111 }
1112
1113 let timeout_wait = wait_for_bridge_call_timeout(method);
1114 tokio::pin!(timeout_wait);
1115
1116 let response = tokio::select! {
1117 result = rx => match result {
1118 Ok(msg) => msg,
1119 Err(_) => {
1120 return Err(VmError::Runtime(
1122 "Bridge: host closed connection before responding".into(),
1123 ));
1124 }
1125 },
1126 _ = &mut cancel_wait => {
1127 let mut pending = self.pending.lock().await;
1128 pending.remove(&id);
1129 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1130 }
1131 timeout = &mut timeout_wait => {
1132 let mut pending = self.pending.lock().await;
1133 pending.remove(&id);
1134 return Err(VmError::Runtime(format!(
1135 "Bridge: host did not respond to '{method}' within {}s",
1136 timeout.as_secs()
1137 )));
1138 }
1139 };
1140
1141 if let Some(error) = response.get("error") {
1142 let message = error["message"].as_str().unwrap_or("Unknown host error");
1143 let code = error["code"].as_i64().unwrap_or(-1);
1144 if code == -32001 {
1146 return Err(VmError::CategorizedError {
1147 message: message.to_string(),
1148 category: ErrorCategory::ToolRejected,
1149 });
1150 }
1151 return Err(VmError::Runtime(format!("Host error ({code}): {message}")));
1152 }
1153
1154 Ok(response["result"].clone())
1155 }
1156
1157 pub fn notify(&self, method: &str, params: serde_json::Value) {
1160 let notification = crate::jsonrpc::notification(method, params);
1161 if self.in_process.is_some() {
1162 return;
1163 }
1164 if let Ok(line) = serde_json::to_string(¬ification) {
1165 let _ = self.write_line(&line);
1166 }
1167 }
1168
1169 pub fn is_cancelled(&self) -> bool {
1171 self.cancelled.load(Ordering::SeqCst)
1172 }
1173 pub fn cancelled_flag(&self) -> Arc<AtomicBool> {
1174 self.cancelled.clone()
1175 }
1176 pub fn take_resume_signal(&self) -> bool {
1177 self.resume_requested.swap(false, Ordering::SeqCst)
1178 }
1179 pub fn signal_resume(&self) {
1180 self.resume_requested.store(true, Ordering::SeqCst);
1181 }
1182 pub fn set_daemon_idle(&self, idle: bool) {
1183 self.daemon_idle.store(idle, Ordering::SeqCst);
1184 }
1185
1186 pub fn is_daemon_idle(&self) -> bool {
1187 self.daemon_idle.load(Ordering::SeqCst)
1188 }
1189
1190 pub fn set_prompt_outcome(
1194 &self,
1195 stop_reason: &str,
1196 terminal: &crate::agent_events::AgentTerminalOutcome,
1197 ) {
1198 *self
1199 .prompt_outcome
1200 .lock()
1201 .unwrap_or_else(|e| e.into_inner()) = Some((stop_reason.to_string(), terminal.clone()));
1202 }
1203
1204 pub fn take_prompt_outcome(
1207 &self,
1208 ) -> Option<(String, crate::agent_events::AgentTerminalOutcome)> {
1209 self.prompt_outcome
1210 .lock()
1211 .unwrap_or_else(|e| e.into_inner())
1212 .take()
1213 }
1214
1215 pub fn take_skills_reload_signal(&self) -> bool {
1220 self.skills_reload_requested.swap(false, Ordering::SeqCst)
1221 }
1222
1223 pub fn signal_skills_reload(&self) {
1227 self.skills_reload_requested.store(true, Ordering::SeqCst);
1228 }
1229
1230 pub async fn list_host_skills(&self) -> Result<Vec<serde_json::Value>, VmError> {
1236 let result = self.call("skills/list", serde_json::json!({})).await?;
1237 match result {
1238 serde_json::Value::Array(items) => Ok(items),
1239 serde_json::Value::Object(map) => match map.get("skills") {
1240 Some(serde_json::Value::Array(items)) => Ok(items.clone()),
1241 _ => Err(VmError::Runtime(
1242 "skills/list: host response must be an array or { skills: [...] }".into(),
1243 )),
1244 },
1245 _ => Err(VmError::Runtime(
1246 "skills/list: unexpected response shape".into(),
1247 )),
1248 }
1249 }
1250
1251 pub async fn list_host_tools(&self) -> Result<Vec<serde_json::Value>, VmError> {
1257 let result = self.call("host/tools/list", serde_json::json!({})).await?;
1258 parse_host_tools_list_response(result)
1259 }
1260
1261 pub async fn fetch_host_skill(&self, id: &str) -> Result<serde_json::Value, VmError> {
1265 self.call("skills/fetch", serde_json::json!({ "id": id }))
1266 .await
1267 }
1268
1269 pub fn injection_state(&self) -> HostBridgeInjectionState {
1270 self.queued_transcript_injections.clone()
1271 }
1272
1273 pub async fn push_pending_user_message(
1274 &self,
1275 content: String,
1276 transcript_content: serde_json::Value,
1277 mode: &str,
1278 ) -> String {
1279 self.queued_transcript_injections
1280 .push_pending_user_message(content, transcript_content, mode)
1281 .await
1282 }
1283
1284 pub async fn push_queued_user_message(&self, content: String, mode: &str) -> String {
1285 self.push_pending_user_message(content.clone(), serde_json::Value::String(content), mode)
1286 .await
1287 }
1288
1289 pub async fn revoke_pending_user_message(
1290 &self,
1291 message_id: &str,
1292 ) -> PendingUserMessageMutationResult {
1293 self.queued_transcript_injections
1294 .revoke_pending_user_message(message_id)
1295 .await
1296 }
1297
1298 pub async fn revoke_pending_reminder(
1299 &self,
1300 reminder_id: &str,
1301 ) -> PendingReminderMutationResult {
1302 self.queued_transcript_injections
1303 .revoke_pending_reminder(reminder_id)
1304 .await
1305 }
1306
1307 pub async fn pending_injections_json(&self) -> serde_json::Value {
1308 self.queued_transcript_injections
1309 .pending_injections_json()
1310 .await
1311 }
1312
1313 pub async fn replace_pending_user_message(
1314 &self,
1315 message_id: &str,
1316 content: String,
1317 transcript_content: serde_json::Value,
1318 ) -> PendingUserMessageMutationResult {
1319 self.queued_transcript_injections
1320 .replace_pending_user_message(message_id, content, transcript_content)
1321 .await
1322 }
1323
1324 pub async fn push_queued_session_remind_from_params(
1325 &self,
1326 params: &serde_json::Value,
1327 ) -> Result<String, String> {
1328 let reminder = queued_session_remind_from_params(params)?;
1329 let reminder_id = reminder.reminder.id.clone();
1330 self.queued_transcript_injections
1331 .push_session_reminder(reminder)
1332 .await;
1333 Ok(reminder_id)
1334 }
1335
1336 pub async fn take_queued_user_messages(
1337 &self,
1338 include_interrupt_immediate: bool,
1339 include_finish_step: bool,
1340 include_audit_only: bool,
1341 ) -> Vec<QueuedUserMessage> {
1342 let mut state = self.queued_transcript_injections.inner.lock().await;
1343 let mut selected = Vec::new();
1344 let mut retained = VecDeque::new();
1345 while let Some(injection) = state.queue.pop_front() {
1346 let should_take = match injection.mode() {
1347 QueuedUserMessageMode::InterruptImmediate => include_interrupt_immediate,
1348 QueuedUserMessageMode::FinishStep => include_finish_step,
1349 QueuedUserMessageMode::AuditOnly => include_audit_only,
1350 };
1351 match (should_take, injection) {
1352 (true, QueuedTranscriptInjection::User(message)) => {
1353 state
1354 .delivered_user_message_ids
1355 .insert(message.message_id.clone());
1356 selected.push(message);
1357 }
1358 (_, injection) => retained.push_back(injection),
1359 }
1360 }
1361 state.queue = retained;
1362 selected
1363 }
1364
1365 pub async fn take_queued_transcript_injections(
1366 &self,
1367 include_interrupt_immediate: bool,
1368 include_finish_step: bool,
1369 include_audit_only: bool,
1370 ) -> Vec<QueuedTranscriptInjection> {
1371 let mut state = self.queued_transcript_injections.inner.lock().await;
1372 let mut selected = Vec::new();
1373 let mut retained = VecDeque::new();
1374 while let Some(injection) = state.queue.pop_front() {
1375 let should_take = match injection.mode() {
1376 QueuedUserMessageMode::InterruptImmediate => include_interrupt_immediate,
1377 QueuedUserMessageMode::FinishStep => include_finish_step,
1378 QueuedUserMessageMode::AuditOnly => include_audit_only,
1379 };
1380 if should_take {
1381 match &injection {
1382 QueuedTranscriptInjection::User(message) => {
1383 state
1384 .delivered_user_message_ids
1385 .insert(message.message_id.clone());
1386 }
1387 QueuedTranscriptInjection::Reminder(reminder) => {
1388 state
1389 .delivered_reminder_ids
1390 .insert(reminder.reminder.id.clone());
1391 }
1392 }
1393 selected.push(injection);
1394 } else {
1395 retained.push_back(injection);
1396 }
1397 }
1398 state.queue = retained;
1399 selected
1400 }
1401
1402 pub async fn take_queued_user_messages_for(
1403 &self,
1404 checkpoint: DeliveryCheckpoint,
1405 ) -> Vec<QueuedUserMessage> {
1406 match checkpoint {
1407 DeliveryCheckpoint::InterruptImmediate => {
1408 self.take_queued_user_messages(true, false, false).await
1409 }
1410 DeliveryCheckpoint::AfterCurrentOperation => {
1411 self.take_queued_user_messages(false, true, false).await
1412 }
1413 DeliveryCheckpoint::EndOfInteraction => {
1414 self.take_queued_user_messages(false, false, true).await
1415 }
1416 }
1417 }
1418
1419 pub async fn take_queued_transcript_injections_for(
1420 &self,
1421 checkpoint: DeliveryCheckpoint,
1422 ) -> Vec<QueuedTranscriptInjection> {
1423 match checkpoint {
1424 DeliveryCheckpoint::InterruptImmediate => {
1425 self.take_queued_transcript_injections(true, false, false)
1426 .await
1427 }
1428 DeliveryCheckpoint::AfterCurrentOperation => {
1429 self.take_queued_transcript_injections(false, true, false)
1430 .await
1431 }
1432 DeliveryCheckpoint::EndOfInteraction => {
1433 self.take_queued_transcript_injections(false, false, true)
1434 .await
1435 }
1436 }
1437 }
1438
1439 pub fn send_output(&self, text: &str) {
1441 self.notify("output", serde_json::json!({"text": text}));
1442 }
1443
1444 pub fn send_progress(
1446 &self,
1447 phase: &str,
1448 message: &str,
1449 progress: Option<i64>,
1450 total: Option<i64>,
1451 data: Option<serde_json::Value>,
1452 ) {
1453 let mut payload = serde_json::json!({"phase": phase, "message": message});
1454 if let Some(p) = progress {
1455 payload["progress"] = serde_json::json!(p);
1456 }
1457 if let Some(t) = total {
1458 payload["total"] = serde_json::json!(t);
1459 }
1460 if let Some(d) = data {
1461 payload["data"] = d;
1462 }
1463 self.notify("progress", payload);
1464 }
1465
1466 pub fn send_log(&self, level: &str, message: &str, fields: Option<serde_json::Value>) {
1468 let mut payload = serde_json::json!({"level": level, "message": message});
1469 if let Some(f) = fields {
1470 payload["fields"] = f;
1471 }
1472 self.notify("log", payload);
1473 }
1474
1475 pub fn send_call_start(
1478 &self,
1479 call_id: &str,
1480 call_type: &str,
1481 name: &str,
1482 metadata: serde_json::Value,
1483 ) {
1484 let session_id = self.get_session_id();
1485 let script = self.get_script_name();
1486 let stream_publicly = metadata
1487 .get("stream_publicly")
1488 .and_then(|value| value.as_bool())
1489 .unwrap_or(true);
1490 self.visible_call_streams
1491 .lock()
1492 .unwrap_or_else(|e| e.into_inner())
1493 .insert(call_id.to_string(), stream_publicly);
1494 self.notify(
1495 "session/update",
1496 serde_json::json!({
1497 "sessionId": session_id,
1498 "update": {
1499 "sessionUpdate": "call_start",
1500 "content": {
1501 "toolCallId": call_id,
1502 "call_type": call_type,
1503 "name": name,
1504 "script": script,
1505 "metadata": metadata,
1506 },
1507 },
1508 }),
1509 );
1510 }
1511
1512 pub fn send_call_progress(
1515 &self,
1516 call_id: &str,
1517 delta: &str,
1518 accumulated_tokens: u64,
1519 user_visible: bool,
1520 ) {
1521 let session_id = self.get_session_id();
1522 let (visible_text, visible_delta) = {
1523 let stream_publicly = self
1524 .visible_call_streams
1525 .lock()
1526 .unwrap_or_else(|e| e.into_inner())
1527 .get(call_id)
1528 .copied()
1529 .unwrap_or(true);
1530 if !user_visible || !stream_publicly {
1531 (String::new(), String::new())
1532 } else {
1533 let mut states = self
1534 .visible_call_states
1535 .lock()
1536 .unwrap_or_else(|e| e.into_inner());
1537 let state = states.entry(call_id.to_string()).or_default();
1538 state.push(delta, true)
1539 }
1540 };
1541 self.notify(
1542 "session/update",
1543 serde_json::json!({
1544 "sessionId": session_id,
1545 "update": {
1546 "sessionUpdate": "call_progress",
1547 "content": {
1548 "toolCallId": call_id,
1549 "delta": delta,
1550 "accumulated_tokens": accumulated_tokens,
1551 "visible_text": visible_text,
1552 "visible_delta": visible_delta,
1553 "user_visible": user_visible,
1554 },
1555 },
1556 }),
1557 );
1558 }
1559
1560 pub fn send_call_end(
1562 &self,
1563 call_id: &str,
1564 call_type: &str,
1565 name: &str,
1566 duration_ms: u64,
1567 status: &str,
1568 metadata: serde_json::Value,
1569 ) {
1570 let session_id = self.get_session_id();
1571 let script = self.get_script_name();
1572 self.visible_call_states
1573 .lock()
1574 .unwrap_or_else(|e| e.into_inner())
1575 .remove(call_id);
1576 self.visible_call_streams
1577 .lock()
1578 .unwrap_or_else(|e| e.into_inner())
1579 .remove(call_id);
1580 self.notify(
1581 "session/update",
1582 serde_json::json!({
1583 "sessionId": session_id,
1584 "update": {
1585 "sessionUpdate": "call_end",
1586 "content": {
1587 "toolCallId": call_id,
1588 "call_type": call_type,
1589 "name": name,
1590 "script": script,
1591 "duration_ms": duration_ms,
1592 "status": status,
1593 "metadata": metadata,
1594 },
1595 },
1596 }),
1597 );
1598 }
1599
1600 pub fn send_worker_update(
1602 &self,
1603 worker_id: &str,
1604 worker_name: &str,
1605 status: &str,
1606 metadata: serde_json::Value,
1607 audit: Option<&MutationSessionRecord>,
1608 ) {
1609 let session_id = self.get_session_id();
1610 let script = self.get_script_name();
1611 let started_at = metadata.get("started_at").cloned().unwrap_or_default();
1612 let finished_at = metadata.get("finished_at").cloned().unwrap_or_default();
1613 let snapshot_path = metadata.get("snapshot_path").cloned().unwrap_or_default();
1614 let run_id = metadata.get("child_run_id").cloned().unwrap_or_default();
1615 let run_path = metadata.get("child_run_path").cloned().unwrap_or_default();
1616 let lifecycle = serde_json::json!({
1617 "event": status,
1618 "worker_id": worker_id,
1619 "worker_name": worker_name,
1620 "started_at": started_at,
1621 "finished_at": finished_at,
1622 });
1623 self.notify(
1624 "session/update",
1625 serde_json::json!({
1626 "sessionId": session_id,
1627 "update": {
1628 "sessionUpdate": "worker_update",
1629 "content": {
1630 "worker_id": worker_id,
1631 "worker_name": worker_name,
1632 "status": status,
1633 "script": script,
1634 "started_at": started_at,
1635 "finished_at": finished_at,
1636 "snapshot_path": snapshot_path,
1637 "run_id": run_id,
1638 "run_path": run_path,
1639 "lifecycle": lifecycle,
1640 "audit": audit,
1641 "metadata": metadata,
1642 },
1643 },
1644 }),
1645 );
1646 }
1647}
1648
1649pub fn json_result_to_vm_value(val: &serde_json::Value) -> VmValue {
1651 crate::stdlib::json_to_vm_value(val)
1652}
1653
1654fn parse_host_tools_list_response(
1655 result: serde_json::Value,
1656) -> Result<Vec<serde_json::Value>, VmError> {
1657 let tools = match result {
1658 serde_json::Value::Array(items) => items,
1659 serde_json::Value::Object(map) => match map.get("tools").cloned().or_else(|| {
1660 map.get("result")
1661 .and_then(|value| value.get("tools"))
1662 .cloned()
1663 }) {
1664 Some(serde_json::Value::Array(items)) => items,
1665 _ => {
1666 return Err(VmError::Runtime(
1667 "host/tools/list: host response must be an array or { tools: [...] }".into(),
1668 ));
1669 }
1670 },
1671 _ => {
1672 return Err(VmError::Runtime(
1673 "host/tools/list: unexpected response shape".into(),
1674 ));
1675 }
1676 };
1677
1678 let mut normalized = Vec::with_capacity(tools.len());
1679 for tool in tools {
1680 let serde_json::Value::Object(map) = tool else {
1681 return Err(VmError::Runtime(
1682 "host/tools/list: every tool must be an object".into(),
1683 ));
1684 };
1685 let Some(name) = map.get("name").and_then(|value| value.as_str()) else {
1686 return Err(VmError::Runtime(
1687 "host/tools/list: every tool must include a string `name`".into(),
1688 ));
1689 };
1690 let description = map
1691 .get("description")
1692 .and_then(|value| value.as_str())
1693 .or_else(|| {
1694 map.get("short_description")
1695 .and_then(|value| value.as_str())
1696 })
1697 .unwrap_or_default();
1698 let schema = map
1699 .get("schema")
1700 .cloned()
1701 .or_else(|| map.get("parameters").cloned())
1702 .or_else(|| map.get("input_schema").cloned())
1703 .unwrap_or(serde_json::Value::Null);
1704 let deprecated = map
1705 .get("deprecated")
1706 .and_then(|value| value.as_bool())
1707 .unwrap_or(false);
1708 normalized.push(serde_json::json!({
1709 "name": name,
1710 "description": description,
1711 "schema": schema,
1712 "deprecated": deprecated,
1713 }));
1714 }
1715 Ok(normalized)
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720 use super::*;
1721
1722 fn test_bridge() -> HostBridge {
1723 HostBridge::from_parts(
1724 Arc::new(Mutex::new(HashMap::new())),
1725 Arc::new(AtomicBool::new(false)),
1726 Arc::new(std::sync::Mutex::new(())),
1727 1,
1728 )
1729 }
1730
1731 fn test_bridge_sharing_injection_state(owner: &HostBridge) -> HostBridge {
1732 HostBridge::from_parts_with_writer_cancel_notify_and_injection_state(
1733 Arc::new(Mutex::new(HashMap::new())),
1734 Arc::new(AtomicBool::new(false)),
1735 Arc::new(Notify::new()),
1736 Arc::new(|_| Ok(())),
1737 100,
1738 Some(owner.injection_state()),
1739 )
1740 }
1741
1742 #[test]
1743 fn test_json_rpc_request_format() {
1744 let request = crate::jsonrpc::request(
1745 1,
1746 "llm_call",
1747 serde_json::json!({
1748 "prompt": "Hello",
1749 "system": "Be helpful",
1750 }),
1751 );
1752 let s = serde_json::to_string(&request).unwrap();
1753 assert!(s.contains("\"jsonrpc\":\"2.0\""));
1754 assert!(s.contains("\"id\":1"));
1755 assert!(s.contains("\"method\":\"llm_call\""));
1756 }
1757
1758 #[test]
1759 fn test_json_rpc_notification_format() {
1760 let notification =
1761 crate::jsonrpc::notification("output", serde_json::json!({"text": "[harn] hello\n"}));
1762 let s = serde_json::to_string(¬ification).unwrap();
1763 assert!(s.contains("\"method\":\"output\""));
1764 assert!(!s.contains("\"id\""));
1765 }
1766
1767 #[test]
1768 fn test_json_rpc_error_response_parsing() {
1769 let response = crate::jsonrpc::error_response(1, -32600, "Invalid request");
1770 assert!(response.get("error").is_some());
1771 assert_eq!(
1772 response["error"]["message"].as_str().unwrap(),
1773 "Invalid request"
1774 );
1775 }
1776
1777 #[test]
1778 fn test_json_rpc_success_response_parsing() {
1779 let response = crate::jsonrpc::response(
1780 1,
1781 serde_json::json!({
1782 "text": "Hello world",
1783 "input_tokens": 10,
1784 "output_tokens": 5,
1785 }),
1786 );
1787 assert!(response.get("result").is_some());
1788 assert_eq!(response["result"]["text"].as_str().unwrap(), "Hello world");
1789 }
1790
1791 #[test]
1792 fn test_cancelled_flag() {
1793 let cancelled = Arc::new(AtomicBool::new(false));
1794 assert!(!cancelled.load(Ordering::SeqCst));
1795 cancelled.store(true, Ordering::SeqCst);
1796 assert!(cancelled.load(Ordering::SeqCst));
1797 }
1798
1799 #[tokio::test(flavor = "current_thread", start_paused = true)]
1800 async fn pending_permission_calls_return_when_cancellation_arrives() {
1801 let pending = Arc::new(Mutex::new(HashMap::new()));
1802 let cancelled = Arc::new(AtomicBool::new(false));
1803 let bridge = HostBridge::from_parts_with_writer(
1804 pending.clone(),
1805 cancelled.clone(),
1806 Arc::new(|_| Ok(())),
1807 1,
1808 );
1809
1810 let call = bridge.call(
1811 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION,
1812 serde_json::json!({}),
1813 );
1814 tokio::pin!(call);
1815 wait_for_pending(&pending, 1, call.as_mut()).await;
1816
1817 cancelled.store(true, Ordering::SeqCst);
1818 bridge.cancel_notify.notify_waiters();
1819
1820 let result = tokio::time::timeout(Duration::from_secs(1), call)
1821 .await
1822 .expect("pending permission call should observe cancellation");
1823 assert!(matches!(
1824 result,
1825 Err(VmError::Runtime(message)) if message.contains("cancelled")
1826 ));
1827 assert!(pending.lock().await.is_empty());
1828 }
1829
1830 #[tokio::test(flavor = "current_thread", start_paused = true)]
1831 async fn registered_cancel_wait_survives_notification_before_first_poll() {
1832 let notify = Notify::new();
1833 let wait = notify.notified();
1834 tokio::pin!(wait);
1835 wait.as_mut().enable();
1836
1837 notify.notify_waiters();
1838
1839 tokio::select! {
1840 () = &mut wait => {}
1841 _ = tokio::task::yield_now() => panic!("registered cancellation notification was lost"),
1842 }
1843 }
1844
1845 #[tokio::test(flavor = "current_thread", start_paused = true)]
1846 async fn bridge_call_cannot_register_after_disconnect_clear() {
1847 let pending = Arc::new(Mutex::new(HashMap::new()));
1848 let bridge = HostBridge::from_parts_with_writer(
1849 pending.clone(),
1850 Arc::new(AtomicBool::new(false)),
1851 Arc::new(|_| Ok(())),
1852 1,
1853 );
1854 let guard = pending.lock().await;
1855 let call = bridge.call(
1856 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION,
1857 serde_json::json!({}),
1858 );
1859 tokio::pin!(call);
1860 tokio::select! {
1861 result = &mut call => panic!("call bypassed pending lock: {result:?}"),
1862 _ = tokio::task::yield_now() => {}
1863 }
1864
1865 bridge.disconnected.store(true, Ordering::SeqCst);
1866 drop(guard);
1867
1868 let result = call.await;
1869 assert!(matches!(
1870 result,
1871 Err(VmError::Runtime(message)) if message.contains("already closed")
1872 ));
1873 assert!(pending.lock().await.is_empty());
1874 }
1875
1876 #[test]
1877 fn call_progress_hides_non_user_visible_deltas() {
1878 let lines = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1879 let captured = lines.clone();
1880 let bridge = HostBridge::from_parts_with_writer(
1881 Arc::new(Mutex::new(HashMap::new())),
1882 Arc::new(AtomicBool::new(false)),
1883 Arc::new(move |line| {
1884 captured
1885 .lock()
1886 .unwrap_or_else(|e| e.into_inner())
1887 .push(line.to_string());
1888 Ok(())
1889 }),
1890 1,
1891 );
1892
1893 bridge.send_call_start(
1894 "call-1",
1895 "llm",
1896 "llm_call",
1897 serde_json::json!({"stream_publicly": true}),
1898 );
1899 bridge.send_call_progress(
1900 "call-1",
1901 r#"{"verdict":"done","reasoning":"internal"}"#,
1902 1,
1903 false,
1904 );
1905
1906 let lines = lines.lock().unwrap_or_else(|e| e.into_inner());
1907 let progress: serde_json::Value =
1908 serde_json::from_str(&lines[1]).expect("call_progress notification json");
1909 let content = &progress["params"]["update"]["content"];
1910 assert_eq!(
1911 content["delta"],
1912 r#"{"verdict":"done","reasoning":"internal"}"#
1913 );
1914 assert_eq!(content["user_visible"], false);
1915 assert_eq!(content["visible_text"], "");
1916 assert_eq!(content["visible_delta"], "");
1917 }
1918
1919 #[test]
1920 fn call_progress_hides_non_public_streams() {
1921 let lines = Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
1922 let captured = lines.clone();
1923 let bridge = HostBridge::from_parts_with_writer(
1924 Arc::new(Mutex::new(HashMap::new())),
1925 Arc::new(AtomicBool::new(false)),
1926 Arc::new(move |line| {
1927 captured
1928 .lock()
1929 .unwrap_or_else(|e| e.into_inner())
1930 .push(line.to_string());
1931 Ok(())
1932 }),
1933 1,
1934 );
1935
1936 bridge.send_call_start(
1937 "call-1",
1938 "llm",
1939 "llm_call",
1940 serde_json::json!({"stream_publicly": false}),
1941 );
1942 bridge.send_call_progress("call-1", "secret schema bytes", 1, true);
1943
1944 let lines = lines.lock().unwrap_or_else(|e| e.into_inner());
1945 let progress: serde_json::Value =
1946 serde_json::from_str(&lines[1]).expect("call_progress notification json");
1947 let content = &progress["params"]["update"]["content"];
1948 assert_eq!(content["delta"], "secret schema bytes");
1949 assert_eq!(content["user_visible"], true);
1950 assert_eq!(content["visible_text"], "");
1951 assert_eq!(content["visible_delta"], "");
1952 }
1953
1954 #[test]
1955 fn queued_messages_are_filtered_by_delivery_mode() {
1956 let runtime = tokio::runtime::Builder::new_current_thread()
1957 .enable_all()
1958 .build()
1959 .unwrap();
1960 runtime.block_on(async {
1961 let bridge = test_bridge();
1962 bridge
1963 .push_queued_user_message("first".to_string(), "finish_step")
1964 .await;
1965 bridge
1966 .push_queued_user_message("second".to_string(), "audit_only")
1967 .await;
1968
1969 let finish_step = bridge.take_queued_user_messages(false, true, false).await;
1970 assert_eq!(finish_step.len(), 1);
1971 assert_eq!(finish_step[0].content, "first");
1972
1973 let audit_only = bridge.take_queued_user_messages(false, false, true).await;
1974 assert_eq!(audit_only.len(), 1);
1975 assert_eq!(audit_only[0].content, "second");
1976 });
1977 }
1978
1979 #[test]
1980 fn pending_user_messages_support_revoke_replace_and_delivery_states() {
1981 let runtime = tokio::runtime::Builder::new_current_thread()
1982 .enable_all()
1983 .build()
1984 .unwrap();
1985 runtime.block_on(async {
1986 let bridge = test_bridge();
1987 let first_id = bridge
1988 .push_pending_user_message(
1989 "first".to_string(),
1990 serde_json::json!("first"),
1991 "audit_only",
1992 )
1993 .await;
1994 let second_id = bridge
1995 .push_pending_user_message(
1996 "second".to_string(),
1997 serde_json::json!("second"),
1998 "audit_only",
1999 )
2000 .await;
2001
2002 assert_eq!(
2003 bridge
2004 .replace_pending_user_message(
2005 &second_id,
2006 "second edited".to_string(),
2007 serde_json::json!("second edited"),
2008 )
2009 .await,
2010 PendingUserMessageMutationResult::Mutated
2011 );
2012 assert_eq!(
2013 bridge.revoke_pending_user_message(&first_id).await,
2014 PendingUserMessageMutationResult::Mutated
2015 );
2016 assert_eq!(
2017 bridge.revoke_pending_user_message(&first_id).await,
2018 PendingUserMessageMutationResult::AlreadyRevoked
2019 );
2020
2021 let delivered = bridge
2022 .take_queued_user_messages_for(DeliveryCheckpoint::EndOfInteraction)
2023 .await;
2024 assert_eq!(delivered.len(), 1);
2025 assert_eq!(delivered[0].message_id, second_id);
2026 assert_eq!(delivered[0].content, "second edited");
2027
2028 assert_eq!(
2029 bridge.revoke_pending_user_message(&second_id).await,
2030 PendingUserMessageMutationResult::AlreadyDelivered
2031 );
2032 assert_eq!(
2033 bridge
2034 .replace_pending_user_message(
2035 &second_id,
2036 "too late".to_string(),
2037 serde_json::json!("too late"),
2038 )
2039 .await,
2040 PendingUserMessageMutationResult::AlreadyDelivered
2041 );
2042 assert_eq!(
2043 bridge.revoke_pending_user_message("missing").await,
2044 PendingUserMessageMutationResult::UnknownMessageId
2045 );
2046 });
2047 }
2048
2049 #[test]
2050 fn pending_user_message_replace_preserves_fifo_position_and_mode() {
2051 let runtime = tokio::runtime::Builder::new_current_thread()
2052 .enable_all()
2053 .build()
2054 .unwrap();
2055 runtime.block_on(async {
2056 let bridge = test_bridge();
2057 let first_id = bridge
2058 .push_pending_user_message(
2059 "first".to_string(),
2060 serde_json::json!("first"),
2061 "finish_step",
2062 )
2063 .await;
2064 let second_id = bridge
2065 .push_pending_user_message(
2066 "second".to_string(),
2067 serde_json::json!("second"),
2068 "finish_step",
2069 )
2070 .await;
2071 assert_eq!(
2072 bridge
2073 .replace_pending_user_message(
2074 &first_id,
2075 "first edited".to_string(),
2076 serde_json::json!("first edited"),
2077 )
2078 .await,
2079 PendingUserMessageMutationResult::Mutated
2080 );
2081
2082 let delivered = bridge
2083 .take_queued_user_messages_for(DeliveryCheckpoint::AfterCurrentOperation)
2084 .await;
2085 assert_eq!(
2086 delivered
2087 .iter()
2088 .map(|message| (&message.message_id, message.content.as_str(), message.mode))
2089 .collect::<Vec<_>>(),
2090 vec![
2091 (&first_id, "first edited", QueuedUserMessageMode::FinishStep,),
2092 (&second_id, "second", QueuedUserMessageMode::FinishStep),
2093 ]
2094 );
2095 });
2096 }
2097
2098 #[test]
2099 fn pending_user_message_state_survives_bridge_replacement() {
2100 let runtime = tokio::runtime::Builder::new_current_thread()
2101 .enable_all()
2102 .build()
2103 .unwrap();
2104 runtime.block_on(async {
2105 let bridge = test_bridge();
2106 let revoked_id = bridge
2107 .push_pending_user_message(
2108 "revoke me".to_string(),
2109 serde_json::json!("revoke me"),
2110 "audit_only",
2111 )
2112 .await;
2113 let delivered_id = bridge
2114 .push_pending_user_message(
2115 "deliver me".to_string(),
2116 serde_json::json!("deliver me"),
2117 "audit_only",
2118 )
2119 .await;
2120 assert_eq!(
2121 bridge.revoke_pending_user_message(&revoked_id).await,
2122 PendingUserMessageMutationResult::Mutated
2123 );
2124 bridge.cancelled.store(true, Ordering::SeqCst);
2125
2126 let replacement_bridge = test_bridge_sharing_injection_state(&bridge);
2127 assert_eq!(
2128 replacement_bridge
2129 .revoke_pending_user_message(&revoked_id)
2130 .await,
2131 PendingUserMessageMutationResult::AlreadyRevoked
2132 );
2133 let delivered = replacement_bridge
2134 .take_queued_user_messages_for(DeliveryCheckpoint::EndOfInteraction)
2135 .await;
2136 assert_eq!(delivered.len(), 1);
2137 assert_eq!(delivered[0].message_id, delivered_id);
2138 assert_eq!(delivered[0].content, "deliver me");
2139 assert_eq!(
2140 bridge.revoke_pending_user_message(&delivered_id).await,
2141 PendingUserMessageMutationResult::AlreadyDelivered
2142 );
2143 });
2144 }
2145
2146 #[test]
2147 fn queued_transcript_injections_preserve_user_reminder_separation() {
2148 let runtime = tokio::runtime::Builder::new_current_thread()
2149 .enable_all()
2150 .build()
2151 .unwrap();
2152 runtime.block_on(async {
2153 let bridge = test_bridge();
2154 bridge
2155 .push_queued_user_message("human follow-up".to_string(), "finish_step")
2156 .await;
2157 let reminder_id = bridge
2158 .push_queued_session_remind_from_params(&serde_json::json!({
2159 "body": "Host-provided ambient context.",
2160 "tags": ["host"],
2161 "dedupe_key": "host-context",
2162 "ttl_turns": 2,
2163 "mode": "audit_only",
2164 "_meta": {"harn": {"source": "test"}},
2165 }))
2166 .await
2167 .expect("valid reminder");
2168
2169 let finish_step = bridge.take_queued_user_messages(false, true, false).await;
2170 assert_eq!(finish_step.len(), 1);
2171 assert_eq!(finish_step[0].content, "human follow-up");
2172
2173 let no_user_messages = bridge.take_queued_user_messages(false, false, true).await;
2174 assert!(no_user_messages.is_empty());
2175
2176 let injections = bridge
2177 .take_queued_transcript_injections_for(DeliveryCheckpoint::EndOfInteraction)
2178 .await;
2179 assert_eq!(injections.len(), 1);
2180 let QueuedTranscriptInjection::Reminder(reminder) = &injections[0] else {
2181 panic!("expected queued reminder");
2182 };
2183 assert_eq!(reminder.reminder.id, reminder_id);
2184 assert_eq!(reminder.reminder.body, "Host-provided ambient context.");
2185 assert_eq!(reminder.reminder.tags, vec!["host".to_string()]);
2186 assert_eq!(
2187 reminder.reminder.dedupe_key.as_deref(),
2188 Some("host-context")
2189 );
2190 assert_eq!(reminder.reminder.ttl_turns, Some(2));
2191 assert_eq!(
2192 reminder.reminder.source,
2193 crate::llm::helpers::ReminderSource::Bridge
2194 );
2195 });
2196 }
2197
2198 #[test]
2199 fn pending_injections_list_user_messages_and_reminders_in_fifo_order() {
2200 let runtime = tokio::runtime::Builder::new_current_thread()
2201 .enable_all()
2202 .build()
2203 .unwrap();
2204 runtime.block_on(async {
2205 let bridge = test_bridge();
2206 let message_id = bridge
2207 .push_pending_user_message(
2208 "human follow-up".to_string(),
2209 serde_json::json!([{"type": "text", "text": "human follow-up"}]),
2210 "finish_step",
2211 )
2212 .await;
2213 let reminder_id = bridge
2214 .push_queued_session_remind_from_params(&serde_json::json!({
2215 "id": "rem-test",
2216 "body": "Host reminder",
2217 "tags": ["host"],
2218 "dedupe_key": "host-reminder",
2219 "ttl_turns": 2,
2220 "mode": "interrupt_immediate",
2221 }))
2222 .await
2223 .expect("valid session/remind payload");
2224
2225 let pending = bridge.pending_injections_json().await;
2226 assert_eq!(pending["pendingCount"], 2);
2227 assert_eq!(pending["injections"][0]["kind"], "user");
2228 assert_eq!(pending["injections"][0]["id"], message_id);
2229 assert_eq!(pending["injections"][0]["messageId"], message_id);
2230 assert_eq!(pending["injections"][0]["mode"], "finish_step");
2231 assert_eq!(pending["injections"][0]["position"], 0);
2232 assert_eq!(pending["injections"][1]["kind"], "reminder");
2233 assert_eq!(pending["injections"][1]["id"], reminder_id);
2234 assert_eq!(pending["injections"][1]["reminderId"], "rem-test");
2235 assert_eq!(pending["injections"][1]["mode"], "interrupt_immediate");
2236 assert_eq!(pending["injections"][1]["body"], "Host reminder");
2237 assert_eq!(pending["injections"][1]["dedupeKey"], "host-reminder");
2238 assert_eq!(pending["injections"][1]["ttlTurns"], 2);
2239 assert_eq!(pending["injections"][1]["position"], 1);
2240 });
2241 }
2242
2243 #[test]
2244 fn pending_reminders_support_revoke_and_delivery_states() {
2245 let runtime = tokio::runtime::Builder::new_current_thread()
2246 .enable_all()
2247 .build()
2248 .unwrap();
2249 runtime.block_on(async {
2250 let bridge = test_bridge();
2251 let revoked_id = bridge
2252 .push_queued_session_remind_from_params(&serde_json::json!({
2253 "id": "rem-revoke",
2254 "body": "remove me",
2255 "mode": "finish_step",
2256 }))
2257 .await
2258 .expect("valid session/remind payload");
2259 let delivered_id = bridge
2260 .push_queued_session_remind_from_params(&serde_json::json!({
2261 "id": "rem-deliver",
2262 "body": "deliver me",
2263 "mode": "finish_step",
2264 }))
2265 .await
2266 .expect("valid session/remind payload");
2267
2268 assert_eq!(
2269 bridge.revoke_pending_reminder(&revoked_id).await,
2270 PendingReminderMutationResult::Mutated
2271 );
2272 assert_eq!(
2273 bridge.revoke_pending_reminder(&revoked_id).await,
2274 PendingReminderMutationResult::AlreadyRevoked
2275 );
2276
2277 let pending = bridge.pending_injections_json().await;
2278 assert_eq!(pending["pendingCount"], 1);
2279 assert_eq!(pending["injections"][0]["reminderId"], delivered_id);
2280
2281 let delivered = bridge
2282 .take_queued_transcript_injections_for(DeliveryCheckpoint::AfterCurrentOperation)
2283 .await;
2284 assert_eq!(delivered.len(), 1);
2285 let QueuedTranscriptInjection::Reminder(reminder) = &delivered[0] else {
2286 panic!("expected delivered reminder");
2287 };
2288 assert_eq!(reminder.reminder.id, delivered_id);
2289
2290 assert_eq!(
2291 bridge.revoke_pending_reminder(&delivered_id).await,
2292 PendingReminderMutationResult::AlreadyDelivered
2293 );
2294 assert_eq!(
2295 bridge.revoke_pending_reminder("missing").await,
2296 PendingReminderMutationResult::UnknownReminderId
2297 );
2298 });
2299 }
2300
2301 #[test]
2302 fn bridge_remind_modes_honor_delivery_checkpoints() {
2303 let runtime = tokio::runtime::Builder::new_current_thread()
2304 .enable_all()
2305 .build()
2306 .unwrap();
2307 runtime.block_on(async {
2308 let cases = [
2309 (
2310 "interrupt_immediate",
2311 DeliveryCheckpoint::InterruptImmediate,
2312 DeliveryCheckpoint::AfterCurrentOperation,
2313 ),
2314 (
2315 "finish_step",
2316 DeliveryCheckpoint::AfterCurrentOperation,
2317 DeliveryCheckpoint::EndOfInteraction,
2318 ),
2319 (
2320 "audit_only",
2321 DeliveryCheckpoint::EndOfInteraction,
2322 DeliveryCheckpoint::InterruptImmediate,
2323 ),
2324 ];
2325
2326 for (mode, expected_checkpoint, wrong_checkpoint) in cases {
2327 let bridge = test_bridge();
2328 bridge
2329 .push_queued_session_remind_from_params(&serde_json::json!({
2330 "body": format!("Reminder for {mode}"),
2331 "mode": mode,
2332 }))
2333 .await
2334 .expect("valid session/remind payload");
2335
2336 let premature = bridge
2337 .take_queued_transcript_injections_for(wrong_checkpoint)
2338 .await;
2339 assert!(
2340 premature.is_empty(),
2341 "{mode} reminder must not be delivered at {wrong_checkpoint:?}"
2342 );
2343
2344 let delivered = bridge
2345 .take_queued_transcript_injections_for(expected_checkpoint)
2346 .await;
2347 assert_eq!(delivered.len(), 1, "{mode} reminder was not delivered");
2348 let QueuedTranscriptInjection::Reminder(reminder) = &delivered[0] else {
2349 panic!("expected reminder for {mode}");
2350 };
2351 assert_eq!(reminder.reminder.body, format!("Reminder for {mode}"));
2352 }
2353 });
2354 }
2355
2356 #[test]
2357 fn session_remind_validation_rejects_user_message_shape() {
2358 let err = queued_session_remind_from_params(&serde_json::json!({
2359 "content": "this is still a user message",
2360 "mode": "interrupt_immediate",
2361 }))
2362 .expect_err("session/remind must require a reminder body");
2363 assert!(err.contains(Code::ReminderInvalidShape.as_str()));
2364 assert!(err.contains("body"));
2365 }
2366
2367 #[test]
2368 fn session_remind_validation_rejects_unknown_options_separately() {
2369 let err = queued_session_remind_from_params(&serde_json::json!({
2370 "body": "valid body",
2371 "unknown_host_field": true,
2372 }))
2373 .expect_err("session/remind must reject unknown top-level fields");
2374 assert!(err.contains(Code::ReminderUnknownOption.as_str()));
2375 assert!(err.contains("unknown_host_field"));
2376 }
2377
2378 #[test]
2379 fn session_remind_validation_rejects_unknown_propagate_with_specific_code() {
2380 let err = queued_session_remind_from_params(&serde_json::json!({
2381 "body": "valid body",
2382 "propagate": "workspace",
2383 }))
2384 .expect_err("session/remind must reject unknown propagate values");
2385 assert!(err.contains(Code::ReminderUnknownPropagate.as_str()));
2386 assert!(err.contains("propagate"));
2387 }
2388
2389 #[test]
2390 fn test_json_result_to_vm_value_string() {
2391 let val = serde_json::json!("hello");
2392 let vm_val = json_result_to_vm_value(&val);
2393 assert_eq!(vm_val.display(), "hello");
2394 }
2395
2396 #[test]
2397 fn test_json_result_to_vm_value_dict() {
2398 let val = serde_json::json!({"name": "test", "count": 42});
2399 let vm_val = json_result_to_vm_value(&val);
2400 let VmValue::Dict(d) = &vm_val else {
2401 unreachable!("Expected Dict, got {:?}", vm_val);
2402 };
2403 assert_eq!(d.get("name").unwrap().display(), "test");
2404 assert_eq!(d.get("count").unwrap().display(), "42");
2405 }
2406
2407 #[test]
2408 fn test_json_result_to_vm_value_null() {
2409 let val = serde_json::json!(null);
2410 let vm_val = json_result_to_vm_value(&val);
2411 assert!(matches!(vm_val, VmValue::Nil));
2412 }
2413
2414 #[test]
2415 fn test_json_result_to_vm_value_nested() {
2416 let val = serde_json::json!({
2417 "text": "response",
2418 "tool_calls": [
2419 {"id": "tc_1", "name": "read_file", "arguments": {"path": "foo.rs"}}
2420 ],
2421 "input_tokens": 100,
2422 "output_tokens": 50,
2423 });
2424 let vm_val = json_result_to_vm_value(&val);
2425 let VmValue::Dict(d) = &vm_val else {
2426 unreachable!("Expected Dict, got {:?}", vm_val);
2427 };
2428 assert_eq!(d.get("text").unwrap().display(), "response");
2429 let VmValue::List(list) = d.get("tool_calls").unwrap() else {
2430 unreachable!("Expected List for tool_calls");
2431 };
2432 assert_eq!(list.len(), 1);
2433 }
2434
2435 #[test]
2436 fn parse_host_tools_list_accepts_object_wrapper() {
2437 let tools = parse_host_tools_list_response(serde_json::json!({
2438 "tools": [
2439 {
2440 "name": "Read",
2441 "description": "Read a file",
2442 "schema": {"type": "object"},
2443 }
2444 ]
2445 }))
2446 .expect("tool list");
2447
2448 assert_eq!(tools.len(), 1);
2449 assert_eq!(tools[0]["name"], "Read");
2450 assert_eq!(tools[0]["deprecated"], false);
2451 }
2452
2453 #[test]
2454 fn parse_host_tools_list_accepts_compat_fields() {
2455 let tools = parse_host_tools_list_response(serde_json::json!({
2456 "result": {
2457 "tools": [
2458 {
2459 "name": "Edit",
2460 "short_description": "Apply an edit",
2461 "input_schema": {"type": "object"},
2462 "deprecated": true,
2463 }
2464 ]
2465 }
2466 }))
2467 .expect("tool list");
2468
2469 assert_eq!(tools[0]["description"], "Apply an edit");
2470 assert_eq!(tools[0]["schema"]["type"], "object");
2471 assert_eq!(tools[0]["deprecated"], true);
2472 }
2473
2474 #[test]
2475 fn parse_host_tools_list_requires_tool_names() {
2476 let err = parse_host_tools_list_response(serde_json::json!({
2477 "tools": [
2478 {"description": "missing name"}
2479 ]
2480 }))
2481 .expect_err("expected error");
2482 assert!(err
2483 .to_string()
2484 .contains("host/tools/list: every tool must include a string `name`"));
2485 }
2486
2487 #[test]
2488 fn test_timeout_duration() {
2489 assert_eq!(bridge_call_timeout("host/work"), Some(DEFAULT_TIMEOUT));
2490 assert_eq!(DEFAULT_TIMEOUT.as_secs(), 300);
2491 }
2492
2493 #[test]
2494 fn interactive_permission_requests_have_no_bridge_timeout() {
2495 assert_eq!(
2496 bridge_call_timeout(crate::llm::acp_permission::METHOD_REQUEST_PERMISSION),
2497 None
2498 );
2499 }
2500
2501 #[tokio::test(flavor = "current_thread", start_paused = true)]
2502 async fn non_interactive_bridge_calls_timeout_under_paused_time() {
2503 let pending = Arc::new(Mutex::new(HashMap::new()));
2504 let bridge = HostBridge::from_parts_with_writer(
2505 pending.clone(),
2506 Arc::new(AtomicBool::new(false)),
2507 Arc::new(|_| Ok(())),
2508 1,
2509 );
2510
2511 let call = bridge.call("host/work", serde_json::json!({}));
2512 tokio::pin!(call);
2513 wait_for_pending(&pending, 1, call.as_mut()).await;
2514
2515 tokio::time::advance(DEFAULT_TIMEOUT).await;
2516 let result = call.await;
2517 assert!(matches!(
2518 result,
2519 Err(VmError::Runtime(message)) if message.contains("host/work")
2520 && message.contains("within 300s")
2521 ));
2522 assert!(pending.lock().await.is_empty());
2523 }
2524
2525 #[tokio::test(flavor = "current_thread", start_paused = true)]
2526 async fn permission_bridge_calls_survive_timeout_window_under_paused_time() {
2527 let pending = Arc::new(Mutex::new(HashMap::new()));
2528 let bridge = HostBridge::from_parts_with_writer(
2529 pending.clone(),
2530 Arc::new(AtomicBool::new(false)),
2531 Arc::new(|_| Ok(())),
2532 1,
2533 );
2534
2535 let call = bridge.call(
2536 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION,
2537 serde_json::json!({}),
2538 );
2539 tokio::pin!(call);
2540 wait_for_pending(&pending, 1, call.as_mut()).await;
2541
2542 tokio::time::advance(DEFAULT_TIMEOUT + Duration::from_secs(1)).await;
2543 tokio::select! {
2544 result = &mut call => panic!("permission request timed out: {result:?}"),
2545 _ = tokio::task::yield_now() => {}
2546 }
2547 assert!(pending.lock().await.contains_key(&1));
2548
2549 let sender = pending
2550 .lock()
2551 .await
2552 .remove(&1)
2553 .expect("pending permission sender");
2554 sender
2555 .send(serde_json::json!({
2556 "result": crate::llm::acp_permission::allow_response()
2557 }))
2558 .expect("send permission response");
2559
2560 let response = call.await.expect("permission response");
2561 assert_eq!(
2562 crate::llm::acp_permission::parse_response(&response),
2563 crate::llm::acp_permission::WireOutcome::Allowed
2564 );
2565 }
2566
2567 async fn wait_for_pending<F>(
2568 pending: &Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
2569 id: u64,
2570 mut call: Pin<&mut F>,
2571 ) where
2572 F: Future<Output = Result<serde_json::Value, VmError>>,
2573 {
2574 for _ in 0..8 {
2575 tokio::select! {
2576 result = call.as_mut() => panic!("call completed before entering pending map: {result:?}"),
2577 _ = tokio::task::yield_now() => {}
2578 }
2579 if pending.lock().await.contains_key(&id) {
2580 return;
2581 }
2582 }
2583 panic!("bridge call {id} did not enter the pending map after bounded polling");
2584 }
2585}