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_stop_reason: std::sync::Mutex<Option<String>>,
107 visible_call_states: std::sync::Mutex<HashMap<String, VisibleTextState>>,
109 visible_call_streams: std::sync::Mutex<HashMap<String, bool>>,
111 in_process: Option<InProcessHost>,
113}
114
115struct InProcessHost {
116 module_path: PathBuf,
117 exported_functions: BTreeMap<String, Arc<VmClosure>>,
118 vm: Vm,
119}
120
121impl InProcessHost {
122 fn dispatch<'a>(
128 &'a self,
129 method: &'a str,
130 params: serde_json::Value,
131 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, VmError>> + Send + 'a>> {
132 Box::pin(async move {
133 match method {
134 "builtin_call" => {
135 let name = params
136 .get("name")
137 .and_then(|value| value.as_str())
138 .unwrap_or_default();
139 let args = params
140 .get("args")
141 .and_then(|value| value.as_array())
142 .cloned()
143 .unwrap_or_default()
144 .into_iter()
145 .map(|value| json_result_to_vm_value(&value))
146 .collect::<Vec<_>>();
147 self.invoke_export(name, &args).await
148 }
149 "host/tools/list" => self
150 .invoke_optional_export("host_tools_list", &[])
151 .await
152 .map(|value| value.unwrap_or_else(|| serde_json::json!({ "tools": [] }))),
153 crate::llm::acp_permission::METHOD_REQUEST_PERMISSION => {
154 self.request_permission(params).await
155 }
156 other => Err(VmError::Runtime(format!(
157 "playground host backend does not implement bridge method '{other}'"
158 ))),
159 }
160 })
161 }
162
163 async fn invoke_export(
164 &self,
165 name: &str,
166 args: &[VmValue],
167 ) -> Result<serde_json::Value, VmError> {
168 let Some(closure) = self.exported_functions.get(name) else {
169 return Err(VmError::Runtime(format!(
170 "Playground host is missing capability '{name}'. Define `pub fn {name}(...)` in {}",
171 self.module_path.display()
172 )));
173 };
174
175 let mut vm = self.vm.child_vm_for_host();
176 let result = vm.call_closure_pub(closure, args).await?;
177 Ok(crate::llm::vm_value_to_json(&result))
178 }
179
180 async fn invoke_optional_export(
181 &self,
182 name: &str,
183 args: &[VmValue],
184 ) -> Result<Option<serde_json::Value>, VmError> {
185 if !self.exported_functions.contains_key(name) {
186 return Ok(None);
187 }
188 self.invoke_export(name, args).await.map(Some)
189 }
190
191 async fn request_permission(
192 &self,
193 params: serde_json::Value,
194 ) -> Result<serde_json::Value, VmError> {
195 let Some(closure) = self.exported_functions.get("request_permission") else {
198 return Ok(crate::llm::acp_permission::allow_response());
199 };
200
201 let tool_call = params.get("toolCall");
202 let tool_name = tool_call
203 .and_then(|tool_call| tool_call.pointer("/_meta/harn/toolName"))
204 .or_else(|| tool_call.and_then(|tool_call| tool_call.get("toolName")))
205 .or_else(|| tool_call.and_then(|tool_call| tool_call.get("title")))
206 .and_then(|value| value.as_str())
207 .unwrap_or_default();
208 let tool_args = tool_call
209 .and_then(|tool_call| tool_call.get("rawInput"))
210 .map(json_result_to_vm_value)
211 .unwrap_or(VmValue::Nil);
212 let full_payload = json_result_to_vm_value(¶ms);
213
214 let arg_count = closure.func.params.len();
215 let args = if arg_count >= 3 {
216 vec![
217 VmValue::String(arcstr::ArcStr::from(tool_name.to_string())),
218 tool_args,
219 full_payload,
220 ]
221 } else if arg_count == 2 {
222 vec![
223 VmValue::String(arcstr::ArcStr::from(tool_name.to_string())),
224 tool_args,
225 ]
226 } else if arg_count == 1 {
227 vec![full_payload]
228 } else {
229 Vec::new()
230 };
231
232 let mut vm = self.vm.child_vm_for_host();
233 let result = vm.call_closure_pub(closure, &args).await?;
234 let payload = match result {
239 VmValue::Bool(granted) => {
240 if granted {
241 crate::llm::acp_permission::allow_response()
242 } else {
243 crate::llm::acp_permission::reject_response(None)
244 }
245 }
246 VmValue::String(reason) if !reason.is_empty() => {
247 crate::llm::acp_permission::reject_response(Some(reason.to_string()))
248 }
249 other => {
250 let json = crate::llm::vm_value_to_json(&other);
251 if let Some(granted) = json.get("granted").and_then(|value| value.as_bool()) {
252 if granted {
253 crate::llm::acp_permission::allow_response()
254 } else {
255 crate::llm::acp_permission::reject_response(
256 json.get("reason")
257 .and_then(|value| value.as_str())
258 .map(str::to_string),
259 )
260 }
261 } else if json.get("outcome").is_some() {
262 json
264 } else if other.is_truthy() {
265 crate::llm::acp_permission::allow_response()
266 } else {
267 crate::llm::acp_permission::reject_response(None)
268 }
269 }
270 };
271 Ok(payload)
272 }
273}
274
275#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub enum QueuedUserMessageMode {
285 InterruptImmediate,
286 FinishStep,
287 AuditOnly,
288}
289
290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
291pub enum DeliveryCheckpoint {
292 InterruptImmediate,
293 AfterCurrentOperation,
294 EndOfInteraction,
295}
296
297impl QueuedUserMessageMode {
298 fn from_str(value: &str) -> Self {
299 match value {
300 "interrupt_immediate" | "interrupt" => Self::InterruptImmediate,
301 "finish_step" | "after_current_operation" | "steer" => Self::FinishStep,
305 "queue" => Self::AuditOnly,
307 _ => Self::AuditOnly,
312 }
313 }
314
315 fn as_str(self) -> &'static str {
316 match self {
317 Self::InterruptImmediate => "interrupt_immediate",
318 Self::FinishStep => "finish_step",
319 Self::AuditOnly => "audit_only",
320 }
321 }
322}
323
324#[derive(Clone, Debug, PartialEq, Eq)]
325pub struct QueuedUserMessage {
326 pub message_id: String,
327 pub content: String,
328 pub transcript_content: serde_json::Value,
329 pub mode: QueuedUserMessageMode,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq)]
333pub struct QueuedReminder {
334 pub reminder: crate::llm::helpers::SystemReminder,
335 pub mode: QueuedUserMessageMode,
336}
337
338#[derive(Clone, Debug, PartialEq, Eq)]
339pub enum QueuedTranscriptInjection {
340 User(QueuedUserMessage),
341 Reminder(QueuedReminder),
342}
343
344#[derive(Debug, Default)]
345struct QueuedTranscriptInjections {
346 queue: VecDeque<QueuedTranscriptInjection>,
347 revoked_user_message_ids: HashSet<String>,
348 delivered_user_message_ids: HashSet<String>,
349 revoked_reminder_ids: HashSet<String>,
350 delivered_reminder_ids: HashSet<String>,
351}
352
353#[derive(Clone, Debug, Default)]
354pub struct HostBridgeInjectionState {
355 inner: Arc<Mutex<QueuedTranscriptInjections>>,
356}
357
358#[derive(Clone, Copy, Debug, PartialEq, Eq)]
359pub enum PendingUserMessageMutationResult {
360 Mutated,
361 AlreadyRevoked,
362 AlreadyDelivered,
363 UnknownMessageId,
364}
365
366impl QueuedTranscriptInjection {
367 fn mode(&self) -> QueuedUserMessageMode {
368 match self {
369 Self::User(message) => message.mode,
370 Self::Reminder(reminder) => reminder.mode,
371 }
372 }
373
374 fn pending_json(&self, position: usize) -> serde_json::Value {
375 match self {
376 Self::User(message) => serde_json::json!({
377 "kind": "user",
378 "id": message.message_id,
379 "messageId": message.message_id,
380 "mode": message.mode.as_str(),
381 "position": position,
382 "content": message.transcript_content,
383 }),
384 Self::Reminder(reminder) => serde_json::json!({
385 "kind": "reminder",
386 "id": reminder.reminder.id,
387 "reminderId": reminder.reminder.id,
388 "mode": reminder.mode.as_str(),
389 "position": position,
390 "body": reminder.reminder.body,
391 "tags": reminder.reminder.tags,
392 "dedupeKey": reminder.reminder.dedupe_key,
393 "ttlTurns": reminder.reminder.ttl_turns,
394 "preserveOnCompact": reminder.reminder.preserve_on_compact,
395 "propagate": reminder.reminder.propagate.as_str(),
396 "roleHint": reminder.reminder.role_hint.as_str(),
397 "source": reminder.reminder.source.as_str(),
398 "firedAtTurn": reminder.reminder.fired_at_turn,
399 "originatingAgentId": reminder.reminder.originating_agent_id,
400 }),
401 }
402 }
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq)]
406pub enum PendingReminderMutationResult {
407 Mutated,
408 AlreadyRevoked,
409 AlreadyDelivered,
410 UnknownReminderId,
411}
412
413fn new_inject_message_id() -> String {
414 format!("msg_inj_{}", uuid::Uuid::now_v7().simple())
415}
416
417impl HostBridgeInjectionState {
418 pub fn new() -> Self {
419 Self::default()
420 }
421
422 pub async fn push_pending_user_message(
423 &self,
424 content: String,
425 transcript_content: serde_json::Value,
426 mode: &str,
427 ) -> String {
428 let message_id = new_inject_message_id();
429 self.inner
430 .lock()
431 .await
432 .queue
433 .push_back(QueuedTranscriptInjection::User(QueuedUserMessage {
434 message_id: message_id.clone(),
435 content,
436 transcript_content,
437 mode: QueuedUserMessageMode::from_str(mode),
438 }));
439 message_id
440 }
441
442 pub async fn revoke_pending_user_message(
443 &self,
444 message_id: &str,
445 ) -> PendingUserMessageMutationResult {
446 let mut state = self.inner.lock().await;
447 let mut retained = VecDeque::new();
448 let mut revoked = false;
449 while let Some(injection) = state.queue.pop_front() {
450 match &injection {
451 QueuedTranscriptInjection::User(message) if message.message_id == message_id => {
452 revoked = true;
453 }
454 _ => retained.push_back(injection),
455 }
456 }
457 state.queue = retained;
458 if revoked {
459 state
460 .revoked_user_message_ids
461 .insert(message_id.to_string());
462 return PendingUserMessageMutationResult::Mutated;
463 }
464 if state.revoked_user_message_ids.contains(message_id) {
465 PendingUserMessageMutationResult::AlreadyRevoked
466 } else if state.delivered_user_message_ids.contains(message_id) {
467 PendingUserMessageMutationResult::AlreadyDelivered
468 } else {
469 PendingUserMessageMutationResult::UnknownMessageId
470 }
471 }
472
473 pub async fn revoke_pending_reminder(
474 &self,
475 reminder_id: &str,
476 ) -> PendingReminderMutationResult {
477 let mut state = self.inner.lock().await;
478 let mut retained = VecDeque::new();
479 let mut revoked = false;
480 while let Some(injection) = state.queue.pop_front() {
481 match &injection {
482 QueuedTranscriptInjection::Reminder(reminder)
483 if reminder.reminder.id == reminder_id =>
484 {
485 revoked = true;
486 }
487 _ => retained.push_back(injection),
488 }
489 }
490 state.queue = retained;
491 if revoked {
492 state.revoked_reminder_ids.insert(reminder_id.to_string());
493 return PendingReminderMutationResult::Mutated;
494 }
495 if state.revoked_reminder_ids.contains(reminder_id) {
496 PendingReminderMutationResult::AlreadyRevoked
497 } else if state.delivered_reminder_ids.contains(reminder_id) {
498 PendingReminderMutationResult::AlreadyDelivered
499 } else {
500 PendingReminderMutationResult::UnknownReminderId
501 }
502 }
503
504 pub async fn replace_pending_user_message(
505 &self,
506 message_id: &str,
507 content: String,
508 transcript_content: serde_json::Value,
509 ) -> PendingUserMessageMutationResult {
510 let mut state = self.inner.lock().await;
511 for injection in &mut state.queue {
512 if let QueuedTranscriptInjection::User(message) = injection {
513 if message.message_id == message_id {
514 message.content = content;
515 message.transcript_content = transcript_content;
516 return PendingUserMessageMutationResult::Mutated;
517 }
518 }
519 }
520 if state.revoked_user_message_ids.contains(message_id) {
521 PendingUserMessageMutationResult::AlreadyRevoked
522 } else if state.delivered_user_message_ids.contains(message_id) {
523 PendingUserMessageMutationResult::AlreadyDelivered
524 } else {
525 PendingUserMessageMutationResult::UnknownMessageId
526 }
527 }
528
529 async fn push_session_reminder(&self, reminder: QueuedReminder) {
530 self.inner
531 .lock()
532 .await
533 .queue
534 .push_back(QueuedTranscriptInjection::Reminder(reminder));
535 }
536
537 pub async fn pending_injections_json(&self) -> serde_json::Value {
538 let state = self.inner.lock().await;
539 let injections = state
540 .queue
541 .iter()
542 .enumerate()
543 .map(|(position, injection)| injection.pending_json(position))
544 .collect::<Vec<_>>();
545 serde_json::json!({
546 "pendingCount": injections.len(),
547 "injections": injections,
548 })
549 }
550}
551
552fn reminder_unknown_option_error(message: impl AsRef<str>) -> String {
553 format!(
554 "{}: {}",
555 Code::ReminderUnknownOption.as_str(),
556 message.as_ref()
557 )
558}
559
560fn session_remind_shape_error(message: impl AsRef<str>) -> String {
561 format!(
562 "{}: {}",
563 Code::ReminderInvalidShape.as_str(),
564 message.as_ref()
565 )
566}
567
568fn reminder_unknown_propagate_error(message: impl AsRef<str>) -> String {
569 format!(
570 "{}: {}",
571 Code::ReminderUnknownPropagate.as_str(),
572 message.as_ref()
573 )
574}
575
576fn string_field(
577 map: &serde_json::Map<String, serde_json::Value>,
578 key: &str,
579 required: bool,
580) -> Result<Option<String>, String> {
581 match map.get(key) {
582 None | Some(serde_json::Value::Null) if required => Err(session_remind_shape_error(
583 format!("`{key}` must be a non-empty string"),
584 )),
585 None | Some(serde_json::Value::Null) => Ok(None),
586 Some(serde_json::Value::String(value)) if required && value.trim().is_empty() => Err(
587 session_remind_shape_error(format!("`{key}` must be a non-empty string")),
588 ),
589 Some(serde_json::Value::String(value)) => {
590 let trimmed = value.trim();
591 if trimmed.is_empty() {
592 Ok(None)
593 } else {
594 Ok(Some(trimmed.to_string()))
595 }
596 }
597 Some(other) => Err(session_remind_shape_error(format!(
598 "`{key}` must be a string, got {other}"
599 ))),
600 }
601}
602
603fn bool_field(
604 map: &serde_json::Map<String, serde_json::Value>,
605 key: &str,
606) -> Result<Option<bool>, String> {
607 match map.get(key) {
608 None | Some(serde_json::Value::Null) => Ok(None),
609 Some(serde_json::Value::Bool(value)) => Ok(Some(*value)),
610 Some(other) => Err(session_remind_shape_error(format!(
611 "`{key}` must be a bool, got {other}"
612 ))),
613 }
614}
615
616fn int_field(
617 map: &serde_json::Map<String, serde_json::Value>,
618 key: &str,
619) -> Result<Option<i64>, String> {
620 match map.get(key) {
621 None | Some(serde_json::Value::Null) => Ok(None),
622 Some(serde_json::Value::Number(value)) => {
623 let Some(value) = value.as_i64() else {
624 return Err(session_remind_shape_error(format!(
625 "`{key}` must be an integer"
626 )));
627 };
628 Ok(Some(value))
629 }
630 Some(other) => Err(session_remind_shape_error(format!(
631 "`{key}` must be an int, got {other}"
632 ))),
633 }
634}
635
636fn tags_field(map: &serde_json::Map<String, serde_json::Value>) -> Result<Vec<String>, String> {
637 let Some(value) = map.get("tags") else {
638 return Ok(Vec::new());
639 };
640 if value.is_null() {
641 return Ok(Vec::new());
642 }
643 let Some(values) = value.as_array() else {
644 return Err(session_remind_shape_error("`tags` must be a list"));
645 };
646 let mut tags = Vec::new();
647 for value in values {
648 let Some(tag) = value.as_str() else {
649 return Err(session_remind_shape_error(format!(
650 "`tags` entries must be strings, got {value}"
651 )));
652 };
653 let tag = tag.trim();
654 if tag.is_empty() {
655 return Err(session_remind_shape_error(
656 "`tags` entries must be non-empty strings",
657 ));
658 }
659 if !tags.iter().any(|existing| existing == tag) {
660 tags.push(tag.to_string());
661 }
662 }
663 Ok(tags)
664}
665
666fn session_remind_payload_from_value(
667 value: &serde_json::Value,
668) -> Result<crate::llm::helpers::SystemReminder, String> {
669 let Some(map) = value.as_object() else {
670 return Err(session_remind_shape_error(
671 "session/remind payload must be a reminder object",
672 ));
673 };
674 const ALLOWED: &[&str] = &[
675 "_meta",
676 "body",
677 "dedupe_key",
678 "fired_at_turn",
679 "id",
680 "preserve_on_compact",
681 "propagate",
682 "role_hint",
683 "source",
684 "tags",
685 "ttl_turns",
686 ];
687 let unknown = map
688 .keys()
689 .filter(|key| !ALLOWED.contains(&key.as_str()))
690 .map(String::as_str)
691 .collect::<Vec<_>>();
692 if !unknown.is_empty() {
693 if unknown.contains(&"content") {
694 return Err(session_remind_shape_error(
695 "session/remind expects reminder `body`, not user-message `content`",
696 ));
697 }
698 return Err(reminder_unknown_option_error(format!(
699 "unknown reminder option(s): {}",
700 unknown.join(", ")
701 )));
702 }
703 if let Some(meta) = map.get("_meta") {
704 if !meta.is_null() && !meta.is_object() {
705 return Err(session_remind_shape_error("`_meta` must be an object"));
706 }
707 }
708 let ttl_turns = int_field(map, "ttl_turns")?;
709 if let Some(value) = ttl_turns {
710 if value <= 0 {
711 return Err(session_remind_shape_error("`ttl_turns` must be > 0"));
712 }
713 }
714 let fired_at_turn = int_field(map, "fired_at_turn")?.unwrap_or(0);
715 if fired_at_turn < 0 {
716 return Err(session_remind_shape_error(
717 "`fired_at_turn` must be >= 0 when provided",
718 ));
719 }
720 match string_field(map, "source", false)?.as_deref() {
721 None | Some("bridge") => {}
722 Some(_) => {
723 return Err(session_remind_shape_error(
724 "`source` for session/remind must be bridge when provided",
725 ))
726 }
727 }
728 let propagate = match string_field(map, "propagate", false)?.as_deref() {
729 None => crate::llm::helpers::ReminderPropagate::Session,
730 Some("all") => crate::llm::helpers::ReminderPropagate::All,
731 Some("session") => crate::llm::helpers::ReminderPropagate::Session,
732 Some("none") => crate::llm::helpers::ReminderPropagate::None,
733 Some(_) => {
734 return Err(reminder_unknown_propagate_error(
735 "`propagate` must be one of all, session, or none",
736 ))
737 }
738 };
739 let role_hint = match string_field(map, "role_hint", false)?.as_deref() {
740 None => crate::llm::helpers::ReminderRoleHint::System,
741 Some("system") => crate::llm::helpers::ReminderRoleHint::System,
742 Some("developer") => crate::llm::helpers::ReminderRoleHint::Developer,
743 Some("user_block") => crate::llm::helpers::ReminderRoleHint::UserBlock,
744 Some("ephemeral_cache") => crate::llm::helpers::ReminderRoleHint::EphemeralCache,
745 Some(_) => {
746 return Err(session_remind_shape_error(
747 "`role_hint` must be one of system, developer, user_block, or ephemeral_cache",
748 ))
749 }
750 };
751 Ok(crate::llm::helpers::SystemReminder {
752 id: string_field(map, "id", false)?.unwrap_or_else(|| uuid::Uuid::now_v7().to_string()),
753 tags: tags_field(map)?,
754 dedupe_key: string_field(map, "dedupe_key", false)?,
755 ttl_turns,
756 preserve_on_compact: bool_field(map, "preserve_on_compact")?.unwrap_or(false),
757 propagate,
758 role_hint,
759 source: crate::llm::helpers::ReminderSource::Bridge,
760 body: string_field(map, "body", true)?.unwrap_or_default(),
761 fired_at_turn,
762 originating_agent_id: None,
763 })
764}
765
766fn handle_cancel_tool_call_notification(params: &serde_json::Value) {
776 let session_id = params
777 .get("sessionId")
778 .or_else(|| params.get("session_id"))
779 .and_then(|value| value.as_str())
780 .unwrap_or_default();
781 let call_id = params
782 .get("toolCallId")
783 .or_else(|| params.get("tool_call_id"))
784 .or_else(|| params.get("callId"))
785 .or_else(|| params.get("call_id"))
786 .and_then(|value| value.as_str())
787 .unwrap_or_default();
788 if call_id.is_empty() {
789 return;
790 }
791 let reason = params
792 .get("reason")
793 .and_then(|value| value.as_str())
794 .unwrap_or("host cancelled in-flight tool call")
795 .to_string();
796 let inject_reminder = params
797 .get("injectReminder")
798 .or_else(|| params.get("inject_reminder"))
799 .and_then(|value| value.as_bool())
800 .unwrap_or(true);
801 crate::tool_call_cancellations::cancel(session_id, call_id, reason, inject_reminder);
802}
803
804fn queued_session_remind_from_params(params: &serde_json::Value) -> Result<QueuedReminder, String> {
805 let mode = QueuedUserMessageMode::from_str(
806 params
807 .get("mode")
808 .and_then(|value| value.as_str())
809 .unwrap_or("audit_only"),
810 );
811 let reminder_value = if let Some(reminder) = params.get("reminder") {
812 reminder.clone()
813 } else {
814 let Some(params) = params.as_object() else {
815 return Err(session_remind_shape_error(
816 "session/remind params must be an object",
817 ));
818 };
819 let mut reminder = params.clone();
820 reminder.remove("mode");
821 reminder.remove("sessionId");
822 reminder.remove("session_id");
823 serde_json::Value::Object(reminder)
824 };
825 Ok(QueuedReminder {
826 reminder: session_remind_payload_from_value(&reminder_value)?,
827 mode,
828 })
829}
830
831#[allow(clippy::new_without_default)]
833impl HostBridge {
834 pub fn new() -> Self {
839 let pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>> =
840 Arc::new(Mutex::new(HashMap::new()));
841 let cancelled = Arc::new(AtomicBool::new(false));
842 let cancel_notify = Arc::new(Notify::new());
843 let disconnected = Arc::new(AtomicBool::new(false));
844 let queued_transcript_injections = HostBridgeInjectionState::default();
845 let resume_requested = Arc::new(AtomicBool::new(false));
846 let skills_reload_requested = Arc::new(AtomicBool::new(false));
847 let daemon_idle = Arc::new(AtomicBool::new(false));
848
849 let pending_clone = pending.clone();
851 let cancelled_clone = cancelled.clone();
852 let cancel_notify_clone = cancel_notify.clone();
853 let disconnected_clone = disconnected.clone();
854 let queued_clone = queued_transcript_injections.clone();
855 let resume_clone = resume_requested.clone();
856 let skills_reload_clone = skills_reload_requested.clone();
857 tokio::task::spawn_local(async move {
858 let stdin = tokio::io::stdin();
859 let reader = tokio::io::BufReader::new(stdin);
860 let mut lines = reader.lines();
861
862 while let Ok(Some(line)) = lines.next_line().await {
863 let line = line.trim().to_string();
864 if line.is_empty() {
865 continue;
866 }
867
868 let msg: serde_json::Value = match serde_json::from_str(&line) {
869 Ok(v) => v,
870 Err(_) => continue,
871 };
872
873 if msg.get("id").is_none() {
875 if let Some(method) = msg["method"].as_str() {
876 if method == "cancel" {
877 cancelled_clone.store(true, Ordering::SeqCst);
878 cancel_notify_clone.notify_waiters();
879 } else if method == "agent/resume" {
880 resume_clone.store(true, Ordering::SeqCst);
881 } else if method == "skills/update" {
882 skills_reload_clone.store(true, Ordering::SeqCst);
883 } else if method == "session/remind" {
884 let params = &msg["params"];
885 if let Ok(reminder) = queued_session_remind_from_params(params) {
886 queued_clone.push_session_reminder(reminder).await;
887 }
888 } else if method == "session/cancel_tool_call" {
889 handle_cancel_tool_call_notification(&msg["params"]);
890 }
891 }
892 continue;
893 }
894
895 if let Some(id) = msg["id"].as_u64() {
896 let mut pending = pending_clone.lock().await;
897 if let Some(sender) = pending.remove(&id) {
898 let _ = sender.send(msg);
899 }
900 }
901 }
902
903 disconnected_clone.store(true, Ordering::SeqCst);
907 let mut pending = pending_clone.lock().await;
908 pending.clear();
909 });
910
911 Self {
912 next_id: AtomicU64::new(1),
913 pending,
914 cancelled,
915 cancel_notify,
916 disconnected,
917 writer: stdout_writer(Arc::new(std::sync::Mutex::new(()))),
918 session_id: std::sync::Mutex::new(String::new()),
919 script_name: std::sync::Mutex::new(String::new()),
920 queued_transcript_injections,
921 resume_requested,
922 skills_reload_requested,
923 daemon_idle,
924 prompt_stop_reason: std::sync::Mutex::new(None),
925 visible_call_states: std::sync::Mutex::new(HashMap::new()),
926 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
927 in_process: None,
928 }
929 }
930
931 pub fn from_parts(
937 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
938 cancelled: Arc<AtomicBool>,
939 stdout_lock: Arc<std::sync::Mutex<()>>,
940 start_id: u64,
941 ) -> Self {
942 Self::from_parts_with_writer(pending, cancelled, stdout_writer(stdout_lock), start_id)
943 }
944
945 pub fn from_parts_with_writer(
946 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
947 cancelled: Arc<AtomicBool>,
948 writer: HostBridgeWriter,
949 start_id: u64,
950 ) -> Self {
951 Self::from_parts_with_writer_and_cancel_notify(
952 pending,
953 cancelled,
954 Arc::new(Notify::new()),
955 writer,
956 start_id,
957 )
958 }
959
960 pub fn from_parts_with_writer_and_cancel_notify(
961 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
962 cancelled: Arc<AtomicBool>,
963 cancel_notify: Arc<Notify>,
964 writer: HostBridgeWriter,
965 start_id: u64,
966 ) -> Self {
967 Self::from_parts_with_writer_cancel_notify_and_injection_state(
968 pending,
969 cancelled,
970 cancel_notify,
971 writer,
972 start_id,
973 None,
974 )
975 }
976
977 pub fn from_parts_with_writer_cancel_notify_and_injection_state(
978 pending: Arc<Mutex<HashMap<u64, oneshot::Sender<serde_json::Value>>>>,
979 cancelled: Arc<AtomicBool>,
980 cancel_notify: Arc<Notify>,
981 writer: HostBridgeWriter,
982 start_id: u64,
983 injection_state: Option<HostBridgeInjectionState>,
984 ) -> Self {
985 Self {
986 next_id: AtomicU64::new(start_id),
987 pending,
988 cancelled,
989 cancel_notify,
990 disconnected: Arc::new(AtomicBool::new(false)),
991 writer,
992 session_id: std::sync::Mutex::new(String::new()),
993 script_name: std::sync::Mutex::new(String::new()),
994 queued_transcript_injections: injection_state.unwrap_or_default(),
995 resume_requested: Arc::new(AtomicBool::new(false)),
996 skills_reload_requested: Arc::new(AtomicBool::new(false)),
997 daemon_idle: Arc::new(AtomicBool::new(false)),
998 prompt_stop_reason: std::sync::Mutex::new(None),
999 visible_call_states: std::sync::Mutex::new(HashMap::new()),
1000 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
1001 in_process: None,
1002 }
1003 }
1004
1005 pub async fn from_harn_module(mut vm: Vm, module_path: &Path) -> Result<Self, VmError> {
1008 let exported_functions = vm.load_module_exports(module_path).await?;
1009 Ok(Self {
1010 next_id: AtomicU64::new(1),
1011 pending: Arc::new(Mutex::new(HashMap::new())),
1012 cancelled: Arc::new(AtomicBool::new(false)),
1013 cancel_notify: Arc::new(Notify::new()),
1014 disconnected: Arc::new(AtomicBool::new(false)),
1015 writer: stdout_writer(Arc::new(std::sync::Mutex::new(()))),
1016 session_id: std::sync::Mutex::new(String::new()),
1017 script_name: std::sync::Mutex::new(String::new()),
1018 queued_transcript_injections: HostBridgeInjectionState::default(),
1019 resume_requested: Arc::new(AtomicBool::new(false)),
1020 skills_reload_requested: Arc::new(AtomicBool::new(false)),
1021 daemon_idle: Arc::new(AtomicBool::new(false)),
1022 prompt_stop_reason: std::sync::Mutex::new(None),
1023 visible_call_states: std::sync::Mutex::new(HashMap::new()),
1024 visible_call_streams: std::sync::Mutex::new(HashMap::new()),
1025 in_process: Some(InProcessHost {
1026 module_path: module_path.to_path_buf(),
1027 exported_functions,
1028 vm,
1029 }),
1030 })
1031 }
1032
1033 pub fn set_session_id(&self, id: &str) {
1035 *self.session_id.lock().unwrap_or_else(|e| e.into_inner()) = id.to_string();
1036 }
1037
1038 pub fn set_script_name(&self, name: &str) {
1040 *self.script_name.lock().unwrap_or_else(|e| e.into_inner()) = name.to_string();
1041 }
1042
1043 fn get_script_name(&self) -> String {
1045 self.script_name
1046 .lock()
1047 .unwrap_or_else(|e| e.into_inner())
1048 .clone()
1049 }
1050
1051 pub fn get_session_id(&self) -> String {
1053 self.session_id
1054 .lock()
1055 .unwrap_or_else(|e| e.into_inner())
1056 .clone()
1057 }
1058
1059 fn write_line(&self, line: &str) -> Result<(), VmError> {
1061 (self.writer)(line).map_err(VmError::Runtime)
1062 }
1063
1064 pub async fn call(
1069 &self,
1070 method: &str,
1071 params: serde_json::Value,
1072 ) -> Result<serde_json::Value, VmError> {
1073 if let Some(in_process) = &self.in_process {
1074 return in_process.dispatch(method, params).await;
1075 }
1076
1077 if self.is_cancelled() {
1078 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1079 }
1080
1081 let id = self.next_id.fetch_add(1, Ordering::SeqCst);
1082 let cancel_wait = self.cancel_notify.notified();
1083 tokio::pin!(cancel_wait);
1084 cancel_wait.as_mut().enable();
1088
1089 let request = crate::jsonrpc::request(id, method, params);
1090
1091 let (tx, rx) = oneshot::channel();
1092 {
1093 let mut pending = self.pending.lock().await;
1094 if self.disconnected.load(Ordering::SeqCst) {
1095 return Err(VmError::Runtime(
1096 "Bridge: host connection is already closed".into(),
1097 ));
1098 }
1099 pending.insert(id, tx);
1100 }
1101
1102 let line = serde_json::to_string(&request)
1103 .map_err(|e| VmError::Runtime(format!("Bridge serialization error: {e}")))?;
1104 if let Err(e) = self.write_line(&line) {
1105 let mut pending = self.pending.lock().await;
1106 pending.remove(&id);
1107 return Err(e);
1108 }
1109
1110 if self.is_cancelled() {
1111 let mut pending = self.pending.lock().await;
1112 pending.remove(&id);
1113 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1114 }
1115
1116 let timeout_wait = wait_for_bridge_call_timeout(method);
1117 tokio::pin!(timeout_wait);
1118
1119 let response = tokio::select! {
1120 result = rx => match result {
1121 Ok(msg) => msg,
1122 Err(_) => {
1123 return Err(VmError::Runtime(
1125 "Bridge: host closed connection before responding".into(),
1126 ));
1127 }
1128 },
1129 _ = &mut cancel_wait => {
1130 let mut pending = self.pending.lock().await;
1131 pending.remove(&id);
1132 return Err(VmError::Runtime("Bridge: operation cancelled".into()));
1133 }
1134 timeout = &mut timeout_wait => {
1135 let mut pending = self.pending.lock().await;
1136 pending.remove(&id);
1137 return Err(VmError::Runtime(format!(
1138 "Bridge: host did not respond to '{method}' within {}s",
1139 timeout.as_secs()
1140 )));
1141 }
1142 };
1143
1144 if let Some(error) = response.get("error") {
1145 let message = error["message"].as_str().unwrap_or("Unknown host error");
1146 let code = error["code"].as_i64().unwrap_or(-1);
1147 if code == -32001 {
1149 return Err(VmError::CategorizedError {
1150 message: message.to_string(),
1151 category: ErrorCategory::ToolRejected,
1152 });
1153 }
1154 return Err(VmError::Runtime(format!("Host error ({code}): {message}")));
1155 }
1156
1157 Ok(response["result"].clone())
1158 }
1159
1160 pub fn notify(&self, method: &str, params: serde_json::Value) {
1163 let notification = crate::jsonrpc::notification(method, params);
1164 if self.in_process.is_some() {
1165 return;
1166 }
1167 if let Ok(line) = serde_json::to_string(¬ification) {
1168 let _ = self.write_line(&line);
1169 }
1170 }
1171
1172 pub fn is_cancelled(&self) -> bool {
1174 self.cancelled.load(Ordering::SeqCst)
1175 }
1176
1177 pub fn take_resume_signal(&self) -> bool {
1178 self.resume_requested.swap(false, Ordering::SeqCst)
1179 }
1180
1181 pub fn signal_resume(&self) {
1182 self.resume_requested.store(true, Ordering::SeqCst);
1183 }
1184
1185 pub fn set_daemon_idle(&self, idle: bool) {
1186 self.daemon_idle.store(idle, Ordering::SeqCst);
1187 }
1188
1189 pub fn is_daemon_idle(&self) -> bool {
1190 self.daemon_idle.load(Ordering::SeqCst)
1191 }
1192
1193 pub fn set_prompt_stop_reason(&self, reason: &str) {
1198 *self
1199 .prompt_stop_reason
1200 .lock()
1201 .unwrap_or_else(|e| e.into_inner()) = Some(reason.to_string());
1202 }
1203
1204 pub fn take_prompt_stop_reason(&self) -> Option<String> {
1209 self.prompt_stop_reason
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}