1use std::path::PathBuf;
5use std::sync::Arc;
6use std::time::Duration;
7
8use futures::future::BoxFuture;
9use futures::stream::BoxStream;
10use oino_ai::{Model, ProviderEvent};
11use tokio_util::sync::CancellationToken;
12
13use crate::guest::{CompletionItem, InitInput, ToolInvocation};
14use crate::json::RawJson;
15use crate::manifest::Manifest;
16use crate::ops::{
17 ControlOp, ControlReply, DeviceOp, EditorOp, HostContextInfo, ModelsOp, ModelsReply, PanelOp,
18 ProviderHook, ProviderHookReply, SecretOp, SessionQuery, SessionWrite, SessionsIndexQuery,
19 SettingsLayer, SettingsScope, TaskOp,
20};
21use crate::registry::{
22 ArbiterRoles, CommandSpec, EventKind, FlagSpec, Registration, RendererSpec, ShortcutSpec,
23 ToolSpec,
24};
25use crate::types::{CallContext, ExtensionError, ExtensionId, HostError};
26
27pub type BoxFut<'a, T> = BoxFuture<'a, T>;
29
30pub struct DetachedBash {
32 pub command: String,
34 pub pid: Option<u32>,
36 pub output_so_far: String,
38 pub full_output_path: Option<String>,
40 pub output: BoxStream<'static, Vec<u8>>,
42 pub exit: BoxFuture<'static, Option<i32>>,
44 pub kill: CancellationToken,
46}
47
48impl std::fmt::Debug for DetachedBash {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("DetachedBash")
51 .field("command", &self.command)
52 .field("pid", &self.pid)
53 .finish_non_exhaustive()
54 }
55}
56
57#[derive(Clone, Debug)]
59pub enum ComponentSource {
60 Bytes(Arc<[u8]>),
62 Path(PathBuf),
64}
65
66#[derive(Clone, Debug)]
68pub struct LoadedPackage {
69 pub manifest: Manifest,
71 pub component: ComponentSource,
73 pub root: Option<PathBuf>,
75 pub bundled: bool,
77}
78
79pub trait ExtensionRuntime: Send + Sync {
81 fn load(
83 &self,
84 package: LoadedPackage,
85 host: Arc<dyn HostServices>,
86 ) -> BoxFut<'_, Result<Arc<dyn ExtensionInstance>, ExtensionError>>;
87}
88
89pub trait ExtensionInstance: Send + Sync {
91 fn id(&self) -> &ExtensionId;
93 fn manifest(&self) -> &Manifest;
95 fn init(
97 &self,
98 input: InitInput,
99 cx: CallContext,
100 ) -> BoxFut<'_, Result<Registration, ExtensionError>>;
101 fn on_event(
103 &self,
104 kind: EventKind,
105 payload: RawJson,
106 cx: CallContext,
107 ) -> BoxFut<'_, Result<Option<RawJson>, ExtensionError>>;
108 fn run_tool(
110 &self,
111 inv: ToolInvocation,
112 cancel: CancellationToken,
113 cx: CallContext,
114 ) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
115 fn prepare_arguments(&self, tool: &str, raw: RawJson) -> Result<RawJson, ExtensionError>;
121 fn prepare_loadout(
127 &self,
128 tool: &str,
129 loadout: RawJson,
130 ) -> Result<Option<RawJson>, ExtensionError>;
131 fn reaches_tools(&self, tool: &str, input: RawJson) -> Result<RawJson, ExtensionError>;
137 fn run_command(
139 &self,
140 name: &str,
141 args: &str,
142 cancel: CancellationToken,
143 cx: CallContext,
144 ) -> BoxFut<'_, Result<Option<RawJson>, ExtensionError>>;
145 fn complete_command(
147 &self,
148 name: &str,
149 prefix: &str,
150 cx: CallContext,
151 ) -> BoxFut<'_, Vec<CompletionItem>>;
152 fn run_shortcut(&self, id: &str, cx: CallContext) -> BoxFut<'_, Result<(), ExtensionError>>;
154 fn handle_service(
156 &self,
157 name: &str,
158 payload: RawJson,
159 caller: &ExtensionId,
160 cx: CallContext,
161 ) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
162 fn propose_nudge(&self, cx: CallContext) -> BoxFut<'_, Option<RawJson>>;
164 fn nudge_delivered(&self, id: &str, cx: CallContext);
166 fn wait_reason(&self, cx: CallContext) -> BoxFut<'_, Option<String>>;
168 fn render(&self, request: RawJson, cx: CallContext) -> Option<RawJson>;
170 fn route_model(
172 &self,
173 request: RawJson,
174 cx: CallContext,
175 ) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
176 fn complete_input(&self, request: RawJson, cx: CallContext) -> BoxFut<'_, Option<RawJson>>;
178 fn provider(&self) -> Option<&dyn ProviderInstance>;
180 fn shutdown(&self, reason: &str) -> BoxFut<'_, ()>;
182}
183
184pub type ProviderEventStream = BoxStream<'static, Result<ProviderEvent, ExtensionError>>;
186
187pub trait ProviderInstance: Send + Sync {
189 fn stream(&self, request: RawJson) -> BoxFut<'_, Result<ProviderEventStream, ExtensionError>>;
191 fn list_models(
193 &self,
194 provider_id: &str,
195 credential: Option<RawJson>,
196 ) -> BoxFut<'_, Result<Vec<Model>, ExtensionError>>;
197 fn oauth_login(
199 &self,
200 provider_id: &str,
201 cx: CallContext,
202 ) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
203 fn oauth_refresh(
205 &self,
206 provider_id: &str,
207 credential: RawJson,
208 ) -> BoxFut<'_, Result<RawJson, ExtensionError>>;
209}
210
211pub trait AgentHandle: Send + Sync {
213 fn id(&self) -> String;
215 fn session_file(&self) -> Option<String>;
217 fn prompt(
219 &self,
220 text: String,
221 delivery: Option<String>,
222 ) -> BoxFut<'_, Result<String, HostError>>;
223 fn steer(&self, text: String) -> BoxFut<'_, Result<(), HostError>>;
225 fn follow_up(&self, text: String) -> BoxFut<'_, Result<(), HostError>>;
227 fn abort(&self);
229 fn set_thinking(&self, level: &str) -> Result<String, HostError>;
235 fn last_assistant_text(&self) -> Option<String>;
237 fn wait_idle(&self) -> BoxFut<'_, Result<(), HostError>>;
239 fn usage(&self) -> RawJson;
241 fn events(&self) -> BoxStream<'static, String>;
243 fn close(&self, grace: Duration) -> BoxFut<'_, ()>;
245}
246
247macro_rules! unsupported {
248 ($name:literal) => {
249 Err(HostError::unsupported($name))
250 };
251}
252
253#[allow(unused_variables, reason = "default bodies ignore their arguments")]
257pub trait HostServices: Send + Sync {
258 fn subscribe(&self, ext: &ExtensionId, events: &[EventKind]) {}
261 fn unsubscribe(&self, ext: &ExtensionId, events: &[EventKind]) {}
263 fn register_tool(&self, ext: &ExtensionId, spec: ToolSpec) -> Result<(), HostError> {
269 unsupported!("registry.register-tool")
270 }
271 fn unregister_tool(&self, ext: &ExtensionId, name: &str) {}
273 fn register_command(&self, ext: &ExtensionId, spec: CommandSpec) -> Result<String, HostError> {
279 unsupported!("registry.register-command")
280 }
281 fn unregister_command(&self, ext: &ExtensionId, name: &str) {}
283 fn register_shortcut(&self, ext: &ExtensionId, spec: ShortcutSpec) -> Result<(), HostError> {
289 unsupported!("registry.register-shortcut")
290 }
291 fn unregister_shortcut(&self, ext: &ExtensionId, id: &str) {}
293 fn register_flag(&self, ext: &ExtensionId, spec: FlagSpec) -> Result<(), HostError> {
299 unsupported!("registry.register-flag")
300 }
301 fn get_flag(&self, ext: &ExtensionId, name: &str) -> Option<RawJson> {
303 None
304 }
305 fn register_renderer(&self, ext: &ExtensionId, spec: RendererSpec) {}
307 fn register_service(&self, ext: &ExtensionId, name: &str) -> Result<(), HostError> {
313 unsupported!("registry.register-service")
314 }
315 fn set_arbiter_roles(&self, ext: &ExtensionId, roles: ArbiterRoles) {}
317 fn register_settings(&self, ext: &ExtensionId, ns: RawJson) -> Result<(), HostError> {
323 unsupported!("registry.register-settings")
324 }
325 fn extension_settings(
331 &self,
332 ext: &ExtensionId,
333 namespace: &str,
334 layer: SettingsLayer,
335 ) -> Result<RawJson, HostError> {
336 unsupported!("registry.extension-settings")
337 }
338 fn set_extension_setting(
344 &self,
345 ext: &ExtensionId,
346 namespace: &str,
347 key: &str,
348 value: Option<RawJson>,
349 scope: SettingsScope,
350 ) -> Result<(), HostError> {
351 unsupported!("registry.set-extension-setting")
352 }
353 fn register_provider(&self, ext: &ExtensionId, cfg: RawJson) -> Result<(), HostError> {
359 unsupported!("registry.register-provider")
360 }
361 fn unregister_provider(&self, ext: &ExtensionId, id: &str) {}
363 fn register_virtual_model(&self, ext: &ExtensionId, def: RawJson) -> Result<(), HostError> {
369 unsupported!("registry.register-virtual-model")
370 }
371 fn unregister_virtual_model(&self, ext: &ExtensionId, provider: &str, id: &str) {}
373
374 fn session_read(&self, cx: &CallContext, q: SessionQuery) -> Result<RawJson, HostError> {
381 unsupported!("session reads")
382 }
383 fn session_write(
385 &self,
386 ext: &ExtensionId,
387 cx: &CallContext,
388 w: SessionWrite,
389 ) -> BoxFut<'_, Result<Option<String>, HostError>> {
390 Box::pin(async { unsupported!("session writes") })
391 }
392 fn sessions_index(
394 &self,
395 ext: &ExtensionId,
396 q: SessionsIndexQuery,
397 ) -> BoxFut<'_, Result<RawJson, HostError>> {
398 Box::pin(async { unsupported!("session.sessions-list") })
399 }
400
401 fn control(
404 &self,
405 ext: &ExtensionId,
406 cx: &CallContext,
407 op: ControlOp,
408 ) -> BoxFut<'_, Result<ControlReply, HostError>> {
409 let name = op.name();
410 Box::pin(async move { Err(HostError::unsupported(&format!("control.{name}"))) })
411 }
412 fn tool_progress(&self, cx: &CallContext, tool_call_id: &str, partial: RawJson) {}
414
415 fn models(
418 &self,
419 ext: &ExtensionId,
420 cx: &CallContext,
421 op: ModelsOp,
422 ) -> BoxFut<'_, Result<ModelsReply, HostError>> {
423 Box::pin(async { unsupported!("models") })
424 }
425
426 fn ui_dialog(
429 &self,
430 ext: &ExtensionId,
431 cx: &CallContext,
432 req: RawJson,
433 cancel: Option<CancellationToken>,
434 ) -> BoxFut<'_, Result<RawJson, HostError>> {
435 Box::pin(async { unsupported!("ui.dialog") })
436 }
437 fn ui_fire(&self, ext: &ExtensionId, cx: &CallContext, req: RawJson) -> Result<(), HostError> {
443 unsupported!("ui.fire")
444 }
445 fn ui_panel(
451 &self,
452 ext: &ExtensionId,
453 cx: &CallContext,
454 op: PanelOp,
455 ) -> Result<Option<String>, HostError> {
456 unsupported!("ui panels")
457 }
458 fn ui_editor(&self, cx: &CallContext, op: EditorOp) -> Result<Option<String>, HostError> {
464 unsupported!("ui editor")
465 }
466 fn ui_device(
468 &self,
469 ext: &ExtensionId,
470 cx: &CallContext,
471 op: DeviceOp,
472 ) -> BoxFut<'_, Result<Option<RawJson>, HostError>> {
473 Box::pin(async { unsupported!("ui device") })
474 }
475
476 fn agent_spawn(
479 &self,
480 ext: &ExtensionId,
481 cx: &CallContext,
482 spec: RawJson,
483 ) -> BoxFut<'_, Result<Arc<dyn AgentHandle>, HostError>> {
484 Box::pin(async { unsupported!("agents") })
485 }
486 fn agents_list(&self, cx: &CallContext) -> RawJson {
488 RawJson::from_json_value(&serde_json::Value::Array(Vec::new()))
489 }
490
491 fn take_bash(
497 &self,
498 ext: &ExtensionId,
499 cx: &CallContext,
500 tool_call_id: &str,
501 task_id: &str,
502 reason: &str,
503 ) -> BoxFut<'_, Result<DetachedBash, HostError>> {
504 Box::pin(async { unsupported!("bash detach") })
505 }
506
507 fn tasks(&self, ext: &ExtensionId, cx: &CallContext, op: TaskOp) -> Result<RawJson, HostError> {
514 unsupported!("tasks")
515 }
516
517 fn bus_publish(
520 &self,
521 ext: &ExtensionId,
522 cx: &CallContext,
523 topic: &str,
524 data: RawJson,
525 sticky: bool,
526 ) {
527 }
528 fn bus_subscribe(&self, ext: &ExtensionId, cx: &CallContext, pattern: &str, on: bool) {}
530 fn bus_get(&self, cx: &CallContext, topic: &str) -> Option<RawJson> {
532 None
533 }
534 fn service_call(
536 &self,
537 ext: &ExtensionId,
538 cx: &CallContext,
539 target: Option<&ExtensionId>,
540 name: &str,
541 payload: RawJson,
542 timeout: Duration,
543 ) -> BoxFut<'_, Result<RawJson, HostError>> {
544 Box::pin(async { unsupported!("services.call") })
545 }
546 fn services_list(&self) -> Vec<(ExtensionId, String)> {
548 Vec::new()
549 }
550 fn secret(&self, ext: &ExtensionId, op: SecretOp) -> Result<Option<String>, HostError> {
556 unsupported!("secrets")
557 }
558
559 fn provider_hook(
562 &self,
563 ext: &ExtensionId,
564 hook: ProviderHook,
565 ) -> BoxFut<'_, ProviderHookReply> {
566 let reply = hook.passthrough();
567 Box::pin(async move { reply })
568 }
569
570 fn host_context(&self, cx: &CallContext) -> HostContextInfo {
573 HostContextInfo::default()
574 }
575
576 fn host_service(
583 &self,
584 ext: &ExtensionId,
585 cx: &CallContext,
586 name: &str,
587 op: &str,
588 payload: RawJson,
589 cancel: CancellationToken,
590 ) -> BoxFut<'_, Result<RawJson, HostError>> {
591 Box::pin(async { unsupported!("host-service.call") })
592 }
593 fn host_services(&self) -> Vec<String> {
597 Vec::new()
598 }
599
600 fn deliver_event(
603 &self,
604 ext: &ExtensionId,
605 session_id: &str,
606 kind: EventKind,
607 payload: RawJson,
608 ) {
609 }
610}