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