oino-core 1.0.1

Coding-agent runtime for Oino: AgentSession, extension runner, compaction, retry
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
//! Native contract between the runner and a loaded extension instance.
//!
//! This mirrors `oino-extension-api` (extension-api.md §6) closely so the WASM host can
//! implement it directly; once `oino-extension-api` lands these definitions are replaced by
//! re-exports. Event payloads and results are fixed-shape JSON (D5).

use std::path::PathBuf;

use futures::future::BoxFuture;
use futures::stream::BoxStream;
use oino_agent::{AgentToolResult, ToolInvocation, UpdateSink};
use oino_ai::{JsonObject, JsonValue, Model, ProviderEvent};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;

/// Stable extension id (manifest `id`).
pub type ExtensionId = String;

/// Manifest `kind` (D15): `-ne` disables only `Extension`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExtensionKind {
    /// A provider component.
    Provider,
    /// Any other extension.
    #[default]
    Extension,
}

/// Extension mode plus ACP.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ExtensionMode {
    /// Interactive terminal.
    Tui,
    /// RPC.
    Rpc,
    /// JSON event stream.
    Json,
    /// Print mode.
    #[default]
    Print,
    /// ACP adapter.
    Acp,
    /// The GPUI desktop app (in-process backend).
    Desktop,
    /// The GPUI mobile app (in-process backend).
    Mobile,
}

/// What kind of guest call is being served (runtime §7.3).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CallKind {
    /// Event handler.
    #[default]
    Event,
    /// Tool execution.
    Tool,
    /// Slash command.
    Command,
    /// Shortcut.
    Shortcut,
    /// Service request.
    Service,
    /// Transcript row rendering (`guest.render`, ui-protocol §6.4).
    Render,
}

/// Context carried by every guest call.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallContext {
    /// Session the call belongs to.
    pub session_id: String,
    /// Run mode.
    pub mode: ExtensionMode,
    /// Dialog-capable UI attached.
    pub has_ui: bool,
    /// Session cwd.
    pub cwd: PathBuf,
    /// Nested (child) session.
    pub is_child: bool,
    /// Child role, for nested sessions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub child_role: Option<String>,
    /// Call kind.
    pub call_kind: CallKind,
}

impl CallContext {
    /// The same context for another call kind.
    #[must_use]
    pub fn with_kind(&self, call_kind: CallKind) -> Self {
        Self {
            call_kind,
            ..self.clone()
        }
    }
}

/// Failure of a guest call.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct ExtensionCallError {
    /// Error text.
    pub message: String,
    /// The guest trapped (instance restarted lazily).
    pub trap: bool,
}

impl ExtensionCallError {
    /// A non-trap error.
    #[must_use]
    pub fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
            trap: false,
        }
    }
}

/// How a tool is exposed to the model.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolExposure {
    /// Declared to the model and callable by tools.
    #[default]
    Direct,
    /// Declared to the model only.
    ModelOnly,
    /// Callable from code mode only.
    Codemode,
    /// Declared through tool search.
    Deferred,
    /// Not declared.
    Hidden,
}

/// Tool registered by an extension (`ToolDefinition` data part).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolSpec {
    /// Model-facing name.
    pub name: String,
    /// Frontend label.
    #[serde(default)]
    pub label: String,
    /// Model-facing description.
    pub description: String,
    /// JSON schema.
    pub parameters: JsonValue,
    /// One-line prompt snippet.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt_snippet: Option<String>,
    /// Prompt guidelines.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub prompt_guidelines: Vec<String>,
    /// Exposure.
    #[serde(default)]
    pub exposure: ToolExposure,
    /// Activated on registration (default `true`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_active: Option<bool>,
    /// `sequential` forces sequential batches.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub execution_mode: Option<oino_agent::ExecutionMode>,
    /// Tool namespace.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub namespace: Option<JsonValue>,
    /// Annotations (ACP kind, read-only hints, …).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub annotations: Option<JsonValue>,
    /// Tools this tool may call (`ctx.executeTool`).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reaches_tools: Option<JsonValue>,
    /// Output schema.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output_schema: Option<JsonValue>,
    /// The guest implements `prepare-arguments` for this tool.
    #[serde(default)]
    pub prepare_arguments: bool,
}

/// Slash command (`RegisteredCommand` data part).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandSpec {
    /// Command name without `/`.
    pub name: String,
    /// Description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    /// Hidden commands are invokable but not listed.
    #[serde(default)]
    pub hidden: bool,
    /// The guest completes arguments.
    #[serde(default)]
    pub completions: bool,
}

/// Shortcut (ui-protocol §8).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShortcutSpec {
    /// Stable action id.
    pub id: String,
    /// Default key ids.
    #[serde(default)]
    pub default_keys: Vec<String>,
    /// Description.
    #[serde(default)]
    pub description: String,
}

/// Flag type.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FlagType {
    /// `--name` / `--no-name`.
    Boolean,
    /// `--name <value>`.
    String,
}

/// CLI flag registered by an extension.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FlagSpec {
    /// Long name without `--`.
    pub name: String,
    /// Type.
    #[serde(rename = "type")]
    pub kind: FlagType,
    /// Default value.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default: Option<JsonValue>,
    /// Help text.
    #[serde(default)]
    pub description: String,
}

/// Everything an extension registers at `init` (dynamic registration uses [`super::host::HostApi`]).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Registration {
    /// Tools.
    #[serde(default)]
    pub tools: Vec<ToolSpec>,
    /// Commands.
    #[serde(default)]
    pub commands: Vec<CommandSpec>,
    /// Shortcuts.
    #[serde(default)]
    pub shortcuts: Vec<ShortcutSpec>,
    /// Flags.
    #[serde(default)]
    pub flags: Vec<FlagSpec>,
    /// Subscribed event kinds (`type` strings or `oino_*`).
    #[serde(default)]
    pub events: Vec<String>,
    /// Provider registrations (extension-api §4.4 JSON).
    #[serde(default)]
    pub providers: Vec<JsonValue>,
    /// Virtual model definitions.
    #[serde(default)]
    pub virtual_models: Vec<JsonValue>,
    /// Render specs (ui-protocol §6.4).
    #[serde(default)]
    pub renderers: Vec<JsonValue>,
    /// APIs this provider component implements (`provides.apis`).
    #[serde(default)]
    pub apis: Vec<String>,
    /// Turn arbiter roles (runtime §7.6).
    #[serde(default)]
    pub arbiter: ArbiterRoles,
    /// Settings namespaces (ui-protocol §6.6 declarations, runtime §10.3).
    #[serde(default)]
    pub settings: Vec<JsonValue>,
}

/// Turn arbiter roles of an extension (runtime §7.6, D9).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArbiterRoles {
    /// Implements `propose-nudge` / `nudge-delivered`.
    #[serde(default)]
    pub nudge_provider: bool,
    /// Implements `wait-reason`.
    #[serde(default)]
    pub wait_source: bool,
}

/// Argument completion item.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompletionItem {
    /// Inserted text.
    pub value: String,
    /// Label shown.
    pub label: String,
    /// Extra description.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
}

/// Streaming provider implementation of a provider component.
pub trait ProviderInstance: Send + Sync {
    /// Starts a request (`{model, context, options, apiKey?, headers?}` JSON) and streams
    /// provider events.
    fn stream(
        &self,
        request: JsonValue,
        cancel: CancellationToken,
    ) -> BoxFuture<
        '_,
        Result<BoxStream<'static, Result<ProviderEvent, ExtensionCallError>>, ExtensionCallError>,
    >;

    /// Lists models of `provider_id`.
    fn list_models(
        &self,
        provider_id: &str,
        credential: Option<JsonValue>,
    ) -> BoxFuture<'_, Result<Vec<Model>, ExtensionCallError>>;

    /// Runs the provider's OAuth sign-in (`oauth-login`) in `cx` (its dialogs reach that
    /// session's frontend); returns the credential JSON to store.
    fn oauth_login(
        &self,
        provider_id: &str,
        cx: CallContext,
    ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
        let _ = (provider_id, cx);
        Box::pin(async { Err(ExtensionCallError::new("OAuth sign-in is not supported")) })
    }

    /// Exchanges the refresh token of a stored OAuth credential (`oauth-refresh`).
    fn oauth_refresh(
        &self,
        provider_id: &str,
        credential: JsonValue,
    ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
        let _ = (provider_id, credential);
        Box::pin(async { Err(ExtensionCallError::new("OAuth refresh is not supported")) })
    }
}

/// Skill, prompt and theme directories of an extension package (manifest `resources`).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PackageResourceDirs {
    /// Skill directories.
    pub skills: Vec<PathBuf>,
    /// Prompt template directories.
    pub prompts: Vec<PathBuf>,
    /// Theme directories.
    pub themes: Vec<PathBuf>,
}

/// One loaded extension instance (≈ `oino_extension_api::ExtensionInstance`).
pub trait ExtensionInstance: Send + Sync {
    /// Manifest id.
    fn id(&self) -> &str;

    /// Manifest kind.
    fn kind(&self) -> ExtensionKind {
        ExtensionKind::Extension
    }

    /// Path or label used in `extension_error.extensionPath`.
    fn path(&self) -> String {
        self.id().to_owned()
    }

    /// Resource directories the package declares (manifest `resources`, absolute paths under
    /// the package root); loaded like package resources (runtime §9.6).
    fn resource_dirs(&self) -> PackageResourceDirs {
        PackageResourceDirs::default()
    }

    /// Initialises the instance and returns its static registrations.
    fn init(&self, cx: CallContext) -> BoxFuture<'_, Result<Registration, ExtensionCallError>>;

    /// Handles one subscribed event; `Ok(None)` means "no result".
    fn on_event(
        &self,
        kind: &str,
        payload: JsonValue,
        cx: CallContext,
    ) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;

    /// Runs a registered tool.
    fn run_tool(
        &self,
        tool: &str,
        invocation: ToolInvocation,
        updates: UpdateSink,
        cancel: CancellationToken,
        cx: CallContext,
    ) -> BoxFuture<'_, Result<AgentToolResult, ExtensionCallError>>;

    /// `prepareArguments` for a registered tool.
    fn prepare_arguments(&self, _tool: &str, raw: JsonObject) -> JsonObject {
        raw
    }

    /// Runs a registered command.
    fn run_command(
        &self,
        name: &str,
        args: &str,
        cancel: CancellationToken,
        cx: CallContext,
    ) -> BoxFuture<'_, Result<Option<JsonValue>, ExtensionCallError>>;

    /// Completes command arguments.
    fn complete_command(
        &self,
        _name: &str,
        _prefix: &str,
        _cx: CallContext,
    ) -> BoxFuture<'_, Vec<CompletionItem>> {
        Box::pin(async { Vec::new() })
    }

    /// Runs a registered shortcut.
    fn run_shortcut(
        &self,
        _id: &str,
        _cx: CallContext,
    ) -> BoxFuture<'_, Result<(), ExtensionCallError>> {
        Box::pin(async { Ok(()) })
    }

    /// Autocomplete contributor (`complete-input`, ui-protocol §7.3): `{text, cursor}` in,
    /// `[CompletionGroup]` JSON out.
    fn complete_input(
        &self,
        _request: JsonValue,
        _cx: CallContext,
    ) -> BoxFuture<'_, Option<JsonValue>> {
        Box::pin(async { None })
    }

    /// Service names this instance serves (`registry.register-service`).
    fn services(&self) -> Vec<String> {
        Vec::new()
    }

    /// Serves a service request (`handle-service`) from `caller`.
    fn handle_service(
        &self,
        name: &str,
        _payload: JsonValue,
        _caller: &str,
        _cx: CallContext,
    ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
        let message = format!("service {name} is not implemented");
        Box::pin(async move { Err(ExtensionCallError::new(message)) })
    }

    /// Built-in tools whose rendering this extension may override (manifest
    /// `tools.override`); `render-only` — no tool registration needed (U1). `"*"` covers all.
    fn tool_overrides(&self) -> Vec<String> {
        Vec::new()
    }

    /// `guest.render`: an OUS node for one transcript row (ui-protocol §6.4), or `None` for the
    /// default rendering. Called synchronously from the render path; the request JSON is
    /// `{target, key, entryId?, density, expanded, width, data}`.
    fn render(&self, _request: JsonValue, _cx: CallContext) -> Option<JsonValue> {
        None
    }

    /// Routes a virtual model (`route-model` export): a route request in, a route result
    /// out.
    fn route_model(
        &self,
        _request: JsonValue,
        _cx: CallContext,
    ) -> BoxFuture<'_, Result<JsonValue, ExtensionCallError>> {
        Box::pin(async { Err(ExtensionCallError::new("route-model is not implemented")) })
    }

    /// Arbiter `propose-nudge`: `{id?, priority, customType, content, display, details?}`.
    fn propose_nudge(&self, _cx: CallContext) -> BoxFuture<'_, Option<JsonValue>> {
        Box::pin(async { None })
    }

    /// Arbiter `nudge-delivered` for the winning proposal.
    fn nudge_delivered(&self, _id: &str, _cx: CallContext) {}

    /// Arbiter `wait-reason`: why the session should wait instead of settling.
    fn wait_reason(&self, _cx: CallContext) -> BoxFuture<'_, Option<String>> {
        Box::pin(async { None })
    }

    /// Manifest deadline for handlers of `kind` (`limits.hookTimeoutMs`,
    /// `toolCallHookTimeoutMs`); the runner never goes below its own default.
    fn hook_deadline(&self, _kind: &str) -> Option<std::time::Duration> {
        None
    }

    /// The provider implementation, for provider components.
    fn provider(&self) -> Option<&dyn ProviderInstance> {
        None
    }

    /// Shuts the instance down.
    fn shutdown(&self, _reason: &str) -> BoxFuture<'_, ()> {
        Box::pin(async {})
    }
}