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