1use std::collections::{BTreeMap, HashMap, HashSet};
19use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
20use std::sync::{Arc, Mutex as StdMutex, RwLock};
21use std::time::Duration;
22
23use futures::future::BoxFuture;
24use pi_agent::{
25 AfterToolCallResult, AgentMessage, AgentTool, AgentToolResult, BeforeToolCallResult, ToolError,
26 ToolExecutionMode, ToolUpdates,
27};
28use pi_ai::{AssistantMessage, AssistantMessageEvent, ToolResultContent};
29use pi_ext::adapters::{
30 self, CommandRegistration, ExtensionAgentTool, ExtensionProvider, FlagRegistration,
31 ProviderRegistration, Registry, RendererRegistration, ShortcutRegistration, ToolRegistration,
32};
33use pi_ext::client::{HostClient, HostClientError, HostEvent, HostUiRequest, HostUiResponse};
34use pi_ext::host::{self, HostError, HostSpec};
35use pi_ext::protocol::{
36 self, DisposeSlot, ExtensionErrorEvent, FlagValueWire, FlagsSetRequest, FlagsSetResponse,
37 NotifyRequest, ProviderEvent, ShortcutExecuteRequest, ShortcutExecuteResponse, ToolUpdate,
38 UiEventRequest, UiEventResponse, UiSlot,
39};
40use pi_ext::sanitize::{SanitizedSlot, sanitize_slot};
41use serde::{Deserialize, Serialize};
42use serde_json::{Map, Value};
43use tokio::sync::{broadcast, mpsc, watch};
44use tokio_util::sync::CancellationToken;
45
46use super::agent_session::events::AgentSessionEvent;
47use super::agent_session::extension_runner::{
48 BeforeAgentStartResult, CancelResult, ExtensionRunner, InputTransformResult,
49};
50use super::model_runtime::{
51 ModelRuntime, ModelRuntimeError, ProviderConfigInput, ProviderModelDefinition,
52};
53use super::resources::{ExtensionResourcePath, ResourceExtensionPaths};
54
55pub const HOOK_TIMEOUT: Duration = Duration::from_secs(30);
57
58pub const START_TIMEOUT: Duration = Duration::from_secs(30);
60
61pub const EVENT_CHANNEL_CAPACITY: usize = 256;
63
64pub const LOAD_METHOD: &str = "extensions.load";
66
67pub const COMMAND_EXECUTE_METHOD: &str = "command.execute";
69pub const MESSAGE_UPDATE_DELTA_METHOD: &str = "message_update_delta";
71
72pub const TOOL_RENDER_HTML_METHOD: &str = "tool.renderHtml";
74
75pub const ALL_EVENT_TYPES: &[&str] = &[
79 "project_trust",
80 "resources_discover",
81 "session_start",
82 "session_info_changed",
83 "session_before_switch",
84 "session_before_fork",
85 "session_before_compact",
86 "session_compact",
87 "session_shutdown",
88 "session_before_tree",
89 "session_tree",
90 "context",
91 "before_provider_request",
92 "before_provider_headers",
93 "after_provider_response",
94 "before_agent_start",
95 "agent_start",
96 "agent_end",
97 "agent_settled",
98 "turn_start",
99 "turn_end",
100 "message_start",
101 "message_update",
102 "message_end",
103 "tool_execution_start",
104 "tool_execution_update",
105 "tool_execution_end",
106 "model_select",
107 "thinking_level_select",
108 "tool_call",
109 "tool_result",
110 "user_bash",
111 "input",
112];
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
116pub enum ToolRenderPhase {
117 Call,
119 Result,
121}
122
123impl ToolRenderPhase {
124 const fn as_str(self) -> &'static str {
125 match self {
126 Self::Call => "call",
127 Self::Result => "result",
128 }
129 }
130}
131
132#[derive(Debug, Clone)]
134pub enum ExtensionUiEvent {
135 Notify(NotifyRequest),
137 Slot(SanitizedSlot),
139 Dispose {
141 key: String,
143 },
144}
145
146#[derive(Debug, thiserror::Error)]
148pub enum HostStartError {
149 #[error("extension host not available: {0}")]
151 Resolve(#[from] HostError),
152 #[error("extension host unavailable: {0}")]
154 Acquire(#[source] acquire::HostAcquireError),
155 #[error("extension host spawn failed: {0}")]
157 Spawn(String),
158 #[error("extension host handshake failed: {0}")]
160 Handshake(String),
161 #[error("extension host load failed: {0}")]
163 Load(String),
164 #[error("extension host flag synchronization failed: {0}")]
166 FlagSync(String),
167}
168
169impl From<HostClientError> for HostStartError {
170 fn from(value: HostClientError) -> Self {
171 match value {
172 HostClientError::Spawn { message } => Self::Spawn(message),
173 HostClientError::Handshake { message } => Self::Handshake(message),
174 other => Self::Load(other.to_string()),
175 }
176 }
177}
178
179impl From<acquire::HostAcquireError> for HostStartError {
180 fn from(value: acquire::HostAcquireError) -> Self {
181 match value {
182 acquire::HostAcquireError::Resolution(error) => Self::Resolve(error),
184 other => Self::Acquire(other),
185 }
186 }
187}
188
189#[derive(Serialize)]
194#[serde(rename_all = "camelCase")]
195struct ExtensionsLoadRequest<'a> {
196 extension_paths: &'a [String],
197 cwd: &'a str,
198 project_trusted: bool,
199}
200
201#[derive(Debug, Clone, Deserialize)]
203#[serde(rename_all = "camelCase")]
204struct ToolWire {
205 name: String,
206 #[serde(default)]
207 label: String,
208 #[serde(default)]
209 description: String,
210 #[serde(default)]
211 parameters: Value,
212 #[serde(default)]
213 execution_mode: Option<pi_agent::ToolExecutionMode>,
214}
215
216#[derive(Debug, Clone, Deserialize)]
218#[serde(rename_all = "camelCase")]
219struct CommandWire {
220 name: String,
221 #[serde(default)]
222 description: Option<String>,
223 #[serde(default)]
224 source: Option<String>,
225}
226
227#[derive(Debug, Clone, Deserialize)]
229#[serde(rename_all = "camelCase")]
230struct ShortcutWire {
231 key: String,
232 #[serde(default)]
233 description: Option<String>,
234 #[serde(default)]
235 extension_path: Option<String>,
236}
237
238#[derive(Debug, Clone, Deserialize)]
240#[serde(rename_all = "camelCase")]
241struct FlagWire {
242 name: String,
243 #[serde(default)]
244 description: Option<String>,
245 #[serde(default, rename = "type")]
246 kind: Option<String>,
247 #[serde(default)]
248 default: Option<String>,
249 #[serde(default)]
251 value: Option<Value>,
252 #[serde(default)]
253 extension_path: Option<String>,
254}
255
256#[derive(Debug, Clone, Deserialize)]
258#[serde(rename_all = "camelCase")]
259struct RendererWire {
260 #[serde(default, rename = "type")]
261 kind: Option<String>,
262 name: String,
263}
264
265#[derive(Debug, Clone, Deserialize)]
272#[serde(rename_all = "camelCase")]
273struct ProviderWire {
274 name: String,
275 #[serde(default)]
277 display_name: Option<String>,
278 #[serde(default)]
279 base_url: Option<String>,
280 #[serde(default)]
281 api: Option<String>,
282 #[serde(default)]
283 api_key: Option<String>,
284 #[serde(default)]
285 headers: Option<BTreeMap<String, String>>,
286 #[serde(default)]
287 auth_header: Option<bool>,
288 #[serde(default)]
289 models: Option<Vec<ProviderModelDefinition>>,
290 #[serde(default)]
292 stream_simple: bool,
293 #[serde(default)]
295 extension_path: Option<String>,
296}
297
298impl ProviderWire {
299 fn to_config_input(&self) -> ProviderConfigInput {
300 ProviderConfigInput {
301 name: self.display_name.clone(),
302 base_url: self.base_url.clone(),
303 api_key: self.api_key.clone(),
304 api: self.api.clone(),
305 headers: self.headers.clone(),
306 auth_header: self.auth_header,
307 models: self.models.clone(),
308 model_overrides: None,
309 oauth: None,
310 }
311 }
312}
313
314#[derive(Debug, Clone, Default, Deserialize)]
316#[serde(rename_all = "camelCase")]
317pub struct RegistrySnapshotWire {
318 #[serde(default)]
320 tools: Vec<ToolWire>,
321 #[serde(default)]
323 commands: Vec<CommandWire>,
324 #[serde(default)]
326 shortcuts: Vec<ShortcutWire>,
327 #[serde(default)]
329 flags: Vec<FlagWire>,
330 #[serde(default)]
332 renderers: Vec<RendererWire>,
333 #[serde(default)]
335 providers: Vec<ProviderWire>,
336 #[serde(default)]
338 handlers: Vec<String>,
339 #[serde(default)]
341 terminal_input: bool,
342 #[serde(default)]
344 extensions: Option<u64>,
345 #[serde(default)]
347 errors: Vec<LoadErrorWire>,
348}
349
350#[derive(Debug, Clone, Default, Deserialize)]
352#[serde(rename_all = "camelCase")]
353struct LoadErrorWire {
354 #[serde(default)]
355 path: Option<String>,
356 #[serde(default)]
357 error: Option<String>,
358 #[serde(default)]
359 message: Option<String>,
360}
361
362#[derive(Default)]
365struct RegistrySnapshot {
366 registry: Registry,
368 raw_shortcuts: Vec<ShortcutRegistration>,
370 tools: HashMap<String, Arc<dyn AgentTool>>,
372 handlers: HashSet<String>,
374 terminal_input: bool,
376 flag_values: HashMap<String, Value>,
378 provider_configs: HashMap<String, ProviderConfigInput>,
380 stream_provider_ids: HashSet<String>,
382 provider_extension_paths: HashMap<String, String>,
384 load_errors: Vec<(String, String)>,
386}
387
388fn build_snapshot(wire: RegistrySnapshotWire, client: &Arc<HostClient>) -> RegistrySnapshot {
389 let mut snapshot = RegistrySnapshot {
390 terminal_input: wire.terminal_input,
391 ..RegistrySnapshot::default()
392 };
393
394 for tool in wire.tools {
395 let meta = ToolRegistration {
396 name: tool.name.clone(),
397 label: tool.label,
398 description: tool.description,
399 parameters: tool.parameters,
400 execution_mode: tool.execution_mode,
401 };
402 if snapshot.registry.register_tool(meta.clone()) {
405 let adapter = ExtensionAgentTool::new(meta, Arc::clone(client));
406 snapshot
407 .tools
408 .insert(adapter.name().to_owned(), Arc::new(adapter));
409 }
410 }
411
412 for command in wire.commands {
413 let _ = snapshot.registry.register_command(CommandRegistration {
414 name: command.name,
415 description: command.description,
416 source: command.source,
417 });
418 }
419
420 for shortcut in wire.shortcuts {
421 let registration = ShortcutRegistration {
422 key: shortcut.key,
423 description: shortcut.description,
424 extension_path: shortcut.extension_path,
425 };
426 snapshot.raw_shortcuts.push(registration.clone());
427 let _ = snapshot.registry.register_shortcut(registration);
428 }
429
430 for flag in wire.flags {
431 if snapshot.registry.register_flag(FlagRegistration {
432 name: flag.name.clone(),
433 description: flag.description,
434 kind: match flag.kind.as_deref() {
435 Some("boolean") => adapters::FlagKind::Boolean,
436 _ => adapters::FlagKind::String,
437 },
438 default: flag.default.clone(),
439 extension_path: flag.extension_path,
440 }) {
441 let value = flag
443 .value
444 .clone()
445 .or_else(|| flag.default.map(Value::String))
446 .unwrap_or_else(|| Value::String(String::new()));
447 snapshot.flag_values.insert(flag.name, value);
448 }
449 }
450
451 for renderer in wire.renderers {
452 let _ = snapshot.registry.register_renderer(RendererRegistration {
453 kind: match renderer.kind.as_deref() {
454 Some("tool") => adapters::RendererKind::Tool,
455 Some("widget") => adapters::RendererKind::Widget,
456 _ => adapters::RendererKind::Message,
457 },
458 name: renderer.name,
459 });
460 }
461
462 for provider in wire.providers {
463 let name = provider.name.clone();
464 let stream_simple = provider.stream_simple;
465 let extension_path = provider.extension_path.clone();
466 let config = provider.to_config_input();
467 if snapshot.registry.register_provider(ProviderRegistration {
468 name: name.clone(),
469 base_url: config.base_url.clone(),
470 api: config.api.clone(),
471 }) {
472 snapshot.provider_configs.insert(name.clone(), config);
473 if stream_simple {
474 snapshot.stream_provider_ids.insert(name.clone());
475 }
476 if let Some(path) = extension_path {
477 snapshot.provider_extension_paths.insert(name, path);
478 }
479 }
480 }
481
482 for err in wire.errors {
483 let path = err.path.unwrap_or_else(|| "<unknown>".to_owned());
484 let message = err
485 .error
486 .or(err.message)
487 .unwrap_or_else(|| "extension load failed".to_owned());
488 snapshot.load_errors.push((path, message));
489 }
490
491 snapshot.handlers = wire.handlers.into_iter().collect();
492 let _ = wire.extensions;
493 snapshot
494}
495
496#[derive(Debug, Clone, Deserialize)]
501#[serde(rename_all = "camelCase")]
502struct CancelWire {
503 #[serde(default)]
504 cancel: bool,
505 #[serde(default)]
506 reason: Option<String>,
507}
508
509#[derive(Debug, Clone, Deserialize)]
510#[serde(rename_all = "camelCase")]
511struct BeforeToolCallWire {
512 #[serde(default)]
513 block: bool,
514 #[serde(default)]
515 reason: Option<String>,
516}
517
518#[derive(Debug, Clone, Default, Deserialize)]
519#[serde(rename_all = "camelCase")]
520struct AfterToolCallWire {
521 #[serde(default)]
522 content: Option<Vec<ToolResultContent>>,
523 #[serde(default)]
524 details: Option<Value>,
525 #[serde(default)]
526 is_error: Option<bool>,
527 #[serde(default)]
528 terminate: Option<bool>,
529}
530
531#[derive(Debug, Clone, Deserialize)]
532#[serde(tag = "action", rename_all = "camelCase")]
533enum InputTransformWire {
534 Continue,
535 Transform {
536 text: String,
537 #[serde(default)]
538 images: Option<Value>,
539 },
540 Handled,
541}
542
543#[derive(Debug, Clone, Default, Deserialize)]
544#[serde(rename_all = "camelCase")]
545struct BeforeAgentStartWire {
546 #[serde(default)]
547 messages: Vec<AgentMessage>,
548 #[serde(default)]
549 system_prompt: Option<String>,
550}
551#[derive(Debug, Clone, Deserialize)]
552#[serde(rename_all = "camelCase")]
553struct ResourcePathWire {
554 path: String,
555 extension_path: String,
556}
557
558#[derive(Debug, Clone, Default, Deserialize)]
559#[serde(rename_all = "camelCase")]
560struct ResourcesDiscoverWire {
561 #[serde(default, rename = "skillPaths")]
562 skills: Option<Vec<ResourcePathWire>>,
563 #[serde(default, rename = "promptPaths")]
564 prompts: Option<Vec<ResourcePathWire>>,
565 #[serde(default, rename = "themePaths")]
566 themes: Option<Vec<ResourcePathWire>>,
567}
568
569#[derive(Debug, Clone, Default, Deserialize)]
570struct MessageEndWire {
571 message: Option<AgentMessage>,
572}
573
574#[derive(Debug, Clone, Deserialize)]
575#[serde(rename_all = "camelCase")]
576struct CommandExecuteWire {
577 #[serde(default)]
578 ok: bool,
579}
580
581#[derive(Debug, Clone, Deserialize)]
582#[serde(rename_all = "camelCase")]
583struct ToolRenderHtmlWire {
584 #[serde(default)]
585 html: Option<String>,
586}
587
588type SlotWatch = watch::Sender<Option<SanitizedSlot>>;
594
595const RETIRED_MESSAGE: &str = "extension host retired by reload";
597
598#[derive(Default)]
606struct TrafficGate {
607 inflight: AtomicU64,
610 closed: AtomicBool,
612 idle: tokio::sync::Notify,
614}
615
616impl TrafficGate {
617 fn enter(self: &Arc<Self>) -> Option<TrafficPermit> {
619 self.inflight.fetch_add(1, Ordering::SeqCst);
622 if self.closed.load(Ordering::SeqCst) {
623 self.release();
624 return None;
625 }
626 Some(TrafficPermit {
627 gate: Arc::clone(self),
628 })
629 }
630
631 fn release(&self) {
632 if self.inflight.fetch_sub(1, Ordering::SeqCst) == 1 {
633 self.idle.notify_waiters();
634 }
635 }
636
637 fn close(&self) {
639 self.closed.store(true, Ordering::SeqCst);
640 }
641
642 async fn drain(&self) {
644 loop {
645 let idle = self.idle.notified();
646 tokio::pin!(idle);
647 idle.as_mut().enable();
648 if self.inflight.load(Ordering::SeqCst) == 0 {
649 return;
650 }
651 idle.await;
652 }
653 }
654}
655
656struct TrafficPermit {
658 gate: Arc<TrafficGate>,
659}
660
661impl Drop for TrafficPermit {
662 fn drop(&mut self) {
663 self.gate.release();
664 }
665}
666
667struct GatedHostTool {
673 delegate: Arc<dyn AgentTool>,
674 traffic: Arc<TrafficGate>,
675}
676
677impl AgentTool for GatedHostTool {
678 fn name(&self) -> &str {
679 self.delegate.name()
680 }
681
682 fn label(&self) -> &str {
683 self.delegate.label()
684 }
685
686 fn description(&self) -> &str {
687 self.delegate.description()
688 }
689
690 fn parameters(&self) -> &Value {
691 self.delegate.parameters()
692 }
693
694 fn execution_mode(&self) -> Option<ToolExecutionMode> {
695 self.delegate.execution_mode()
696 }
697
698 fn prepare_arguments(&self, raw: &Map<String, Value>) -> Result<Map<String, Value>, ToolError> {
699 self.delegate.prepare_arguments(raw)
700 }
701
702 fn validate_arguments(
703 &self,
704 args: &Map<String, Value>,
705 ) -> Result<Map<String, Value>, ToolError> {
706 self.delegate.validate_arguments(args)
707 }
708
709 fn prepare_and_validate_arguments(
710 &self,
711 raw: Map<String, Value>,
712 ) -> BoxFuture<'_, Result<Map<String, Value>, ToolError>> {
713 Box::pin(async move {
714 let Some(_permit) = self.traffic.enter() else {
715 return Err(ToolError::new(RETIRED_MESSAGE));
716 };
717 self.delegate.prepare_and_validate_arguments(raw).await
718 })
719 }
720
721 fn execute(
722 &self,
723 tool_call_id: &str,
724 args: Map<String, Value>,
725 cancel: CancellationToken,
726 updates: ToolUpdates,
727 ) -> BoxFuture<'static, Result<AgentToolResult, ToolError>> {
728 let delegate = Arc::clone(&self.delegate);
729 let traffic = Arc::clone(&self.traffic);
730 let tool_call_id = tool_call_id.to_owned();
731 Box::pin(async move {
732 let Some(_permit) = traffic.enter() else {
733 return Err(ToolError::new(RETIRED_MESSAGE));
734 };
735 delegate.execute(&tool_call_id, args, cancel, updates).await
736 })
737 }
738}
739
740struct Inner {
741 client: Arc<HostClient>,
742 snapshot: RwLock<RegistrySnapshot>,
743 flag_values: RwLock<HashMap<String, Value>>,
744 slots: RwLock<HashMap<String, SlotWatch>>,
745 tool_updates_tx: broadcast::Sender<ToolUpdate>,
746 provider_events_tx: broadcast::Sender<ProviderEvent>,
747 errors_tx: broadcast::Sender<ExtensionErrorEvent>,
748 ui_tx: broadcast::Sender<ExtensionUiEvent>,
749 ui_requests_tx: mpsc::Sender<HostUiRequest>,
750 ui_requests_rx: StdMutex<Option<mpsc::Receiver<HostUiRequest>>>,
751 ui_requests_claimed: AtomicBool,
752 extension_paths: Vec<String>,
754 load_cwd: String,
756 project_trusted: bool,
758 reload_generation: AtomicU64,
760 disabled: AtomicBool,
763 stale: AtomicBool,
765 shutdown_done: AtomicBool,
767 shutdown_lock: tokio::sync::Mutex<()>,
769 hook_timeout: Duration,
772 traffic: Arc<TrafficGate>,
774}
775
776impl Inner {
777 fn new(
778 client: Arc<HostClient>,
779 mut snapshot: RegistrySnapshot,
780 extension_paths: Vec<String>,
781 load_cwd: String,
782 project_trusted: bool,
783 hook_timeout: Duration,
784 ) -> Self {
785 let (tool_updates_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
786 let (provider_events_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
787 let (errors_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
788 let (ui_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
789 let (ui_requests_tx, ui_requests_rx) = mpsc::channel(EVENT_CHANNEL_CAPACITY);
790 let flag_values = snapshot.flag_values.clone();
791 let traffic = Arc::new(TrafficGate::default());
792 snapshot.tools = snapshot
796 .tools
797 .into_iter()
798 .map(|(name, delegate)| {
799 let gated: Arc<dyn AgentTool> = Arc::new(GatedHostTool {
800 delegate,
801 traffic: Arc::clone(&traffic),
802 });
803 (name, gated)
804 })
805 .collect();
806 Self {
807 client,
808 snapshot: RwLock::new(snapshot),
809 flag_values: RwLock::new(flag_values),
810 slots: RwLock::new(HashMap::new()),
811 tool_updates_tx,
812 provider_events_tx,
813 errors_tx,
814 ui_tx,
815 ui_requests_tx,
816 ui_requests_rx: StdMutex::new(Some(ui_requests_rx)),
817 ui_requests_claimed: AtomicBool::new(false),
818 extension_paths,
819 load_cwd,
820 project_trusted,
821 reload_generation: AtomicU64::new(1),
822 disabled: AtomicBool::new(false),
823 stale: AtomicBool::new(false),
824 shutdown_done: AtomicBool::new(false),
825 shutdown_lock: tokio::sync::Mutex::new(()),
826 hook_timeout,
827 traffic,
828 }
829 }
830
831 fn has_handlers(&self, event: &str) -> bool {
832 if self.disabled.load(Ordering::Relaxed) || self.stale.load(Ordering::Relaxed) {
833 return false;
834 }
835 self.snapshot
836 .read()
837 .is_ok_and(|guard| guard.handlers.contains(event))
838 }
839
840 fn active(&self) -> bool {
841 !self.disabled.load(Ordering::Relaxed) && !self.stale.load(Ordering::Relaxed)
842 }
843
844 fn has_command(&self, name: &str) -> bool {
847 if !self.active() {
848 return false;
849 }
850 self.snapshot.read().is_ok_and(|guard| {
851 guard
852 .registry
853 .commands()
854 .iter()
855 .any(|command| command.name == name)
856 })
857 }
858
859 async fn hook_request(
862 &self,
863 method: &str,
864 payload: Value,
865 ) -> Result<protocol::Frame, HostClientError> {
866 let Some(_permit) = self.traffic.enter() else {
867 return Err(HostClientError::NotRunning);
868 };
869 if !self.active() {
870 return Err(HostClientError::NotRunning);
871 }
872 self.client
873 .request_raw(method, payload, self.hook_timeout)
874 .await
875 }
876
877 fn report_host_error(&self, err: &HostClientError) {
881 let fatal = matches!(
882 err,
883 HostClientError::Closed { .. }
884 | HostClientError::Protocol { .. }
885 | HostClientError::NotRunning
886 );
887 if fatal {
888 self.disabled.store(true, Ordering::Relaxed);
889 }
890 let event = ExtensionErrorEvent {
891 code: error_code(err).to_owned(),
892 message: err.to_string(),
893 retryable: false,
894 data: None,
895 };
896 let _ = self.errors_tx.send(event);
897 }
898
899 fn publish_error(&self, code: &str, message: &str, data: Option<Value>) {
900 let event = ExtensionErrorEvent {
901 code: code.to_owned(),
902 message: message.to_owned(),
903 retryable: false,
904 data,
905 };
906 let _ = self.errors_tx.send(event);
907 }
908
909 fn slot_send(&self, slot: SanitizedSlot) {
910 let Ok(mut slots) = self.slots.write() else {
916 return;
917 };
918 if !self.active() {
919 return;
920 }
921 let sender = slots
922 .entry(slot.key.clone())
923 .or_insert_with(|| watch::channel(None).0);
924 let _ = sender.send(Some(slot.clone()));
925 let _ = self.ui_tx.send(ExtensionUiEvent::Slot(slot));
926 }
927
928 fn slot_dispose(&self, key: &str) {
929 let Ok(mut slots) = self.slots.write() else {
932 return;
933 };
934 if !self.active() {
935 return;
936 }
937 if let Some(sender) = slots.get(key) {
938 let _ = sender.send(None);
939 }
940 slots.remove(key);
941 let _ = self.ui_tx.send(ExtensionUiEvent::Dispose {
942 key: key.to_owned(),
943 });
944 }
945
946 fn notify_send(&self, notification: NotifyRequest) {
947 let Ok(_slots) = self.slots.write() else {
951 return;
952 };
953 if !self.active() {
954 return;
955 }
956 let _ = self.ui_tx.send(ExtensionUiEvent::Notify(notification));
957 }
958
959 fn dispose_all_slots(&self) {
960 let Ok(mut slots) = self.slots.write() else {
965 return;
966 };
967 for (key, sender) in slots.iter() {
968 let _ = sender.send(None);
969 let _ = self
970 .ui_tx
971 .send(ExtensionUiEvent::Dispose { key: key.clone() });
972 }
973 slots.clear();
974 }
975}
976
977fn error_code(err: &HostClientError) -> &'static str {
978 match err {
979 HostClientError::Handshake { .. } => "extension_handshake",
980 HostClientError::Timeout { .. } => "extension_timeout",
981 HostClientError::Cancelled { .. } => "extension_cancelled",
982 HostClientError::Closed { .. } => "extension_closed",
983 HostClientError::Protocol { .. } => "extension_protocol",
984 HostClientError::Remote { .. } => "extension_remote",
985 HostClientError::Spawn { .. } => "extension_spawn",
986 HostClientError::NotRunning => "extension_not_running",
987 HostClientError::Payload(_) => "extension_payload",
988 }
989}
990
991pub struct HostExtensionRunner {
1000 inner: Arc<Inner>,
1001}
1002
1003impl HostExtensionRunner {
1004 pub async fn start(extension_paths: Vec<String>) -> Result<Arc<Self>, HostStartError> {
1013 let spec = host::resolve_host()?;
1014 Self::spawn_from(&spec, extension_paths).await
1015 }
1016
1017 pub async fn spawn_from(
1023 spec: &HostSpec,
1024 extension_paths: Vec<String>,
1025 ) -> Result<Arc<Self>, HostStartError> {
1026 let client =
1027 Arc::new(HostClient::spawn(spec).map_err(|e| HostStartError::Spawn(e.to_string()))?);
1028 let startup = Self::connect(Arc::clone(&client), extension_paths).await;
1029 Self::finish_startup(&client, startup).await
1030 }
1031
1032 pub async fn connect(
1040 client: Arc<HostClient>,
1041 extension_paths: Vec<String>,
1042 ) -> Result<Arc<Self>, HostStartError> {
1043 Self::connect_with_timeout(client, extension_paths, HOOK_TIMEOUT).await
1044 }
1045
1046 pub async fn connect_with_timeout(
1053 client: Arc<HostClient>,
1054 extension_paths: Vec<String>,
1055 hook_timeout: Duration,
1056 ) -> Result<Arc<Self>, HostStartError> {
1057 let load_cwd = std::env::current_dir()
1058 .map(|p| p.to_string_lossy().into_owned())
1059 .unwrap_or_default();
1060 Self::connect_with_cwd_and_trust(client, extension_paths, load_cwd, false, hook_timeout)
1061 .await
1062 }
1063
1064 pub async fn connect_with_cwd(
1070 client: Arc<HostClient>,
1071 extension_paths: Vec<String>,
1072 load_cwd: impl Into<String>,
1073 hook_timeout: Duration,
1074 ) -> Result<Arc<Self>, HostStartError> {
1075 Self::connect_with_cwd_and_trust(client, extension_paths, load_cwd, false, hook_timeout)
1076 .await
1077 }
1078
1079 pub async fn connect_with_cwd_and_trust(
1085 client: Arc<HostClient>,
1086 extension_paths: Vec<String>,
1087 load_cwd: impl Into<String>,
1088 project_trusted: bool,
1089 hook_timeout: Duration,
1090 ) -> Result<Arc<Self>, HostStartError> {
1091 client.handshake().await?;
1092 let load_cwd = load_cwd.into();
1093 let snapshot = Self::load(&client, &extension_paths, &load_cwd, project_trusted).await?;
1094 let inner = Arc::new(Inner::new(
1095 Arc::clone(&client),
1096 snapshot,
1097 extension_paths,
1098 load_cwd,
1099 project_trusted,
1100 hook_timeout,
1101 ));
1102 let runner = Arc::new(Self {
1103 inner: Arc::clone(&inner),
1104 });
1105 spawn_event_pump(inner);
1106 Ok(runner)
1107 }
1108
1109 async fn finish_startup(
1110 client: &HostClient,
1111 startup: Result<Arc<Self>, HostStartError>,
1112 ) -> Result<Arc<Self>, HostStartError> {
1113 if startup.is_err() {
1114 let _ = client.shutdown().await;
1115 }
1116 startup
1117 }
1118
1119 async fn load(
1120 client: &Arc<HostClient>,
1121 extension_paths: &[String],
1122 cwd: &str,
1123 project_trusted: bool,
1124 ) -> Result<RegistrySnapshot, HostStartError> {
1125 let payload = serde_json::to_value(ExtensionsLoadRequest {
1126 extension_paths,
1127 cwd,
1128 project_trusted,
1129 })
1130 .map_err(|error| HostStartError::Load(error.to_string()))?;
1131 let frame = client
1132 .request_raw(LOAD_METHOD, payload, START_TIMEOUT)
1133 .await?;
1134 let wire: RegistrySnapshotWire = serde_json::from_value(frame.payload)
1135 .map_err(|e| HostStartError::Load(e.to_string()))?;
1136 Ok(build_snapshot(wire, client))
1137 }
1138
1139 #[must_use]
1141 pub fn client(&self) -> &Arc<HostClient> {
1142 &self.inner.client
1143 }
1144
1145 #[must_use]
1147 pub fn extension_paths(&self) -> Vec<String> {
1148 self.inner.extension_paths.clone()
1149 }
1150
1151 #[must_use]
1153 pub fn load_errors(&self) -> Vec<(String, String)> {
1154 self.inner
1155 .snapshot
1156 .read()
1157 .map(|guard| guard.load_errors.clone())
1158 .unwrap_or_default()
1159 }
1160
1161 #[must_use]
1163 pub fn provider_configs(&self) -> HashMap<String, ProviderConfigInput> {
1164 self.inner
1165 .snapshot
1166 .read()
1167 .map(|guard| guard.provider_configs.clone())
1168 .unwrap_or_default()
1169 }
1170
1171 #[must_use]
1173 pub fn stream_provider_ids(&self) -> HashSet<String> {
1174 self.inner
1175 .snapshot
1176 .read()
1177 .map(|guard| guard.stream_provider_ids.clone())
1178 .unwrap_or_default()
1179 }
1180
1181 #[must_use]
1183 pub fn provider_extension_paths(&self) -> HashMap<String, String> {
1184 self.inner
1185 .snapshot
1186 .read()
1187 .map(|guard| guard.provider_extension_paths.clone())
1188 .unwrap_or_default()
1189 }
1190
1191 #[must_use]
1193 pub fn registered_flag_types(
1194 &self,
1195 ) -> BTreeMap<String, super::agent_session_services::ExtensionFlagType> {
1196 use super::agent_session_services::ExtensionFlagType;
1197 self.inner
1198 .snapshot
1199 .read()
1200 .map(|guard| {
1201 guard
1202 .registry
1203 .flags()
1204 .iter()
1205 .map(|flag| {
1206 let kind = match flag.kind {
1207 adapters::FlagKind::Boolean => ExtensionFlagType::Boolean,
1208 adapters::FlagKind::String => ExtensionFlagType::String,
1209 };
1210 (flag.name.clone(), kind)
1211 })
1212 .collect()
1213 })
1214 .unwrap_or_default()
1215 }
1216
1217 #[must_use]
1225 pub fn providers(&self) -> HashMap<String, ExtensionProvider> {
1226 let client = Arc::clone(&self.inner.client);
1227 self.inner
1228 .snapshot
1229 .read()
1230 .map(|guard| {
1231 guard
1232 .registry
1233 .providers()
1234 .iter()
1235 .map(|provider| {
1236 (
1237 provider.name.clone(),
1238 ExtensionProvider::new(provider.name.clone(), Arc::clone(&client)),
1239 )
1240 })
1241 .collect()
1242 })
1243 .unwrap_or_default()
1244 }
1245
1246 #[must_use]
1251 pub fn register_providers_on(
1252 &self,
1253 runtime: &ModelRuntime,
1254 ) -> Vec<(String, Result<(), ModelRuntimeError>)> {
1255 let configs = self.provider_configs();
1256 let stream_ids = self.stream_provider_ids();
1257 let paths = self.provider_extension_paths();
1258 let mut results = Vec::with_capacity(configs.len());
1259 for (name, config) in configs {
1260 let path = paths.get(&name).cloned().unwrap_or_else(|| name.clone());
1261 let outcome = runtime.register_provider(&name, config);
1262 if outcome.is_ok() && stream_ids.contains(&name) {
1263 let adapter = ExtensionProvider::new(name.clone(), Arc::clone(self.client()));
1264 runtime.register_extension_stream_provider(name.clone(), Arc::new(adapter));
1265 }
1266 results.push((path, outcome));
1267 }
1268 results
1269 }
1270
1271 pub fn unregister_providers_from(&self, runtime: &ModelRuntime) {
1273 for name in self.provider_configs().keys() {
1274 runtime.unregister_provider(name);
1275 }
1276 }
1277
1278 #[must_use]
1281 pub fn registry(&self) -> Registry {
1282 self.inner
1283 .snapshot
1284 .read()
1285 .map(|guard| clone_registry(&guard.registry))
1286 .unwrap_or_default()
1287 }
1288
1289 #[must_use]
1293 pub fn raw_shortcuts(&self) -> Vec<ShortcutRegistration> {
1294 self.inner
1295 .snapshot
1296 .read()
1297 .map(|guard| guard.raw_shortcuts.clone())
1298 .unwrap_or_default()
1299 }
1300
1301 #[must_use]
1303 pub fn reload_generation(&self) -> u64 {
1304 self.inner.reload_generation.load(Ordering::Relaxed)
1305 }
1306
1307 #[must_use]
1309 pub fn is_running(&self) -> bool {
1310 self.inner.client.is_running() && !self.inner.disabled.load(Ordering::Relaxed)
1311 }
1312
1313 pub async fn apply_flag_values(
1327 &self,
1328 values: &BTreeMap<String, FlagValueWire>,
1329 ) -> Result<(), HostClientError> {
1330 let payload = protocol::to_payload(&FlagsSetRequest {
1331 values: values.clone(),
1332 })
1333 .map_err(|error| HostClientError::Payload(format!("encode flags.set: {error}")))?;
1334 let frame = self
1335 .inner
1336 .hook_request(protocol::FLAGS_SET_METHOD, payload)
1337 .await?;
1338 let response: FlagsSetResponse = protocol::from_payload(&frame.payload)
1339 .map_err(|error| HostClientError::Payload(format!("decode flags.set: {error}")))?;
1340 if !response.ok {
1341 return Err(HostClientError::Payload(
1342 "flags.set rejected by extension host".to_owned(),
1343 ));
1344 }
1345 if let Ok(mut flags) = self.inner.flag_values.write() {
1346 for (name, value) in values {
1347 let value = match value {
1348 FlagValueWire::Boolean(value) => Value::Bool(*value),
1349 FlagValueWire::String(value) => Value::String(value.clone()),
1350 };
1351 flags.insert(name.clone(), value);
1352 }
1353 }
1354 Ok(())
1355 }
1356
1357 pub async fn execute_shortcut(
1368 &self,
1369 key: impl Into<String>,
1370 ) -> Result<ShortcutExecuteResponse, HostClientError> {
1371 let payload =
1372 protocol::to_payload(&ShortcutExecuteRequest { key: key.into() }).map_err(|error| {
1373 HostClientError::Payload(format!("encode shortcut.execute: {error}"))
1374 })?;
1375 let frame = self
1376 .inner
1377 .hook_request(protocol::SHORTCUT_EXECUTE_METHOD, payload)
1378 .await?;
1379 protocol::from_payload(&frame.payload)
1380 .map_err(|error| HostClientError::Payload(format!("decode shortcut.execute: {error}")))
1381 }
1382
1383 pub async fn send_ui_event(
1394 &self,
1395 request: UiEventRequest,
1396 ) -> Result<UiEventResponse, HostClientError> {
1397 let payload = protocol::to_payload(&request)
1398 .map_err(|error| HostClientError::Payload(format!("encode uiEvent: {error}")))?;
1399 let frame = self
1400 .inner
1401 .hook_request(protocol::Method::UiEvent.as_str(), payload)
1402 .await?;
1403 protocol::from_payload(&frame.payload)
1404 .map_err(|error| HostClientError::Payload(format!("decode uiEvent: {error}")))
1405 }
1406
1407 #[must_use]
1413 pub fn subscribe_slot(&self, key: &str) -> watch::Receiver<Option<SanitizedSlot>> {
1414 if let Ok(mut slots) = self.inner.slots.write() {
1415 let sender = slots
1416 .entry(key.to_owned())
1417 .or_insert_with(|| watch::channel(None).0);
1418 return sender.subscribe();
1419 }
1420 let (tx, rx) = watch::channel(None);
1422 let _ = tx.send(None);
1423 rx
1424 }
1425
1426 #[must_use]
1428 pub fn slot_keys(&self) -> Vec<String> {
1429 self.inner
1430 .slots
1431 .read()
1432 .map(|guard| guard.keys().cloned().collect())
1433 .unwrap_or_default()
1434 }
1435
1436 #[must_use]
1441 pub fn current_slots(&self) -> Vec<SanitizedSlot> {
1442 let mut slots = self
1443 .inner
1444 .slots
1445 .read()
1446 .map(|guard| {
1447 guard
1448 .values()
1449 .filter_map(|sender| sender.borrow().clone())
1450 .collect::<Vec<_>>()
1451 })
1452 .unwrap_or_default();
1453 slots.sort_by(|left, right| left.key.cmp(&right.key));
1454 slots
1455 }
1456
1457 #[must_use]
1459 pub fn subscribe_tool_updates(&self) -> broadcast::Receiver<ToolUpdate> {
1460 self.inner.tool_updates_tx.subscribe()
1461 }
1462
1463 #[must_use]
1465 pub fn subscribe_provider_events(&self) -> broadcast::Receiver<ProviderEvent> {
1466 self.inner.provider_events_tx.subscribe()
1467 }
1468
1469 #[must_use]
1472 pub fn subscribe_errors(&self) -> broadcast::Receiver<ExtensionErrorEvent> {
1473 self.inner.errors_tx.subscribe()
1474 }
1475
1476 #[must_use]
1478 pub fn has_terminal_input_handlers(&self) -> bool {
1479 self.inner
1480 .snapshot
1481 .read()
1482 .is_ok_and(|snapshot| snapshot.terminal_input)
1483 }
1484
1485 pub async fn terminal_input(
1492 &self,
1493 data: &str,
1494 ) -> Result<protocol::TerminalInputResult, HostClientError> {
1495 let frame = self
1496 .inner
1497 .client
1498 .request(
1499 protocol::Method::TerminalInput,
1500 serde_json::json!({ "data": data }),
1501 Duration::from_millis(4),
1502 )
1503 .await?;
1504 protocol::from_payload(&frame.payload)
1505 .map_err(|error| HostClientError::Payload(format!("decode terminalInput: {error}")))
1506 }
1507
1508 #[must_use]
1510 pub fn subscribe_ui(&self) -> broadcast::Receiver<ExtensionUiEvent> {
1511 self.inner.ui_tx.subscribe()
1512 }
1513
1514 #[must_use]
1519 pub fn take_ui_requests(&self) -> Option<mpsc::Receiver<HostUiRequest>> {
1520 let receiver = self
1521 .inner
1522 .ui_requests_rx
1523 .lock()
1524 .unwrap_or_else(std::sync::PoisonError::into_inner)
1525 .take();
1526 if receiver.is_some() {
1527 self.inner
1528 .ui_requests_claimed
1529 .store(true, Ordering::Release);
1530 }
1531 receiver
1532 }
1533
1534 pub async fn respond_ui(&self, response: HostUiResponse) -> Result<(), HostClientError> {
1540 self.inner.client.respond_ui(response).await
1541 }
1542
1543 pub async fn render_extension_tool_html(
1558 &self,
1559 phase: ToolRenderPhase,
1560 tool_name: &str,
1561 payload: &Value,
1562 ) -> Option<String> {
1563 let _permit = self.inner.traffic.enter()?;
1564 if !self.inner.active() {
1565 return None;
1566 }
1567 let request = serde_json::json!({
1568 "phase": phase.as_str(),
1569 "toolName": tool_name,
1570 "payload": payload,
1571 });
1572 match self
1573 .inner
1574 .client
1575 .request_raw(TOOL_RENDER_HTML_METHOD, request, self.inner.hook_timeout)
1576 .await
1577 {
1578 Ok(frame) => match serde_json::from_value::<ToolRenderHtmlWire>(frame.payload) {
1579 Ok(wire) => wire.html.as_deref().map(sanitize_html),
1580 Err(_) => None,
1581 },
1582 Err(err) => {
1583 self.inner.report_host_error(&err);
1584 None
1585 }
1586 }
1587 }
1588
1589 pub async fn reload(&self) -> u64 {
1598 let generation = self
1599 .inner
1600 .reload_generation
1601 .fetch_add(1, Ordering::Relaxed)
1602 .saturating_add(1);
1603 Self::shutdown_once_with_inner(&self.inner).await;
1604 self.inner.stale.store(true, Ordering::Relaxed);
1605 generation
1606 }
1607
1608 pub async fn restart_and_rewire(
1622 &self,
1623 runtime: &ModelRuntime,
1624 preserved_flags: HashMap<String, Value>,
1625 ) -> Result<Arc<Self>, HostStartError> {
1626 self.restart_and_rewire_with(
1627 runtime,
1628 preserved_flags,
1629 |paths, cwd, project_trusted| async move {
1630 Self::start_with_cwd_and_trust(paths, cwd, project_trusted).await
1631 },
1632 )
1633 .await
1634 }
1635
1636 pub(crate) async fn restart_and_rewire_with<F, Fut>(
1637 &self,
1638 runtime: &ModelRuntime,
1639 preserved_flags: HashMap<String, Value>,
1640 start: F,
1641 ) -> Result<Arc<Self>, HostStartError>
1642 where
1643 F: FnOnce(Vec<String>, String, bool) -> Fut,
1644 Fut: std::future::Future<Output = Result<Arc<Self>, HostStartError>>,
1645 {
1646 let replacement = start(
1649 self.inner.extension_paths.clone(),
1650 self.inner.load_cwd.clone(),
1651 self.inner.project_trusted,
1652 )
1653 .await?;
1654 let preserved_flags = match preserved_flags
1656 .into_iter()
1657 .map(|(name, value)| {
1658 let value = match value {
1659 Value::Bool(value) => FlagValueWire::Boolean(value),
1660 Value::String(value) => FlagValueWire::String(value),
1661 other => {
1662 return Err(HostStartError::FlagSync(format!(
1663 "flag {name:?} has unsupported value {other}"
1664 )));
1665 }
1666 };
1667 Ok((name, value))
1668 })
1669 .collect::<Result<BTreeMap<_, _>, _>>()
1670 {
1671 Ok(flags) => flags,
1672 Err(error) => {
1673 replacement.shutdown_once().await;
1674 return Err(error);
1675 }
1676 };
1677 if let Err(error) = replacement.apply_flag_values(&preserved_flags).await {
1678 replacement.shutdown_once().await;
1679 return Err(HostStartError::FlagSync(error.to_string()));
1680 }
1681 self.unregister_providers_from(runtime);
1684 let _ = replacement.register_providers_on(runtime);
1685 Ok(replacement)
1691 }
1692
1693 pub async fn retire_after_cutover(&self) {
1702 self.inner.traffic.close();
1703 self.inner.stale.store(true, Ordering::Relaxed);
1704 let drain_deadline = self
1705 .inner
1706 .hook_timeout
1707 .saturating_add(Duration::from_secs(1));
1708 let _ = tokio::time::timeout(drain_deadline, self.inner.traffic.drain()).await;
1709 let _ = self.reload().await;
1710 }
1711
1712 pub async fn start_with_cwd(
1718 extension_paths: Vec<String>,
1719 load_cwd: impl Into<String>,
1720 ) -> Result<Arc<Self>, HostStartError> {
1721 Self::start_with_cwd_and_trust(extension_paths, load_cwd, false).await
1722 }
1723
1724 pub async fn start_with_cwd_and_trust(
1736 extension_paths: Vec<String>,
1737 load_cwd: impl Into<String>,
1738 project_trusted: bool,
1739 ) -> Result<Arc<Self>, HostStartError> {
1740 let spec = acquire::resolve_or_acquire(!extension_paths.is_empty()).await?;
1741 Self::spawn_with_cwd_and_trust(&spec, extension_paths, load_cwd, project_trusted).await
1742 }
1743
1744 pub async fn spawn_with_cwd_and_trust(
1751 spec: &HostSpec,
1752 extension_paths: Vec<String>,
1753 load_cwd: impl Into<String>,
1754 project_trusted: bool,
1755 ) -> Result<Arc<Self>, HostStartError> {
1756 let client =
1757 Arc::new(HostClient::spawn(spec).map_err(|e| HostStartError::Spawn(e.to_string()))?);
1758 let startup = Self::connect_with_cwd_and_trust(
1759 Arc::clone(&client),
1760 extension_paths,
1761 load_cwd,
1762 project_trusted,
1763 HOOK_TIMEOUT,
1764 )
1765 .await;
1766 Self::finish_startup(&client, startup).await
1767 }
1768
1769 pub fn invalidate(&self) {
1773 self.inner.stale.store(true, Ordering::Relaxed);
1774 self.inner.dispose_all_slots();
1775 }
1776
1777 pub async fn shutdown_once(&self) {
1781 Self::shutdown_once_with_inner(&self.inner).await;
1782 }
1783}
1784
1785fn clone_registry(source: &Registry) -> Registry {
1786 let mut copy = Registry::new();
1787 for tool in source.tools() {
1788 let _ = copy.register_tool(tool.clone());
1789 }
1790 for command in source.commands() {
1791 let _ = copy.register_command(command.clone());
1792 }
1793 for shortcut in source.shortcuts() {
1794 let _ = copy.register_shortcut(shortcut.clone());
1795 }
1796 for flag in source.flags() {
1797 let _ = copy.register_flag(flag.clone());
1798 }
1799 for renderer in source.renderers() {
1800 let _ = copy.register_renderer(renderer.clone());
1801 }
1802 for provider in source.providers() {
1803 let _ = copy.register_provider(provider.clone());
1804 }
1805 copy
1806}
1807
1808fn spawn_event_pump(inner: Arc<Inner>) {
1812 let mut rx = inner.client.subscribe();
1813 tokio::spawn(async move {
1814 loop {
1815 match rx.recv().await {
1816 Ok(HostEvent::UiRequest(request)) => {
1817 if !inner.active() || !inner.ui_requests_claimed.load(Ordering::Acquire) {
1818 let _ = inner.client.respond_ui(default_ui_response(&request)).await;
1819 } else if let Err(error) = inner.ui_requests_tx.send(request).await {
1820 let _ = inner.client.respond_ui(default_ui_response(&error.0)).await;
1821 }
1822 }
1823 Ok(HostEvent::Notify(notification)) => {
1824 inner.notify_send(notification);
1825 }
1826 Ok(HostEvent::UiSlot(slot)) => {
1827 forward_slot(&inner, &slot);
1828 }
1829 Ok(HostEvent::DisposeSlot(d)) => {
1830 forward_dispose(&inner, &d);
1831 }
1832 Ok(HostEvent::ToolUpdate(update)) => {
1833 let _ = inner.tool_updates_tx.send(update);
1834 }
1835 Ok(HostEvent::ProviderEvent(event)) => {
1836 let _ = inner.provider_events_tx.send(event);
1837 }
1838 Ok(HostEvent::ExtensionError(event)) => {
1839 let _ = inner.errors_tx.send(event);
1840 }
1841 Ok(HostEvent::Raw(frame)) => {
1842 inner.disabled.store(true, Ordering::Relaxed);
1843 inner.publish_error(
1844 "extension_protocol",
1845 &format!("unhandled host frame: {} {}", frame.kind, frame.method),
1846 None,
1847 );
1848 HostExtensionRunner::shutdown_once_with_inner(&inner).await;
1849 break;
1850 }
1851 Err(broadcast::error::RecvError::Lagged(skipped)) => {
1852 inner.publish_error(
1853 "extension_event_lagged",
1854 &format!("dropped {skipped} extension host events"),
1855 None,
1856 );
1857 }
1858 Ok(HostEvent::Eof) => {
1859 inner.disabled.store(true, Ordering::Relaxed);
1862 inner.publish_error("extension_closed", "extension host stream closed", None);
1863 HostExtensionRunner::shutdown_once_with_inner(&inner).await;
1864 break;
1865 }
1866 Ok(HostEvent::ProtocolError(message)) => {
1867 inner.disabled.store(true, Ordering::Relaxed);
1868 inner.publish_error("extension_protocol", &message, None);
1869 HostExtensionRunner::shutdown_once_with_inner(&inner).await;
1870 break;
1871 }
1872 Err(broadcast::error::RecvError::Closed) => break,
1873 }
1874 }
1875 });
1876}
1877
1878fn default_ui_response(request: &HostUiRequest) -> HostUiResponse {
1879 match request {
1880 HostUiRequest::Select { id, .. } => HostUiResponse::Select {
1881 id: *id,
1882 value: None,
1883 },
1884 HostUiRequest::Confirm { id, .. } => HostUiResponse::Confirm {
1885 id: *id,
1886 confirmed: false,
1887 },
1888 HostUiRequest::Input { id, .. } => HostUiResponse::Input {
1889 id: *id,
1890 value: None,
1891 },
1892 HostUiRequest::Editor { id, .. } => HostUiResponse::Editor {
1893 id: *id,
1894 value: None,
1895 },
1896 }
1897}
1898
1899fn forward_slot(inner: &Arc<Inner>, slot: &UiSlot) {
1900 let sanitized = sanitize_slot(slot);
1903 if sanitized.had_rejections {
1904 inner.publish_error(
1905 "extension_sanitized",
1906 "extension uiSlot contained rejected control sequences or oversized fields",
1907 None,
1908 );
1909 }
1910 inner.slot_send(sanitized);
1911}
1912
1913fn forward_dispose(inner: &Arc<Inner>, dispose: &DisposeSlot) {
1914 inner.slot_dispose(&dispose.key);
1915}
1916
1917fn sanitize_html(html: &str) -> String {
1922 let lower = html.to_ascii_lowercase();
1923 let mut kept = String::with_capacity(html.len());
1924 let mut cursor = 0usize;
1925 while cursor < html.len() {
1926 let script_rel = lower[cursor..].find("<script");
1927 let style_rel = lower[cursor..].find("<style");
1928 let (start, open_len, close_marker): (usize, usize, &str) = match (script_rel, style_rel) {
1930 (None, None) => {
1931 kept.push_str(&html[cursor..]);
1932 break;
1933 }
1934 (Some(rel), None) => (cursor + rel, "<script".len(), "</script>"),
1935 (None, Some(rel)) => (cursor + rel, "<style".len(), "</style>"),
1936 (Some(script), Some(style)) => {
1937 if script <= style {
1938 (cursor + script, "<script".len(), "</script>")
1939 } else {
1940 (cursor + style, "<style".len(), "</style>")
1941 }
1942 }
1943 };
1944 kept.push_str(&html[cursor..start]);
1946 let search_from = start + open_len;
1947 match lower[search_from..].find(close_marker) {
1948 Some(rel) => {
1949 cursor = search_from + rel + close_marker.len();
1950 }
1951 None => {
1952 cursor = html.len();
1954 }
1955 }
1956 }
1957 kept.replace('&', "&")
1960 .replace('<', "<")
1961 .replace('>', ">")
1962}
1963
1964fn compact_assistant_meta(message: &AssistantMessage) -> Value {
1966 let mut value = serde_json::to_value(message).unwrap_or_else(|_| Value::Object(Map::new()));
1967 if let Value::Object(object) = &mut value {
1968 object.remove("content");
1969 }
1970 value
1971}
1972
1973fn compact_assistant_block(message: &AssistantMessage, content_index: u64) -> Value {
1975 usize::try_from(content_index)
1976 .ok()
1977 .and_then(|index| message.content.get(index))
1978 .and_then(|content| serde_json::to_value(content).ok())
1979 .unwrap_or(Value::Null)
1980}
1981
1982fn compact_message_update_event(event: &AssistantMessageEvent) -> Value {
1983 use AssistantMessageEvent as Ev;
1984 let (kind, partial, content_index, delta, with_block) = match event {
1986 Ev::Start { partial } => ("start", partial, None, None, false),
1987 Ev::TextStart {
1988 content_index,
1989 partial,
1990 } => ("text_start", partial, Some(*content_index), None, true),
1991 Ev::TextDelta {
1992 content_index,
1993 delta,
1994 partial,
1995 } => (
1996 "text_delta",
1997 partial,
1998 Some(*content_index),
1999 Some(delta),
2000 false,
2001 ),
2002 Ev::TextEnd {
2003 content_index,
2004 partial,
2005 ..
2006 } => ("text_end", partial, Some(*content_index), None, true),
2007 Ev::ThinkingStart {
2008 content_index,
2009 partial,
2010 } => ("thinking_start", partial, Some(*content_index), None, true),
2011 Ev::ThinkingDelta {
2012 content_index,
2013 delta,
2014 partial,
2015 } => (
2016 "thinking_delta",
2017 partial,
2018 Some(*content_index),
2019 Some(delta),
2020 false,
2021 ),
2022 Ev::ThinkingEnd {
2023 content_index,
2024 partial,
2025 ..
2026 } => ("thinking_end", partial, Some(*content_index), None, true),
2027 Ev::ToolCallStart {
2028 content_index,
2029 partial,
2030 } => ("toolcall_start", partial, Some(*content_index), None, true),
2031 Ev::ToolCallDelta {
2032 content_index,
2033 delta,
2034 partial,
2035 } => (
2036 "toolcall_delta",
2037 partial,
2038 Some(*content_index),
2039 Some(delta),
2040 false,
2041 ),
2042 Ev::ToolCallEnd {
2043 content_index,
2044 partial,
2045 ..
2046 } => ("toolcall_end", partial, Some(*content_index), None, true),
2047 Ev::Done { reason, message } => {
2048 return serde_json::json!({
2049 "type": "done",
2050 "reason": reason,
2051 "final": message,
2052 });
2053 }
2054 Ev::Error { reason, error } => {
2055 return serde_json::json!({
2056 "type": "error",
2057 "reason": reason,
2058 "final": error,
2059 });
2060 }
2061 };
2062
2063 let mut object = Map::new();
2064 object.insert("type".to_owned(), Value::String(kind.to_owned()));
2065 object.insert("meta".to_owned(), compact_assistant_meta(partial));
2066 if let Some(content_index) = content_index {
2067 object.insert("contentIndex".to_owned(), Value::from(content_index));
2068 if with_block {
2069 object.insert(
2070 "block".to_owned(),
2071 compact_assistant_block(partial, content_index),
2072 );
2073 }
2074 }
2075 if let Some(delta) = delta {
2076 object.insert("delta".to_owned(), Value::String(delta.clone()));
2077 }
2078 Value::Object(object)
2079}
2080
2081impl ExtensionRunner for HostExtensionRunner {
2086 fn has_handlers(&self, event: &str) -> bool {
2087 self.inner.has_handlers(event)
2088 }
2089
2090 fn emit(
2091 &self,
2092 event: AgentSessionEvent,
2093 ) -> BoxFuture<
2094 '_,
2095 Result<Option<CancelResult>, super::agent_session::extension_runner::ExtensionRunnerError>,
2096 > {
2097 let inner = Arc::clone(&self.inner);
2098 Box::pin(async move {
2099 let method = match event.type_name() {
2100 "compaction_start" => "session_before_compact",
2101 "compaction_end" => "session_compact",
2102 "thinking_level_changed" => "thinking_level_select",
2103 name => name,
2104 };
2105 if !inner.has_handlers(method) {
2106 return Ok(None);
2107 }
2108 let payload =
2109 serde_json::to_value(&event).unwrap_or_else(|_| Value::Object(Map::new()));
2110 match inner.hook_request(method, payload).await {
2111 Ok(frame) => {
2112 let result = serde_json::from_value::<Option<CancelWire>>(frame.payload)
2113 .ok()
2114 .flatten()
2115 .map(|wire| CancelResult {
2116 cancel: wire.cancel,
2117 reason: wire.reason,
2118 });
2119 Ok(result)
2120 }
2121 Err(err) => {
2122 inner.report_host_error(&err);
2123 Ok(None)
2124 }
2125 }
2126 })
2127 }
2128
2129 fn emit_message_update_delta<'a>(
2130 &'a self,
2131 event: &'a AssistantMessageEvent,
2132 ) -> BoxFuture<
2133 'a,
2134 Result<Option<CancelResult>, super::agent_session::extension_runner::ExtensionRunnerError>,
2135 > {
2136 let inner = Arc::clone(&self.inner);
2137 let payload = serde_json::json!({
2138 "type": MESSAGE_UPDATE_DELTA_METHOD,
2139 "event": compact_message_update_event(event),
2140 });
2141 Box::pin(async move {
2142 if !inner.has_handlers("message_update") {
2143 return Ok(None);
2144 }
2145 match inner
2146 .hook_request(MESSAGE_UPDATE_DELTA_METHOD, payload)
2147 .await
2148 {
2149 Ok(frame) => {
2150 let result = serde_json::from_value::<Option<CancelWire>>(frame.payload)
2151 .ok()
2152 .flatten()
2153 .map(|wire| CancelResult {
2154 cancel: wire.cancel,
2155 reason: wire.reason,
2156 });
2157 Ok(result)
2158 }
2159 Err(error) => {
2160 inner.report_host_error(&error);
2161 Ok(None)
2162 }
2163 }
2164 })
2165 }
2166
2167 fn emit_message_end(
2168 &self,
2169 message: AgentMessage,
2170 ) -> BoxFuture<
2171 '_,
2172 Result<Option<AgentMessage>, super::agent_session::extension_runner::ExtensionRunnerError>,
2173 > {
2174 let inner = Arc::clone(&self.inner);
2175 Box::pin(async move {
2176 if !inner.has_handlers("message_end") {
2177 return Ok(None);
2178 }
2179 let payload = serde_json::to_value(&message).unwrap_or(Value::Null);
2180 match inner.hook_request("message_end", payload).await {
2181 Ok(frame) => {
2182 let replacement = serde_json::from_value::<MessageEndWire>(frame.payload)
2183 .ok()
2184 .and_then(|wire| wire.message);
2185 let role_matches = replacement
2188 .as_ref()
2189 .is_some_and(|replacement| replacement.role() == message.role());
2190 match replacement {
2191 Some(message) if role_matches => Ok(Some(message)),
2192 Some(_) => {
2193 inner.publish_error(
2194 "extension_message_end",
2195 "message_end handler returned a message with a different role",
2196 None,
2197 );
2198 Ok(None)
2199 }
2200 None => Ok(None),
2201 }
2202 }
2203 Err(err) => {
2204 inner.report_host_error(&err);
2205 Ok(None)
2206 }
2207 }
2208 })
2209 }
2210
2211 fn emit_tool_call(
2212 &self,
2213 tool_name: &str,
2214 tool_call_id: &str,
2215 input: Map<String, Value>,
2216 ) -> BoxFuture<
2217 '_,
2218 Result<
2219 Option<BeforeToolCallResult>,
2220 super::agent_session::extension_runner::ExtensionRunnerError,
2221 >,
2222 > {
2223 let inner = Arc::clone(&self.inner);
2224 let tool_name = tool_name.to_owned();
2225 let tool_call_id = tool_call_id.to_owned();
2226 Box::pin(async move {
2227 if !inner.has_handlers("tool_call") {
2228 return Ok(None);
2229 }
2230 let payload = serde_json::json!({
2231 "toolName": tool_name,
2232 "toolCallId": tool_call_id,
2233 "input": input,
2234 });
2235 match inner.hook_request("tool_call", payload).await {
2236 Ok(frame) => {
2237 let result =
2238 serde_json::from_value::<Option<BeforeToolCallWire>>(frame.payload)
2239 .ok()
2240 .flatten()
2241 .map(|wire| BeforeToolCallResult {
2242 block: wire.block,
2243 reason: wire.reason,
2244 });
2245 Ok(result)
2246 }
2247 Err(err) => {
2248 inner.report_host_error(&err);
2249 Ok(None)
2250 }
2251 }
2252 })
2253 }
2254
2255 fn emit_tool_result(
2256 &self,
2257 tool_name: &str,
2258 tool_call_id: &str,
2259 input: Map<String, Value>,
2260 content: Vec<ToolResultContent>,
2261 details: Value,
2262 is_error: bool,
2263 ) -> BoxFuture<
2264 '_,
2265 Result<
2266 Option<AfterToolCallResult>,
2267 super::agent_session::extension_runner::ExtensionRunnerError,
2268 >,
2269 > {
2270 let inner = Arc::clone(&self.inner);
2271 let tool_name = tool_name.to_owned();
2272 let tool_call_id = tool_call_id.to_owned();
2273 Box::pin(async move {
2274 if !inner.has_handlers("tool_result") {
2275 return Ok(None);
2276 }
2277 let payload = serde_json::json!({
2278 "toolName": tool_name,
2279 "toolCallId": tool_call_id,
2280 "input": input,
2281 "content": content,
2282 "details": details,
2283 "isError": is_error,
2284 });
2285 match inner.hook_request("tool_result", payload).await {
2286 Ok(frame) => {
2287 let result = serde_json::from_value::<Option<AfterToolCallWire>>(frame.payload)
2288 .ok()
2289 .flatten()
2290 .map(|wire| AfterToolCallResult {
2291 content: wire.content,
2292 details: wire.details,
2293 is_error: wire.is_error,
2294 terminate: wire.terminate,
2295 });
2296 Ok(result)
2297 }
2298 Err(err) => {
2299 inner.report_host_error(&err);
2300 Ok(None)
2301 }
2302 }
2303 })
2304 }
2305
2306 fn emit_input(
2307 &self,
2308 text: &str,
2309 images: Option<Value>,
2310 source: &str,
2311 streaming_behavior: Option<&str>,
2312 ) -> BoxFuture<
2313 '_,
2314 Result<InputTransformResult, super::agent_session::extension_runner::ExtensionRunnerError>,
2315 > {
2316 let inner = Arc::clone(&self.inner);
2317 let text = text.to_owned();
2318 let source = source.to_owned();
2319 let streaming_behavior = streaming_behavior.map(str::to_owned);
2320 Box::pin(async move {
2321 if !inner.has_handlers("input") {
2322 return Ok(InputTransformResult::default());
2323 }
2324 let payload = serde_json::json!({
2325 "text": text,
2326 "images": images,
2327 "source": source,
2328 "streamingBehavior": streaming_behavior,
2329 });
2330 match inner.hook_request("input", payload).await {
2331 Ok(frame) => {
2332 let (handled, mapped_text, mapped_images) =
2333 match serde_json::from_value::<InputTransformWire>(frame.payload) {
2334 Ok(InputTransformWire::Handled) => (true, None, None),
2335 Ok(InputTransformWire::Transform { text, images }) => {
2336 (false, Some(text), images)
2337 }
2338 _ => (false, None, None),
2339 };
2340 Ok(InputTransformResult {
2341 handled,
2342 text: mapped_text,
2343 images: mapped_images,
2344 })
2345 }
2346 Err(err) => {
2347 inner.report_host_error(&err);
2348 Ok(InputTransformResult::default())
2349 }
2350 }
2351 })
2352 }
2353
2354 fn emit_before_agent_start(
2355 &self,
2356 prompt: &str,
2357 images: Option<Value>,
2358 ) -> BoxFuture<
2359 '_,
2360 Result<
2361 Option<BeforeAgentStartResult>,
2362 super::agent_session::extension_runner::ExtensionRunnerError,
2363 >,
2364 > {
2365 let inner = Arc::clone(&self.inner);
2366 let prompt = prompt.to_owned();
2367 Box::pin(async move {
2368 if !inner.has_handlers("before_agent_start") {
2369 return Ok(None);
2370 }
2371 let payload = serde_json::json!({
2372 "prompt": prompt,
2373 "images": images,
2374 });
2375 match inner.hook_request("before_agent_start", payload).await {
2376 Ok(frame) => {
2377 let wire =
2378 serde_json::from_value::<Option<BeforeAgentStartWire>>(frame.payload)
2379 .ok()
2380 .flatten();
2381 Ok(wire.map(|wire| BeforeAgentStartResult {
2382 messages: wire.messages,
2383 system_prompt: wire.system_prompt,
2384 }))
2385 }
2386 Err(err) => {
2387 inner.report_host_error(&err);
2388 Ok(None)
2389 }
2390 }
2391 })
2392 }
2393
2394 fn emit_resources_discover(
2395 &self,
2396 cwd: &str,
2397 reason: &str,
2398 ) -> BoxFuture<
2399 '_,
2400 Result<
2401 ResourceExtensionPaths,
2402 super::agent_session::extension_runner::ExtensionRunnerError,
2403 >,
2404 > {
2405 let inner = Arc::clone(&self.inner);
2406 let cwd = cwd.to_owned();
2407 let reason = reason.to_owned();
2408 Box::pin(async move {
2409 if !inner.has_handlers("resources_discover") {
2410 return Ok(ResourceExtensionPaths::default());
2411 }
2412 let payload = serde_json::json!({ "cwd": cwd, "reason": reason });
2413 match inner.hook_request("resources_discover", payload).await {
2414 Ok(frame) => {
2415 let wire = serde_json::from_value::<ResourcesDiscoverWire>(frame.payload)
2416 .unwrap_or_default();
2417 let discovered = |paths: Option<Vec<ResourcePathWire>>| {
2418 paths
2419 .unwrap_or_default()
2420 .into_iter()
2421 .map(|entry| {
2422 ExtensionResourcePath::discovered(entry.path, &entry.extension_path)
2423 })
2424 .collect()
2425 };
2426 Ok(ResourceExtensionPaths {
2427 skill_paths: discovered(wire.skills),
2428 prompt_paths: discovered(wire.prompts),
2429 theme_paths: discovered(wire.themes),
2430 })
2431 }
2432 Err(err) => {
2433 inner.report_host_error(&err);
2434 Ok(ResourceExtensionPaths::default())
2435 }
2436 }
2437 })
2438 }
2439
2440 fn get_registered_commands(&self) -> Vec<String> {
2441 self.inner
2442 .snapshot
2443 .read()
2444 .map(|guard| {
2445 guard
2446 .registry
2447 .commands()
2448 .iter()
2449 .map(|command| command.name.clone())
2450 .collect()
2451 })
2452 .unwrap_or_default()
2453 }
2454
2455 fn execute_command(
2456 &self,
2457 name: &str,
2458 args: &str,
2459 ) -> BoxFuture<'_, Result<bool, super::agent_session::extension_runner::ExtensionRunnerError>>
2460 {
2461 let inner = Arc::clone(&self.inner);
2462 let name = name.to_owned();
2463 let args = args.to_owned();
2464 Box::pin(async move {
2465 if !inner.has_command(&name) {
2466 return Ok(false);
2467 }
2468 let Some(_permit) = inner.traffic.enter() else {
2469 return Ok(false);
2470 };
2471 if !inner.active() {
2472 return Ok(false);
2473 }
2474 let payload = serde_json::json!({ "name": name, "args": args });
2475 match inner
2476 .client
2477 .request_raw(COMMAND_EXECUTE_METHOD, payload, inner.hook_timeout)
2478 .await
2479 {
2480 Ok(frame) => {
2481 let ok = serde_json::from_value::<CommandExecuteWire>(frame.payload)
2482 .is_ok_and(|wire| wire.ok);
2483 Ok(ok)
2484 }
2485 Err(err) => {
2486 inner.report_host_error(&err);
2487 Ok(true)
2488 }
2489 }
2490 })
2491 }
2492
2493 fn get_all_registered_tools(&self) -> HashMap<String, Arc<dyn AgentTool>> {
2494 self.inner
2495 .snapshot
2496 .read()
2497 .map(|guard| guard.tools.clone())
2498 .unwrap_or_default()
2499 }
2500
2501 fn get_flag_values(&self) -> HashMap<String, Value> {
2502 self.inner
2503 .flag_values
2504 .read()
2505 .map(|guard| guard.clone())
2506 .unwrap_or_default()
2507 }
2508
2509 fn invalidate(&self) {
2510 HostExtensionRunner::invalidate(self);
2511 }
2512
2513 fn emit_error(&self, message: String) {
2514 self.inner.publish_error("extension_error", &message, None);
2515 }
2516}
2517
2518impl HostExtensionRunner {
2519 async fn shutdown_once_with_inner(inner: &Arc<Inner>) {
2520 let _guard = inner.shutdown_lock.lock().await;
2521 if inner.shutdown_done.load(Ordering::Relaxed) {
2522 return;
2523 }
2524 Self::reap_inner(inner).await;
2525 inner.shutdown_done.store(true, Ordering::Relaxed);
2526 }
2527
2528 async fn reap_inner(inner: &Arc<Inner>) {
2529 inner.disabled.store(true, Ordering::Relaxed);
2532 inner.dispose_all_slots();
2533 let _ = inner.client.shutdown().await;
2534 }
2535}
2536
2537pub mod acquire;
2539
2540#[cfg(test)]
2541mod tests;