1use std::path::PathBuf;
8
9use futures::future::BoxFuture;
10use futures::stream::BoxStream;
11use oino_agent::{AgentToolResult, ToolInvocation, UpdateSink};
12use oino_ai::{JsonObject, JsonValue, Model, ProviderEvent};
13use serde::{Deserialize, Serialize};
14use tokio_util::sync::CancellationToken;
15
16pub type ExtensionId = String;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum ExtensionKind {
23 Provider,
25 #[default]
27 Extension,
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "lowercase")]
33pub enum ExtensionMode {
34 Tui,
36 Rpc,
38 Json,
40 #[default]
42 Print,
43 Acp,
45 Desktop,
47 Mobile,
49}
50
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(rename_all = "lowercase")]
54pub enum CallKind {
55 #[default]
57 Event,
58 Tool,
60 Command,
62 Shortcut,
64 Service,
66 Render,
68}
69
70#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct CallContext {
74 pub session_id: String,
76 pub mode: ExtensionMode,
78 pub has_ui: bool,
80 pub cwd: PathBuf,
82 pub is_child: bool,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub child_role: Option<String>,
87 pub call_kind: CallKind,
89}
90
91impl CallContext {
92 #[must_use]
94 pub fn with_kind(&self, call_kind: CallKind) -> Self {
95 Self {
96 call_kind,
97 ..self.clone()
98 }
99 }
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
104#[error("{message}")]
105pub struct ExtensionCallError {
106 pub message: String,
108 pub trap: bool,
110}
111
112impl ExtensionCallError {
113 #[must_use]
115 pub fn new(message: impl Into<String>) -> Self {
116 Self {
117 message: message.into(),
118 trap: false,
119 }
120 }
121}
122
123#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
125#[serde(rename_all = "kebab-case")]
126pub enum ToolExposure {
127 #[default]
129 Direct,
130 ModelOnly,
132 Codemode,
134 Deferred,
136 Hidden,
138}
139
140#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct ToolSpec {
144 pub name: String,
146 #[serde(default)]
148 pub label: String,
149 pub description: String,
151 pub parameters: JsonValue,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub prompt_snippet: Option<String>,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub prompt_guidelines: Vec<String>,
159 #[serde(default)]
161 pub exposure: ToolExposure,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub default_active: Option<bool>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub execution_mode: Option<oino_agent::ExecutionMode>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub namespace: Option<JsonValue>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub annotations: Option<JsonValue>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub reaches_tools: Option<JsonValue>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub output_schema: Option<JsonValue>,
180 #[serde(default)]
182 pub prepare_arguments: bool,
183}
184
185#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "camelCase")]
188pub struct CommandSpec {
189 pub name: String,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub description: Option<String>,
194 #[serde(default)]
196 pub hidden: bool,
197 #[serde(default)]
199 pub completions: bool,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct ShortcutSpec {
206 pub id: String,
208 #[serde(default)]
210 pub default_keys: Vec<String>,
211 #[serde(default)]
213 pub description: String,
214}
215
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "lowercase")]
219pub enum FlagType {
220 Boolean,
222 String,
224}
225
226#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct FlagSpec {
230 pub name: String,
232 #[serde(rename = "type")]
234 pub kind: FlagType,
235 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub default: Option<JsonValue>,
238 #[serde(default)]
240 pub description: String,
241}
242
243#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
245#[serde(rename_all = "camelCase")]
246pub struct Registration {
247 #[serde(default)]
249 pub tools: Vec<ToolSpec>,
250 #[serde(default)]
252 pub commands: Vec<CommandSpec>,
253 #[serde(default)]
255 pub shortcuts: Vec<ShortcutSpec>,
256 #[serde(default)]
258 pub flags: Vec<FlagSpec>,
259 #[serde(default)]
261 pub events: Vec<String>,
262 #[serde(default)]
264 pub providers: Vec<JsonValue>,
265 #[serde(default)]
267 pub virtual_models: Vec<JsonValue>,
268 #[serde(default)]
270 pub renderers: Vec<JsonValue>,
271 #[serde(default)]
273 pub apis: Vec<String>,
274 #[serde(default)]
276 pub arbiter: ArbiterRoles,
277 #[serde(default)]
279 pub settings: Vec<JsonValue>,
280}
281
282#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct ArbiterRoles {
286 #[serde(default)]
288 pub nudge_provider: bool,
289 #[serde(default)]
291 pub wait_source: bool,
292}
293
294#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
296pub struct CompletionItem {
297 pub value: String,
299 pub label: String,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub description: Option<String>,
304}
305
306pub trait ProviderInstance: Send + Sync {
308 fn stream(
311 &self,
312 request: JsonValue,
313 cancel: CancellationToken,
314 ) -> BoxFuture<
315 '_,
316 Result<BoxStream<'static, Result<ProviderEvent, ExtensionCallError>>, ExtensionCallError>,
317 >;
318
319 fn list_models(
321 &self,
322 provider_id: &str,
323 credential: Option<JsonValue>,
324 ) -> BoxFuture<'_, Result<Vec<Model>, ExtensionCallError>>;
325
326 fn oauth_login(
329 &self,
330 provider_id: &str,
331 cx: CallContext,
332 ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
333 let _ = (provider_id, cx);
334 Box::pin(async { Err(ExtensionCallError::new("OAuth sign-in is not supported")) })
335 }
336
337 fn oauth_refresh(
339 &self,
340 provider_id: &str,
341 credential: JsonValue,
342 ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
343 let _ = (provider_id, credential);
344 Box::pin(async { Err(ExtensionCallError::new("OAuth refresh is not supported")) })
345 }
346}
347
348#[derive(Clone, Debug, Default, PartialEq, Eq)]
350pub struct PackageResourceDirs {
351 pub skills: Vec<PathBuf>,
353 pub prompts: Vec<PathBuf>,
355 pub themes: Vec<PathBuf>,
357}
358
359pub trait ExtensionInstance: Send + Sync {
361 fn id(&self) -> &str;
363
364 fn kind(&self) -> ExtensionKind {
366 ExtensionKind::Extension
367 }
368
369 fn path(&self) -> String {
371 self.id().to_owned()
372 }
373
374 fn resource_dirs(&self) -> PackageResourceDirs {
377 PackageResourceDirs::default()
378 }
379
380 fn init(&self, cx: CallContext) -> BoxFuture<'_, Result<Registration, ExtensionCallError>>;
382
383 fn on_event(
385 &self,
386 kind: &str,
387 payload: JsonValue,
388 cx: CallContext,
389 ) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;
390
391 fn run_tool(
393 &self,
394 tool: &str,
395 invocation: ToolInvocation,
396 updates: UpdateSink,
397 cancel: CancellationToken,
398 cx: CallContext,
399 ) -> BoxFuture<'_, Result<AgentToolResult, ExtensionCallError>>;
400
401 fn prepare_arguments(&self, _tool: &str, raw: JsonObject) -> JsonObject {
403 raw
404 }
405
406 fn run_command(
408 &self,
409 name: &str,
410 args: &str,
411 cancel: CancellationToken,
412 cx: CallContext,
413 ) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;
414
415 fn complete_command(
417 &self,
418 _name: &str,
419 _prefix: &str,
420 _cx: CallContext,
421 ) -> BoxFuture<'_, Vec<CompletionItem>> {
422 Box::pin(async { Vec::new() })
423 }
424
425 fn run_shortcut(
427 &self,
428 _id: &str,
429 _cx: CallContext,
430 ) -> BoxFuture<'_, Result<(), ExtensionCallError>> {
431 Box::pin(async { Ok(()) })
432 }
433
434 fn complete_input(
437 &self,
438 _request: JsonValue,
439 _cx: CallContext,
440 ) -> BoxFuture<'_, Option<JsonValue>> {
441 Box::pin(async { None })
442 }
443
444 fn services(&self) -> Vec<String> {
446 Vec::new()
447 }
448
449 fn handle_service(
451 &self,
452 name: &str,
453 _payload: JsonValue,
454 _caller: &str,
455 _cx: CallContext,
456 ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
457 let message = format!("service {name} is not implemented");
458 Box::pin(async move { Err(ExtensionCallError::new(message)) })
459 }
460
461 fn tool_overrides(&self) -> Vec<String> {
464 Vec::new()
465 }
466
467 fn render(&self, _request: JsonValue, _cx: CallContext) -> Option<JsonValue> {
471 None
472 }
473
474 fn route_model(
477 &self,
478 _request: JsonValue,
479 _cx: CallContext,
480 ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
481 Box::pin(async { Err(ExtensionCallError::new("route-model is not implemented")) })
482 }
483
484 fn propose_nudge(&self, _cx: CallContext) -> BoxFuture<'_, Option<JsonValue>> {
486 Box::pin(async { None })
487 }
488
489 fn nudge_delivered(&self, _id: &str, _cx: CallContext) {}
491
492 fn wait_reason(&self, _cx: CallContext) -> BoxFuture<'_, Option<String>> {
494 Box::pin(async { None })
495 }
496
497 fn hook_deadline(&self, _kind: &str) -> Option<std::time::Duration> {
500 None
501 }
502
503 fn provider(&self) -> Option<&dyn ProviderInstance> {
505 None
506 }
507
508 fn shutdown(&self, _reason: &str) -> BoxFuture<'_, ()> {
510 Box::pin(async {})
511 }
512}