Skip to main content

harn_vm/
bridge.rs

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