1use std::collections::HashSet;
9use std::panic::AssertUnwindSafe;
10use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
11use std::time::Duration;
12
13use futures::FutureExt;
14use indexmap::{IndexMap, IndexSet};
15use oino_ai::{AgentMessage, ImageContent, JsonObject, JsonValue};
16use serde_json::json;
17
18use super::api::{
19 ArbiterRoles, CallContext, CommandSpec, ExtensionCallError, ExtensionId, ExtensionInstance,
20 FlagSpec, Registration, ShortcutSpec, ToolSpec,
21};
22use crate::events::{ExtensionDisabledReason, ExtensionErrorEvent, OinoEvent};
23use crate::session::panic_message;
24
25pub const HOOK_DEADLINE: Duration = Duration::from_secs(5);
27pub const TOOL_CALL_DEADLINE: Duration = Duration::from_secs(30);
29pub const SESSION_BEFORE_DEADLINE: Duration = Duration::from_secs(60);
31pub const MAX_MANIFEST_DEADLINE: Duration = Duration::from_secs(300);
33pub const ARBITER_DEADLINE: Duration = Duration::from_secs(2);
35
36#[must_use]
38pub fn deadline_for(kind: &str) -> Duration {
39 if kind == "tool_call" {
40 TOOL_CALL_DEADLINE
41 } else if kind.starts_with("session_before_") {
42 SESSION_BEFORE_DEADLINE
43 } else {
44 HOOK_DEADLINE
45 }
46}
47
48fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
49 mutex.lock().unwrap_or_else(PoisonError::into_inner)
50}
51
52pub trait ErrorSink: Send + Sync {
54 fn report(&self, error: ExtensionErrorEvent);
56
57 fn disabled(&self, event: OinoEvent);
60}
61
62#[derive(Clone, Debug, Default)]
64pub struct ExtensionState {
65 pub tools: IndexMap<String, ToolSpec>,
67 pub commands: Vec<CommandSpec>,
69 pub shortcuts: IndexMap<String, ShortcutSpec>,
71 pub flags: IndexMap<String, FlagSpec>,
73 pub events: IndexSet<String>,
75 pub renderers: Vec<JsonValue>,
77 pub apis: Vec<String>,
79 pub arbiter: ArbiterRoles,
81}
82
83impl From<Registration> for ExtensionState {
84 fn from(registration: Registration) -> Self {
85 Self {
86 tools: registration
87 .tools
88 .into_iter()
89 .map(|tool| (tool.name.clone(), tool))
90 .collect(),
91 commands: registration.commands,
92 shortcuts: registration
93 .shortcuts
94 .into_iter()
95 .map(|s| (s.id.clone(), s))
96 .collect(),
97 flags: registration
98 .flags
99 .into_iter()
100 .map(|f| (f.name.clone(), f))
101 .collect(),
102 events: registration.events.into_iter().collect(),
103 renderers: registration.renderers,
104 apis: registration.apis,
105 arbiter: registration.arbiter,
106 }
107 }
108}
109
110pub struct LoadedExtension {
112 pub instance: Arc<dyn ExtensionInstance>,
114 state: Mutex<ExtensionState>,
115}
116
117impl std::fmt::Debug for LoadedExtension {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_struct("LoadedExtension")
120 .field("id", &self.instance.id())
121 .finish_non_exhaustive()
122 }
123}
124
125impl LoadedExtension {
126 #[must_use]
128 pub fn new(instance: Arc<dyn ExtensionInstance>, registration: Registration) -> Self {
129 Self {
130 instance,
131 state: Mutex::new(registration.into()),
132 }
133 }
134
135 #[must_use]
137 pub fn state(&self) -> ExtensionState {
138 lock(&self.state).clone()
139 }
140
141 pub fn update<R>(&self, f: impl FnOnce(&mut ExtensionState) -> R) -> R {
143 f(&mut lock(&self.state))
144 }
145
146 fn subscribed(&self, kind: &str) -> bool {
147 lock(&self.state).events.contains(kind)
148 }
149}
150
151#[derive(Debug, Default)]
153pub struct ExtensionSet {
154 extensions: Vec<Arc<LoadedExtension>>,
155}
156
157impl ExtensionSet {
158 #[must_use]
160 pub fn new(extensions: Vec<Arc<LoadedExtension>>) -> Self {
161 Self { extensions }
162 }
163
164 #[must_use]
166 pub fn extensions(&self) -> &[Arc<LoadedExtension>] {
167 &self.extensions
168 }
169
170 #[must_use]
172 pub fn get(&self, id: &str) -> Option<&Arc<LoadedExtension>> {
173 self.extensions.iter().find(|e| e.instance.id() == id)
174 }
175
176 #[must_use]
178 pub fn len(&self) -> usize {
179 self.extensions.len()
180 }
181
182 #[must_use]
184 pub fn is_empty(&self) -> bool {
185 self.extensions.is_empty()
186 }
187}
188
189#[derive(Clone, Debug)]
191pub struct ResolvedCommand {
192 pub extension: Arc<LoadedExtension>,
194 pub spec: CommandSpec,
196 pub invocation_name: String,
198}
199
200#[derive(Clone, Debug)]
202pub struct RegisteredTool {
203 pub extension: Arc<LoadedExtension>,
205 pub spec: ToolSpec,
207}
208
209#[derive(Clone, Debug, PartialEq)]
211pub enum InputOutcome {
212 Continue,
214 Transform {
216 text: String,
218 images: Option<Vec<ImageContent>>,
220 },
221 Handled,
223}
224
225#[derive(Clone, Debug, PartialEq)]
227pub struct BeforeAgentStartOutcome {
228 pub messages: Vec<JsonValue>,
230 pub system_prompt_options: JsonValue,
232 pub system_prompt: Option<String>,
234}
235
236#[derive(Clone, Debug, PartialEq)]
238pub struct BoundaryOutcome {
239 pub entries: Vec<JsonValue>,
241 pub continue_run: bool,
243}
244
245#[derive(Clone, Debug, Default, PartialEq, Eq)]
247pub struct DiscoveredResources {
248 pub skill_paths: Vec<(String, String)>,
250 pub prompt_paths: Vec<(String, String)>,
252 pub theme_paths: Vec<(String, String)>,
254}
255
256#[derive(Clone)]
258pub struct ExtensionRunner {
259 set: Arc<ExtensionSet>,
260 cx: CallContext,
261 errors: Arc<dyn ErrorSink>,
262 flag_values: Arc<Mutex<IndexMap<String, JsonValue>>>,
263 stale: Arc<Mutex<Option<String>>>,
264 dialogs: Option<crate::ui::UiBridge>,
265 disabled: Arc<Mutex<IndexSet<ExtensionId>>>,
268}
269
270impl std::fmt::Debug for ExtensionRunner {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 f.debug_struct("ExtensionRunner")
273 .field("extensions", &self.set.len())
274 .field("session", &self.cx.session_id)
275 .finish_non_exhaustive()
276 }
277}
278
279pub const STALE_CONTEXT_MESSAGE: &str = "This extension ctx is stale after session replacement or reload. Do not use a captured Oino or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";
281
282impl ExtensionRunner {
283 #[must_use]
285 pub fn new(
286 set: Arc<ExtensionSet>,
287 cx: CallContext,
288 errors: Arc<dyn ErrorSink>,
289 flag_values: IndexMap<String, JsonValue>,
290 ) -> Self {
291 Self {
292 set,
293 cx,
294 errors,
295 flag_values: Arc::new(Mutex::new(flag_values)),
296 stale: Arc::new(Mutex::new(None)),
297 dialogs: None,
298 disabled: Arc::new(Mutex::new(IndexSet::new())),
299 }
300 }
301
302 #[must_use]
305 pub fn with_dialogs(mut self, ui: crate::ui::UiBridge) -> Self {
306 self.dialogs = Some(ui);
307 self
308 }
309
310 #[must_use]
312 pub fn set(&self) -> &Arc<ExtensionSet> {
313 &self.set
314 }
315
316 #[must_use]
318 pub fn call_context(&self) -> &CallContext {
319 &self.cx
320 }
321
322 pub fn set_call_context(&mut self, cx: CallContext) {
324 self.cx = cx;
325 }
326
327 pub fn invalidate(&self, message: Option<&str>) {
329 let mut stale = lock(&self.stale);
330 if stale.is_none() {
331 *stale = Some(message.unwrap_or(STALE_CONTEXT_MESSAGE).to_owned());
332 }
333 }
334
335 #[must_use]
337 pub fn stale_message(&self) -> Option<String> {
338 lock(&self.stale).clone()
339 }
340
341 pub fn emit_error(&self, extension_path: &str, event: &str, error: impl Into<String>) {
343 self.errors.report(ExtensionErrorEvent {
344 extension_path: extension_path.to_owned(),
345 event: event.to_owned(),
346 error: error.into(),
347 stack: None,
348 });
349 }
350
351 fn active(&self) -> impl Iterator<Item = &Arc<LoadedExtension>> {
352 self.set
353 .extensions()
354 .iter()
355 .filter(|extension| !self.is_disabled(extension.instance.id()))
356 }
357
358 fn handlers(&self, kind: &str) -> Vec<Arc<LoadedExtension>> {
359 self.active()
360 .filter(|extension| extension.subscribed(kind))
361 .cloned()
362 .collect()
363 }
364
365 #[must_use]
367 pub fn has_handlers(&self, kind: &str) -> bool {
368 self.active().any(|extension| extension.subscribed(kind))
369 }
370
371 #[must_use]
373 pub fn is_disabled(&self, id: &str) -> bool {
374 lock(&self.disabled).contains(id)
375 }
376
377 #[must_use]
379 pub fn disabled_extensions(&self) -> Vec<ExtensionId> {
380 lock(&self.disabled).iter().cloned().collect()
381 }
382
383 fn disable(&self, extension: &LoadedExtension, reason: ExtensionDisabledReason) {
388 let id = extension.instance.id().to_owned();
389 if !lock(&self.disabled).insert(id.clone()) {
390 return; }
392 if let Some(ui) = &self.dialogs {
393 ui.cancel_for(&id);
394 }
395 self.errors.disabled(OinoEvent::ExtensionDisabled {
396 session_id: self.cx.session_id.clone(),
397 extension_id: id,
398 reason,
399 });
400 }
401
402 async fn call(
403 &self,
404 extension: &LoadedExtension,
405 kind: &str,
406 payload: JsonValue,
407 ) -> Result<Option<JsonValue>, ExtensionCallError> {
408 let deadline = extension.instance.hook_deadline(kind).map_or_else(
409 || deadline_for(kind),
410 |d| d.min(MAX_MANIFEST_DEADLINE).max(deadline_for(kind)),
411 );
412 let id = extension.instance.id();
413 let waiting = || {
414 self.dialogs
415 .as_ref()
416 .is_some_and(|ui| ui.pending_for(id) > 0)
417 };
418 let outcome = AssertUnwindSafe(async {
423 let call = extension.instance.on_event(kind, payload, self.cx.clone());
424 crate::extensions::deadline::with_deadline(call, deadline, waiting).await
425 })
426 .catch_unwind()
427 .await;
428 match outcome {
429 Ok(Some(result)) => {
430 if let Err(error) = &result
431 && error.trap
432 {
433 self.disable(extension, ExtensionDisabledReason::Trap);
434 }
435 result
436 }
437 Ok(None) => {
438 if let Some(ui) = &self.dialogs {
439 ui.cancel_for(id);
440 }
441 Err(ExtensionCallError::new(format!(
442 "Extension handler for {kind} timed out after {}ms",
443 deadline.as_millis()
444 )))
445 }
446 Err(panic) => {
447 let error = ExtensionCallError {
448 message: format!("extension panicked: {}", panic_message(&*panic)),
449 trap: true,
450 };
451 self.disable(extension, ExtensionDisabledReason::Panic);
452 Err(error)
453 }
454 }
455 }
456
457 fn report(&self, extension: &LoadedExtension, kind: &str, error: &ExtensionCallError) {
458 self.emit_error(&extension.instance.path(), kind, error.message.clone());
459 }
460
461 pub async fn emit(&self, payload: JsonValue) -> Option<JsonValue> {
464 let kind = payload["type"].as_str().unwrap_or_default().to_owned();
465 let gate = kind.starts_with("session_before_");
466 let mut result = None;
467 for extension in self.handlers(&kind) {
468 match self.call(&extension, &kind, payload.clone()).await {
469 Ok(Some(value)) if gate && !value.is_null() => {
470 let cancel = value.get("cancel") == Some(&JsonValue::Bool(true));
471 result = Some(value);
472 if cancel {
473 return result;
474 }
475 }
476 Ok(_) => {}
477 Err(error) => self.report(&extension, &kind, &error),
478 }
479 }
480 result
481 }
482
483 pub async fn emit_consumable(&self, payload: JsonValue) -> bool {
486 let kind = payload["type"].as_str().unwrap_or_default().to_owned();
487 for extension in self.handlers(&kind) {
488 match self.call(&extension, &kind, payload.clone()).await {
489 Ok(Some(value)) if value.get("consume") == Some(&JsonValue::Bool(true)) => {
490 return true;
491 }
492 Ok(_) => {}
493 Err(error) => self.report(&extension, &kind, &error),
494 }
495 }
496 false
497 }
498
499 pub async fn emit_chain(&self, mut payload: JsonValue, field: &str) -> JsonValue {
503 let kind = payload["type"].as_str().unwrap_or_default().to_owned();
504 for extension in self.handlers(&kind) {
505 match self.call(&extension, &kind, payload.clone()).await {
506 Ok(Some(value)) => {
507 if let Some(next) = value.get(field) {
508 payload[field] = next.clone();
509 } else if !value.is_object() {
510 payload[field] = value;
511 }
512 }
513 Ok(None) => {}
514 Err(error) => self.report(&extension, &kind, &error),
515 }
516 }
517 payload[field].take()
518 }
519
520 pub async fn emit_tool_call(&self, mut payload: JsonValue) -> Option<JsonValue> {
523 let mut result: Option<JsonValue> = None;
524 for extension in self.handlers("tool_call") {
525 match self.call(&extension, "tool_call", payload.clone()).await {
526 Ok(Some(value)) if value.is_object() => {
527 if let Some(input) = value.get("input").filter(|v| v.is_object()) {
528 payload["input"] = input.clone();
529 }
530 let block = value.get("block") == Some(&JsonValue::Bool(true));
531 result = Some(value);
532 if block {
533 return result;
534 }
535 }
536 Ok(_) => {}
537 Err(error) => {
538 self.report(&extension, "tool_call", &error);
539 return Some(json!({
540 "block": true,
541 "reason": format!("Extension error: {}", error.message),
542 }));
543 }
544 }
545 }
546 if let (Some(result), Some(input)) = (result.as_mut(), payload.get("input"))
547 && result.get("input").is_some()
548 {
549 result["input"] = input.clone();
550 }
551 result
552 }
553
554 pub async fn emit_tool_result(&self, mut event: JsonObject) -> Option<JsonObject> {
556 let mut modified = false;
557 for extension in self.handlers("tool_result") {
558 let result = self
559 .call(&extension, "tool_result", JsonValue::Object(event.clone()))
560 .await;
561 let update = match result {
562 Ok(Some(JsonValue::Object(update))) => update,
563 Ok(_) => continue,
564 Err(error) => {
565 self.report(&extension, "tool_result", &error);
566 continue;
567 }
568 };
569 if let Some(content) = update.get("content").filter(|v| !v.is_null()) {
570 event.insert("content".into(), content.clone());
571 if update
572 .get("structuredContent")
573 .is_none_or(JsonValue::is_null)
574 {
575 event.shift_remove("structuredContent");
576 }
577 modified = true;
578 }
579 for key in ["details", "structuredContent", "isError", "usage"] {
580 if let Some(value) = update.get(key).filter(|v| !v.is_null()) {
581 event.insert(key.into(), value.clone());
582 modified = true;
583 }
584 }
585 }
586 modified.then(|| {
587 [
588 "content",
589 "details",
590 "structuredContent",
591 "isError",
592 "usage",
593 ]
594 .into_iter()
595 .filter_map(|key| event.get(key).map(|v| (key.to_owned(), v.clone())))
596 .collect()
597 })
598 }
599
600 pub async fn emit_message_end(&self, message: &AgentMessage) -> Option<AgentMessage> {
602 let mut current = message.clone();
603 let mut modified = false;
604 for extension in self.handlers("message_end") {
605 let payload = json!({"type": "message_end", "message": current});
606 match self.call(&extension, "message_end", payload).await {
607 Ok(Some(value)) => {
608 let Some(replacement) = value.get("message").filter(|m| !m.is_null()) else {
609 continue;
610 };
611 let mut replacement = replacement.clone();
612 let role = replacement.get("role").and_then(JsonValue::as_str);
613 if role != Some(role_of(¤t)) {
614 self.emit_error(
615 &extension.instance.path(),
616 "message_end",
617 "message_end handlers must return a message with the same role",
618 );
619 continue;
620 }
621 if replacement.get("content").is_none_or(JsonValue::is_null)
622 && matches!(role, Some("user" | "assistant" | "toolResult" | "custom"))
623 {
624 replacement["content"] = json!([]);
625 }
626 match serde_json::from_value::<AgentMessage>(replacement) {
627 Ok(parsed) => {
628 current = parsed;
629 modified = true;
630 }
631 Err(error) => {
632 self.emit_error(
633 &extension.instance.path(),
634 "message_end",
635 error.to_string(),
636 );
637 }
638 }
639 }
640 Ok(None) => {}
641 Err(error) => self.report(&extension, "message_end", &error),
642 }
643 }
644 modified.then_some(current)
645 }
646
647 pub async fn emit_input(
649 &self,
650 text: &str,
651 images: Option<&[ImageContent]>,
652 source: &str,
653 streaming_behavior: Option<&str>,
654 ) -> InputOutcome {
655 let mut current_text = text.to_owned();
656 let mut current_images: Option<Vec<ImageContent>> = images.map(<[ImageContent]>::to_vec);
657 let original_images = current_images.clone();
658 for extension in self.handlers("input") {
659 let mut payload = json!({"type": "input", "text": current_text, "source": source});
660 if let Some(images) = ¤t_images {
661 payload["images"] = images_json(images);
662 }
663 if let Some(behavior) = streaming_behavior {
664 payload["streamingBehavior"] = json!(behavior);
665 }
666 match self.call(&extension, "input", payload).await {
667 Ok(Some(value)) => match value.get("action").and_then(JsonValue::as_str) {
668 Some("handled") => return InputOutcome::Handled,
669 Some("transform") => {
670 if let Some(text) = value.get("text").and_then(JsonValue::as_str) {
671 text.clone_into(&mut current_text);
672 }
673 if let Some(images) = value
674 .get("images")
675 .filter(|v| !v.is_null())
676 .and_then(|v| serde_json::from_value(v.clone()).ok())
677 {
678 current_images = Some(images);
679 }
680 }
681 _ => {}
682 },
683 Ok(None) => {}
684 Err(error) => self.report(&extension, "input", &error),
685 }
686 }
687 if current_text != text || current_images != original_images {
688 InputOutcome::Transform {
689 text: current_text,
690 images: current_images,
691 }
692 } else {
693 InputOutcome::Continue
694 }
695 }
696
697 pub async fn emit_context(&self, messages: Vec<AgentMessage>) -> Vec<AgentMessage> {
699 let mut current = messages;
700 for extension in self.handlers("context") {
701 let visible: Vec<AgentMessage> = current
702 .iter()
703 .filter(|m| !matches!(m, AgentMessage::System(_)))
704 .cloned()
705 .collect();
706 let payload = json!({"type": "context", "messages": visible});
707 match self.call(&extension, "context", payload).await {
708 Ok(Some(value)) => {
709 let Some(returned) = value
710 .get("messages")
711 .filter(|v| !v.is_null())
712 .and_then(|v| serde_json::from_value::<Vec<AgentMessage>>(v.clone()).ok())
713 else {
714 continue;
715 };
716 if returned == visible {
717 continue;
718 }
719 let head = crate::session::current_system_message(¤t);
720 current = head
721 .map(AgentMessage::System)
722 .into_iter()
723 .chain(returned)
724 .collect();
725 }
726 Ok(None) => {}
727 Err(error) => self.report(&extension, "context", &error),
728 }
729 }
730 for extension in self.handlers("context_with_system") {
731 let had_head = matches!(current.first(), Some(AgentMessage::System(_)));
732 let payload = json!({"type": "context_with_system", "messages": current});
733 match self.call(&extension, "context_with_system", payload).await {
734 Ok(Some(value)) => {
735 if let Some(returned) = value
736 .get("messages")
737 .filter(|v| !v.is_null())
738 .and_then(|v| serde_json::from_value::<Vec<AgentMessage>>(v.clone()).ok())
739 {
740 current = returned;
741 }
742 if had_head && !matches!(current.first(), Some(AgentMessage::System(_))) {
743 self.emit_error(&extension.instance.path(), "context_with_system", "Handler removed the leading system message; the request has no prompt or initial tool declarations. Keep it at index 0 or replace a dropped prefix with getCurrentSystemMessage().");
744 }
745 }
746 Ok(None) => {}
747 Err(error) => self.report(&extension, "context_with_system", &error),
748 }
749 }
750 current
751 }
752
753 pub async fn emit_before_agent_start(
757 &self,
758 trigger: JsonObject,
759 options: JsonValue,
760 render: &(dyn Fn(&JsonValue) -> String + Send + Sync),
761 ) -> BeforeAgentStartOutcome {
762 let mut current = options;
763 let mut messages = Vec::new();
764 let mut forced = None;
765 for extension in self.handlers("before_agent_start") {
766 let mut payload = trigger.clone();
767 payload.insert("type".into(), json!("before_agent_start"));
768 payload.insert(
769 "systemPrompt".into(),
770 json!(forced.clone().unwrap_or_else(|| render(¤t))),
771 );
772 payload.insert("systemPromptOptions".into(), current.clone());
773 match self
774 .call(&extension, "before_agent_start", JsonValue::Object(payload))
775 .await
776 {
777 Ok(Some(value)) => {
778 if let Some(message) = value.get("message").filter(|m| m.is_object()) {
779 messages.push(message.clone());
780 }
781 if let Some(options) =
782 value.get("systemPromptOptions").filter(|o| o.is_object())
783 {
784 current = options.clone();
785 }
786 if let Some(prompt) = value.get("systemPrompt").and_then(JsonValue::as_str) {
787 forced = Some(prompt.to_owned());
788 }
789 }
790 Ok(None) => {}
791 Err(error) => self.report(&extension, "before_agent_start", &error),
792 }
793 }
794 BeforeAgentStartOutcome {
795 messages,
796 system_prompt_options: current,
797 system_prompt: forced,
798 }
799 }
800
801 pub async fn emit_resources_discover(&self, cwd: &str, reason: &str) -> DiscoveredResources {
803 let mut found = DiscoveredResources::default();
804 for extension in self.handlers("resources_discover") {
805 let payload = json!({"type": "resources_discover", "cwd": cwd, "reason": reason});
806 match self.call(&extension, "resources_discover", payload).await {
807 Ok(Some(value)) => {
808 let id = extension.instance.id().to_owned();
809 let collect = |key: &str, into: &mut Vec<(String, String)>| {
810 for path in value
811 .get(key)
812 .and_then(JsonValue::as_array)
813 .into_iter()
814 .flatten()
815 {
816 if let Some(path) = path.as_str() {
817 into.push((path.to_owned(), id.clone()));
818 }
819 }
820 };
821 collect("skillPaths", &mut found.skill_paths);
822 collect("promptPaths", &mut found.prompt_paths);
823 collect("themePaths", &mut found.theme_paths);
824 }
825 Ok(None) => {}
826 Err(error) => self.report(&extension, "resources_discover", &error),
827 }
828 }
829 found
830 }
831
832 pub async fn emit_boundary(
835 &self,
836 base: JsonObject,
837 context: &(dyn Fn(&[JsonValue]) -> Result<JsonValue, String> + Send + Sync),
838 ) -> BoundaryOutcome {
839 let kind = base
840 .get("type")
841 .and_then(JsonValue::as_str)
842 .unwrap_or_default()
843 .to_owned();
844 let mut entries: Vec<JsonValue> = Vec::new();
845 let mut continue_run = false;
846 let mut preview = context(&entries).unwrap_or(JsonValue::Null);
847 let mut valid = true;
848 for extension in self.handlers(&kind) {
849 let mut payload = base.clone();
850 payload.insert("entries".into(), JsonValue::Array(entries.clone()));
851 payload.insert("continue".into(), JsonValue::Bool(continue_run));
852 payload.insert("context".into(), preview.clone());
853 match self
854 .call(&extension, &kind, JsonValue::Object(payload))
855 .await
856 {
857 Ok(Some(value)) => {
858 if let Some(list) = value.get("entries").and_then(JsonValue::as_array) {
859 entries.clone_from(list);
860 }
861 if let Some(flag) = value.get("continue").and_then(JsonValue::as_bool) {
862 continue_run = flag;
863 }
864 }
865 Ok(None) => {}
866 Err(error) => self.report(&extension, &kind, &error),
867 }
868 match context(&entries) {
869 Ok(next) => {
870 preview = next;
871 valid = true;
872 }
873 Err(error) => {
874 valid = false;
875 self.emit_error(
876 &extension.instance.path(),
877 &kind,
878 format!("Invalid boundary entries: {error}"),
879 );
880 }
881 }
882 }
883 if valid {
884 BoundaryOutcome {
885 entries,
886 continue_run,
887 }
888 } else {
889 BoundaryOutcome {
890 entries: Vec::new(),
891 continue_run: false,
892 }
893 }
894 }
895
896 pub async fn emit_user_bash(&self, payload: JsonValue) -> Result<Option<JsonValue>, String> {
903 for extension in self.handlers("user_bash") {
904 match self.call(&extension, "user_bash", payload.clone()).await {
905 Ok(Some(value)) => {
906 if let Some(result) = value.get("result").filter(|r| r.is_object()) {
907 return Ok(Some(result.clone()));
908 }
909 }
910 Ok(None) => {}
911 Err(error) => {
912 self.report(&extension, "user_bash", &error);
913 return Err(error.message);
914 }
915 }
916 }
917 Ok(None)
918 }
919
920 pub async fn emit_cache_warming_decision(&self, payload: JsonValue, action: String) -> String {
922 let mut action = action;
923 for extension in self.handlers("cache_warming_decision") {
924 match self
925 .call(&extension, "cache_warming_decision", payload.clone())
926 .await
927 {
928 Ok(Some(value)) => {
929 if let Some(next) = value.get("action").and_then(JsonValue::as_str) {
930 next.clone_into(&mut action);
931 }
932 }
933 Ok(None) => {}
934 Err(error) => self.report(&extension, "cache_warming_decision", &error),
935 }
936 }
937 action
938 }
939
940 pub async fn arbiter_wait_reasons(&self) -> Vec<String> {
943 let sources: Vec<Arc<LoadedExtension>> = self
944 .active()
945 .filter(|e| e.state().arbiter.wait_source)
946 .cloned()
947 .collect();
948 let calls = sources.iter().map(|extension| {
949 let cx = self.cx.clone();
950 async move {
951 tokio::time::timeout(ARBITER_DEADLINE, extension.instance.wait_reason(cx))
952 .await
953 .ok()
954 .flatten()
955 }
956 });
957 futures::future::join_all(calls)
958 .await
959 .into_iter()
960 .flatten()
961 .filter(|reason| !reason.trim().is_empty())
962 .collect()
963 }
964
965 pub async fn arbiter_nudge(&self) -> Option<JsonValue> {
969 let providers: Vec<Arc<LoadedExtension>> = self
970 .active()
971 .filter(|e| e.state().arbiter.nudge_provider)
972 .cloned()
973 .collect();
974 let calls = providers.iter().map(|extension| {
975 let cx = self.cx.clone();
976 async move {
977 tokio::time::timeout(ARBITER_DEADLINE, extension.instance.propose_nudge(cx))
978 .await
979 .ok()
980 .flatten()
981 .filter(|proposal| proposal.get("customType").is_some_and(JsonValue::is_string))
982 }
983 });
984 let proposals = futures::future::join_all(calls).await;
985 let priority = |p: &JsonValue| p.get("priority").and_then(JsonValue::as_f64).unwrap_or(0.0);
986 let mut winner: Option<(usize, JsonValue)> = None;
987 for (index, proposal) in proposals.into_iter().enumerate() {
988 let Some(proposal) = proposal else { continue };
989 if winner
990 .as_ref()
991 .is_none_or(|(_, best)| priority(&proposal) > priority(best))
992 {
993 winner = Some((index, proposal));
994 }
995 }
996 let (index, proposal) = winner?;
997 let id = proposal
998 .get("id")
999 .and_then(JsonValue::as_str)
1000 .unwrap_or_default()
1001 .to_owned();
1002 providers[index]
1003 .instance
1004 .nudge_delivered(&id, self.cx.clone());
1005 Some(proposal)
1006 }
1007
1008 #[must_use]
1010 pub fn tools(&self) -> Vec<RegisteredTool> {
1011 let mut seen = HashSet::new();
1012 let mut tools = Vec::new();
1013 for extension in self.active() {
1014 for spec in extension.state().tools.into_values() {
1015 if seen.insert(spec.name.clone()) {
1016 tools.push(RegisteredTool {
1017 extension: Arc::clone(extension),
1018 spec,
1019 });
1020 }
1021 }
1022 }
1023 tools
1024 }
1025
1026 #[must_use]
1028 pub fn commands(&self) -> Vec<ResolvedCommand> {
1029 let mut all = Vec::new();
1030 for extension in self.active() {
1031 for spec in extension.state().commands {
1032 all.push((Arc::clone(extension), spec));
1033 }
1034 }
1035 let mut counts: IndexMap<String, usize> = IndexMap::new();
1036 for (_, spec) in &all {
1037 *counts.entry(spec.name.clone()).or_default() += 1;
1038 }
1039 let mut seen: IndexMap<String, usize> = IndexMap::new();
1040 let mut taken = HashSet::new();
1041 all.into_iter()
1042 .map(|(extension, spec)| {
1043 let occurrence = {
1044 let slot = seen.entry(spec.name.clone()).or_default();
1045 *slot += 1;
1046 *slot
1047 };
1048 let mut invocation_name = if counts.get(&spec.name).copied().unwrap_or(0) > 1 {
1049 format!("{}:{occurrence}", spec.name)
1050 } else {
1051 spec.name.clone()
1052 };
1053 let mut suffix = occurrence;
1054 while taken.contains(&invocation_name) {
1055 suffix += 1;
1056 invocation_name = format!("{}:{suffix}", spec.name);
1057 }
1058 taken.insert(invocation_name.clone());
1059 ResolvedCommand {
1060 extension,
1061 spec,
1062 invocation_name,
1063 }
1064 })
1065 .collect()
1066 }
1067
1068 #[must_use]
1070 pub fn command(&self, invocation_name: &str) -> Option<ResolvedCommand> {
1071 self.commands()
1072 .into_iter()
1073 .find(|command| command.invocation_name == invocation_name)
1074 }
1075
1076 #[must_use]
1078 pub fn flags(&self) -> IndexMap<String, FlagSpec> {
1079 let mut flags = IndexMap::new();
1080 for extension in self.active() {
1081 for (name, flag) in extension.state().flags {
1082 flags.entry(name).or_insert(flag);
1083 }
1084 }
1085 flags
1086 }
1087
1088 #[must_use]
1090 pub fn flag_value(&self, name: &str) -> Option<JsonValue> {
1091 lock(&self.flag_values)
1092 .get(name)
1093 .cloned()
1094 .or_else(|| self.flags().get(name).and_then(|f| f.default.clone()))
1095 }
1096
1097 pub fn set_flag_value(&self, name: &str, value: JsonValue) {
1099 lock(&self.flag_values).insert(name.to_owned(), value);
1100 }
1101
1102 #[must_use]
1104 pub fn flag_values(&self) -> IndexMap<String, JsonValue> {
1105 lock(&self.flag_values).clone()
1106 }
1107
1108 #[must_use]
1110 pub fn shortcuts(&self) -> Vec<(String, ShortcutSpec)> {
1111 let mut shortcuts: IndexMap<String, (String, ShortcutSpec)> = IndexMap::new();
1112 for extension in self.active() {
1113 for (id, spec) in extension.state().shortcuts {
1114 shortcuts.insert(id, (extension.instance.id().to_owned(), spec));
1115 }
1116 }
1117 shortcuts.into_values().collect()
1118 }
1119
1120 #[must_use]
1122 pub fn renderers(&self) -> Vec<(String, JsonValue)> {
1123 self.active()
1124 .flat_map(|extension| {
1125 let id = extension.instance.id().to_owned();
1126 extension
1127 .state()
1128 .renderers
1129 .into_iter()
1130 .map(move |spec| (id.clone(), spec))
1131 })
1132 .collect()
1133 }
1134
1135 #[must_use]
1137 pub fn provider_for_api(&self, api: &str) -> Option<Arc<LoadedExtension>> {
1138 self.set
1139 .extensions()
1140 .iter()
1141 .find(|extension| {
1142 extension.instance.provider().is_some()
1143 && extension.state().apis.iter().any(|a| a == api)
1144 })
1145 .cloned()
1146 }
1147}
1148
1149#[must_use]
1151pub fn role_of(message: &AgentMessage) -> &'static str {
1152 match message {
1153 AgentMessage::System(_) => "system",
1154 AgentMessage::User(_) => "user",
1155 AgentMessage::Assistant(_) => "assistant",
1156 AgentMessage::ToolResult(_) => "toolResult",
1157 AgentMessage::BashExecution(_) => "bashExecution",
1158 AgentMessage::Custom(_) => "custom",
1159 AgentMessage::BranchSummary(_) => "branchSummary",
1160 AgentMessage::CompactionSummary(_) => "compactionSummary",
1161 }
1162}
1163
1164fn images_json(images: &[ImageContent]) -> JsonValue {
1165 JsonValue::Array(
1166 images
1167 .iter()
1168 .map(|image| json!({"type": "image", "data": image.data, "mimeType": image.mime_type}))
1169 .collect(),
1170 )
1171}