1mod bash;
9mod boundary;
10pub mod checkpoints;
11mod compaction_flow;
12mod feeds;
13mod frontend;
14mod hooks;
15mod host;
16mod listener;
17mod model;
18mod nested;
19mod prompt;
20mod queue;
21mod retry;
22mod stats;
23mod tools;
24mod tree;
25
26use std::any::Any;
27use std::future::Future;
28use std::panic::AssertUnwindSafe;
29use std::path::{Path, PathBuf};
30use std::sync::{Arc, Mutex, MutexGuard, PoisonError, RwLock};
31
32use futures::FutureExt;
33use oino_agent::{Agent, AgentOptions, StreamFn, Subscription};
34use oino_ai::transcript::SystemState;
35use oino_ai::{
36 AgentMessage, AssistantMessage, BashExecutionMessage, CustomMessage, Model, SystemMessage,
37 ThinkingLevel,
38};
39use oino_models::ModelRegistry;
40use oino_resources::{BuildSystemPromptOptions, ResourceLoader};
41use oino_session::{EntryId, SessionEntry, SessionManager};
42use oino_settings::SettingsManager;
43use serde::{Deserialize, Serialize};
44use tokio::sync::watch;
45use tokio_util::sync::CancellationToken;
46
47use crate::events::{AgentSessionEvent, ClientEvent, ExtensionErrorEvent, OinoEvent, RunState};
48use crate::extensions::api::{CallContext, CallKind, ExtensionMode};
49use crate::extensions::runner::{ErrorSink, ExtensionRunner, ExtensionSet};
50use crate::hub::{EventHub, EventStream};
51use crate::ui::{RegistrySink, UiBridge, UiOutput, UiRegistry, UiSink, is_dialog};
52
53pub use bash::BashResult;
54pub use checkpoints::{Checkpoint, CheckpointFile, RewindFailure, RewindResult};
55pub use feeds::{DetachedBash, ExtensionInfo, RunningBash};
56pub use frontend::{
57 AuthProviderInfo, CompletionEntry, CompletionGroup, EditorEvent, EditorIntent, FRONTEND_CALLER,
58 FrontendError, RenderDensity, RenderRequest, RenderTarget, RenderWidth,
59};
60pub use host::SlashCommandInfo;
61pub(crate) use host::check_render_override;
62pub(crate) use model::branch_refs as model_branch_refs;
63pub use model::{CycleDirection, CycleModelResult};
64pub use prompt::{InputSource, PromptDisposition, PromptOptions, StreamingBehavior};
65pub use queue::{QueueEdit, QueueKind};
66pub use stats::{ContextUsage, SessionStats, TokenTotals};
67pub use tools::{BUILTIN_PATH_PREFIX, ToolEntry, ToolInfo, ToolSource};
68pub use tree::{ForkMessage, NavigateTreeOptions, NavigateTreeResult};
69
70pub(crate) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
71 mutex.lock().unwrap_or_else(PoisonError::into_inner)
72}
73
74#[must_use]
76pub(crate) fn panic_message(panic: &(dyn Any + Send)) -> String {
77 if let Some(text) = panic.downcast_ref::<&str>() {
78 (*text).to_owned()
79 } else if let Some(text) = panic.downcast_ref::<String>() {
80 text.clone()
81 } else {
82 "unknown panic".to_owned()
83 }
84}
85
86#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum IsolatedTask {
90 Drive,
93 ChildEvents,
95 ArbiterPoll,
97}
98
99impl IsolatedTask {
100 #[must_use]
102 pub const fn name(self) -> &'static str {
103 match self {
104 Self::Drive => "drive_run",
105 Self::ChildEvents => "forward_child_events",
106 Self::ArbiterPoll => "arbiter_poll",
107 }
108 }
109}
110
111#[must_use]
113pub fn current_system_message(messages: &[AgentMessage]) -> Option<SystemMessage> {
114 SystemState::fold(messages.iter().filter_map(|message| match message {
115 AgentMessage::System(system) => Some(system),
116 _ => None,
117 }))
118 .into_message()
119}
120
121#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
123#[non_exhaustive]
124pub enum SessionOpError {
125 #[error(
127 "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message."
128 )]
129 StreamingWithoutBehavior,
130 #[error("{0}")]
132 NoModel(String),
133 #[error("{message}")]
135 NoApiKey {
136 provider: String,
138 message: String,
140 },
141 #[error(
143 "Extension command \"/{0}\" cannot be queued. Use prompt() or execute the command when not streaming."
144 )]
145 CommandNotAllowed(String),
146 #[error(
148 "Cannot submit a prompt while compaction is in progress. Wait for compaction to finish and retry."
149 )]
150 CompactionInProgress,
151 #[error("{0}")]
154 SessionLocked(String),
155 #[error("This operation is only available in command and shortcut contexts")]
157 CommandContextRequired,
158 #[error("{0}")]
160 Failed(String),
161}
162
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "lowercase")]
166pub enum ActivityOutcome {
167 #[default]
169 Completed,
170 Aborted,
172 Error,
174}
175
176#[allow(
178 clippy::struct_excessive_bools,
179 reason = "mirrors the private AgentSession flags one-to-one"
180)]
181#[derive(Default)]
182pub(crate) struct RunBook {
183 pub(crate) steering: Vec<String>,
184 pub(crate) follow_up: Vec<String>,
185 pub(crate) pending_next_turn: Vec<CustomMessage>,
186 pub(crate) pending_custom: Vec<CustomMessage>,
187 pub(crate) pending_bash: Vec<BashExecutionMessage>,
188 pub(crate) run_active: bool,
189 pub(crate) abort_requested: bool,
190 pub(crate) overflow_recovery_attempted: bool,
191 pub(crate) retry_attempt: u32,
192 pub(crate) failed_response: Option<AssistantMessage>,
193 pub(crate) last_assistant: Option<AssistantMessage>,
194 pub(crate) last_outcome: ActivityOutcome,
195 pub(crate) turn_index: u64,
196 pub(crate) is_before_settle: bool,
197 pub(crate) abort_during_before_settle: bool,
198 pub(crate) emitting_settled: bool,
199 pub(crate) boundary_dispatched: bool,
200 pub(crate) run_prompt_options: Option<BuildSystemPromptOptions>,
201 pub(crate) last_assistant_entry: Option<EntryId>,
203 pub(crate) tool_result_entries: Vec<(String, EntryId)>,
205}
206
207#[derive(Default)]
209pub(crate) struct Cancels {
210 pub(crate) compaction: Option<CancellationToken>,
211 pub(crate) auto_compaction: Option<CancellationToken>,
212 pub(crate) branch_summary: Option<CancellationToken>,
213 pub(crate) retry: Option<CancellationToken>,
214 pub(crate) bash: Vec<(u64, CancellationToken)>,
215 pub(crate) next_bash: u64,
216}
217
218pub struct SessionConfig {
220 pub cwd: PathBuf,
222 pub agent_dir: PathBuf,
224 pub docs_root: PathBuf,
226 pub manager: SessionManager,
228 pub settings: Arc<Mutex<SettingsManager>>,
230 pub resources: Arc<RwLock<ResourceLoader>>,
232 pub models: ModelRegistry,
234 pub extensions: Arc<ExtensionSet>,
236 pub stream: Arc<dyn StreamFn>,
238 pub mode: ExtensionMode,
240 pub has_ui: bool,
242 pub builtin_tools: Vec<ToolEntry>,
244 pub custom_tools: Vec<ToolEntry>,
246 pub initial_active_tools: Vec<String>,
248 pub allowed_tools: Option<Vec<String>>,
250 pub excluded_tools: Vec<String>,
252 pub model: Option<Model>,
254 pub thinking_level: ThinkingLevel,
256 pub scoped_models: Vec<oino_models::resolver::ScopedModel>,
258 pub flag_values: indexmap::IndexMap<String, oino_ai::JsonValue>,
260 pub child: Option<crate::agents::ChildConfig>,
262}
263
264pub(crate) struct Inner {
265 pub(crate) agent: Agent,
266 pub(crate) stream: Arc<dyn StreamFn>,
267 pub(crate) manager: Mutex<SessionManager>,
268 pub(crate) settings: Arc<Mutex<SettingsManager>>,
269 pub(crate) resources: Arc<RwLock<ResourceLoader>>,
270 pub(crate) models: ModelRegistry,
271 pub(crate) runner: RwLock<ExtensionRunner>,
272 pub(crate) hub: EventHub,
273 pub(crate) ui: UiBridge,
274 pub(crate) registry: Arc<Mutex<UiRegistry>>,
276 pub(crate) book: Mutex<RunBook>,
277 pub(crate) cancels: Mutex<Cancels>,
278 pub(crate) tools: Mutex<tools::ToolState>,
279 pub(crate) nested: Mutex<nested::NestedState>,
280 pub(crate) scoped_models: Mutex<Vec<oino_models::resolver::ScopedModel>>,
281 pub(crate) idle: watch::Sender<bool>,
282 pub(crate) cwd: PathBuf,
283 pub(crate) agent_dir: PathBuf,
284 pub(crate) docs_root: PathBuf,
285 pub(crate) mode: ExtensionMode,
286 pub(crate) subscription: Mutex<Option<Subscription>>,
287 pub(crate) agents: crate::agents::AgentsState,
288 pub(crate) arbiter: Mutex<boundary::ArbiterState>,
289 pub(crate) runtime: std::sync::OnceLock<std::sync::Weak<crate::runtime::RuntimeCore>>,
290 pub(crate) editor_text: Mutex<String>,
292 pub(crate) frontend: feeds::FrontendState,
294}
295
296#[derive(Clone)]
298pub struct AgentSession {
299 pub(crate) inner: Arc<Inner>,
300}
301
302impl std::fmt::Debug for AgentSession {
303 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304 f.debug_struct("AgentSession")
305 .field("session_id", &self.session_id())
306 .finish_non_exhaustive()
307 }
308}
309
310struct HubErrors {
311 hub: EventHub,
312 registry: Arc<Mutex<UiRegistry>>,
313}
314
315impl ErrorSink for HubErrors {
316 fn report(&self, error: ExtensionErrorEvent) {
317 tracing::warn!(extension = %error.extension_path, event = %error.event, "{}", error.error);
318 self.hub.publish(error);
319 }
320
321 fn disabled(&self, event: OinoEvent) {
322 if let OinoEvent::ExtensionDisabled { extension_id, .. } = &event {
323 tracing::error!(extension = %extension_id, "extension disabled for this session");
324 lock(&self.registry).remove_owner(extension_id);
325 lock(&self.registry).flush();
326 }
327 self.hub.publish(event);
328 }
329}
330
331struct UiHub {
334 hub: EventHub,
335 registry: Arc<Mutex<UiRegistry>>,
336}
337
338impl UiHub {
339 fn flush(&self) {
340 lock(&self.registry).flush();
341 }
342}
343
344impl UiSink for UiHub {
345 fn publish(&self, output: UiOutput) {
346 match output {
347 UiOutput::Request(request) => {
348 if is_dialog(&request.method) {
349 lock(&self.registry).dialog_opened(request.to_registry());
350 self.flush();
351 }
352 self.hub.publish(ClientEvent::Ui(request));
353 }
354 UiOutput::Dismiss { id, reason } => {
355 lock(&self.registry).dialog_dismissed(&id, reason.into());
356 self.flush();
357 self.hub
358 .publish(OinoEvent::ExtensionUiDismiss { id, reason });
359 }
360 }
361 }
362}
363
364impl RegistrySink for UiHub {
365 fn publish(&self, patch: oino_ui::slot::UiPatch) {
366 self.hub.publish(OinoEvent::from(patch));
367 }
368}
369
370impl AgentSession {
371 #[must_use]
374 #[allow(
375 clippy::too_many_lines,
376 reason = "one constructor for the whole session state, mirroring createAgentSession"
377 )]
378 pub fn new(config: SessionConfig) -> Self {
379 let hub = EventHub::new();
380 let session_id = config.manager.session_id().to_owned();
381 let has_ui = config.has_ui;
382 let cx = CallContext {
383 session_id: session_id.clone(),
384 mode: config.mode,
385 has_ui,
386 cwd: config.cwd.clone(),
387 is_child: config.child.is_some(),
388 child_role: config.child.as_ref().and_then(|c| c.link.role.clone()),
389 call_kind: CallKind::Event,
390 };
391 let registry = Arc::new(Mutex::new(UiRegistry::new(
392 oino_ui::slot::Scope::Session,
393 Some(session_id.clone()),
394 )));
395 let ui_hub = Arc::new(UiHub {
396 hub: hub.clone(),
397 registry: Arc::clone(®istry),
398 });
399 lock(®istry).set_sink(ui_hub.clone());
400 let ui = UiBridge::new(has_ui, session_id.clone(), ui_hub);
401 let runner = ExtensionRunner::new(
402 Arc::clone(&config.extensions),
403 cx,
404 Arc::new(HubErrors {
405 hub: hub.clone(),
406 registry: Arc::clone(®istry),
407 }),
408 config.flag_values.clone(),
409 )
410 .with_dialogs(ui.clone());
411 let context = config.manager.build_session_context();
412 let settings = lock(&config.settings).snapshot();
413 let session_stream = Arc::new(hooks::SessionStream {
414 inner: std::sync::OnceLock::new(),
415 base: Arc::clone(&config.stream),
416 });
417 let mut options = AgentOptions::new(Arc::clone(&session_stream) as Arc<dyn StreamFn>);
418 options.model = config
419 .model
420 .clone()
421 .unwrap_or_else(oino_agent::agent::unknown_model);
422 options.thinking_level = config.thinking_level;
423 options.messages = context.messages;
424 options.steering_mode = map_queue_mode(settings.steering_mode());
425 options.follow_up_mode = map_queue_mode(settings.follow_up_mode());
426 options.stream_options.session_id = Some(session_id);
427 options.stream_options.transport = Some(settings.transport());
428 options.stream_options.thinking_budgets = settings.thinking_budgets();
429 options.stream_options.max_retry_delay_ms =
430 Some(settings.provider_retry_settings().max_retry_delay_ms);
431 options.convert_to_llm = convert_with_block_images(Arc::clone(&config.settings));
432 let agent = Agent::new(options);
433 let (idle, _) = watch::channel(true);
434 let agents = match config.child {
435 Some(child) => crate::agents::AgentsState::child(child),
436 None => crate::agents::AgentsState::root(
437 Arc::clone(&config.extensions),
438 Arc::clone(&config.stream),
439 ),
440 };
441 let tool_state = tools::ToolState::new(
442 config.builtin_tools,
443 config.custom_tools,
444 config.allowed_tools.as_deref(),
445 &config.excluded_tools,
446 );
447 let inner = Arc::new(Inner {
448 agent,
449 stream: Arc::clone(&session_stream) as Arc<dyn StreamFn>,
450 manager: Mutex::new(config.manager),
451 settings: config.settings,
452 resources: config.resources,
453 models: config.models,
454 runner: RwLock::new(runner),
455 hub,
456 ui,
457 registry,
458 book: Mutex::new(RunBook::default()),
459 cancels: Mutex::new(Cancels::default()),
460 tools: Mutex::new(tool_state),
461 nested: Mutex::new(nested::NestedState::default()),
462 scoped_models: Mutex::new(config.scoped_models),
463 idle,
464 cwd: config.cwd,
465 agent_dir: config.agent_dir,
466 docs_root: config.docs_root,
467 mode: config.mode,
468 subscription: Mutex::new(None),
469 agents,
470 arbiter: Mutex::new(boundary::ArbiterState::default()),
471 runtime: std::sync::OnceLock::new(),
472 editor_text: Mutex::new(String::new()),
473 frontend: feeds::FrontendState::new(),
474 });
475 let _ = session_stream.inner.set(Arc::downgrade(&inner));
476 let session = Self { inner };
477 let subscription = session
478 .inner
479 .agent
480 .subscribe(Arc::new(listener::Listener::new(&session.inner)));
481 *lock(&session.inner.subscription) = Some(subscription);
482 session
483 .inner
484 .agent
485 .set_hooks(hooks::install(&session.inner));
486 session.refresh_tool_registry(Some(config.initial_active_tools), true);
487 session
488 }
489
490 pub async fn bind_extensions(&self, reason: &str, previous_session_file: Option<PathBuf>) {
493 let runner = self.runner();
494 if runner.has_handlers("session_start") {
495 let mut payload = serde_json::json!({"type": "session_start", "reason": reason});
496 if let Some(previous) = previous_session_file {
497 payload["previousSessionFile"] = serde_json::json!(previous.display().to_string());
498 }
499 runner.emit(payload).await;
500 }
501 let discover_reason = if reason == "reload" {
502 "reload"
503 } else {
504 "startup"
505 };
506 self.extend_resources_from_extensions(discover_reason).await;
507 for (extension, spec) in runner.renderers() {
510 if spec.get("target").and_then(oino_ai::JsonValue::as_str) == Some("turn-footer")
511 && let Some(key) = spec.get("key").and_then(oino_ai::JsonValue::as_str)
512 {
513 self.ui_register_turn_footer(&extension, key);
514 }
515 }
516 if !runner.shortcuts().is_empty() {
517 self.publish_shortcuts();
518 }
519 }
520
521 async fn extend_resources_from_extensions(&self, reason: &str) {
524 use oino_resources::source::{PathMetadata, SourceOrigin, SourceScope};
525 let runner = self.runner();
526 let entry =
527 |path: PathBuf, extension: &str, origin: SourceOrigin, base: Option<PathBuf>| {
528 oino_resources::ExtraResourcePath {
529 path,
530 metadata: PathMetadata {
531 source: format!("extension:{extension}"),
532 scope: SourceScope::Temporary,
533 origin,
534 base_dir: base,
535 },
536 }
537 };
538 let mut paths = oino_resources::ResourceExtensionPaths::default();
539 for extension in runner.set().extensions() {
540 let id = extension.instance.id();
541 let dirs = extension.instance.resource_dirs();
542 let package = |list: Vec<PathBuf>| {
543 list.into_iter()
544 .map(|dir| {
545 let base = dir.parent().map(Path::to_path_buf);
546 entry(dir, id, SourceOrigin::Package, base)
547 })
548 .collect::<Vec<_>>()
549 };
550 paths.skill_paths.extend(package(dirs.skills));
551 paths.prompt_paths.extend(package(dirs.prompts));
552 paths.theme_paths.extend(package(dirs.themes));
553 }
554 if runner.has_handlers("resources_discover") {
555 let found = runner
556 .emit_resources_discover(&self.inner.cwd.display().to_string(), reason)
557 .await;
558 let discovered = |list: Vec<(String, String)>| {
559 list.into_iter()
560 .map(|(path, extension)| {
561 entry(
562 PathBuf::from(path),
563 &extension,
564 SourceOrigin::TopLevel,
565 None,
566 )
567 })
568 .collect::<Vec<_>>()
569 };
570 paths.skill_paths.extend(discovered(found.skill_paths));
571 paths.prompt_paths.extend(discovered(found.prompt_paths));
572 paths.theme_paths.extend(discovered(found.theme_paths));
573 }
574 if paths.skill_paths.is_empty()
575 && paths.prompt_paths.is_empty()
576 && paths.theme_paths.is_empty()
577 {
578 return;
579 }
580 self.inner
581 .resources
582 .write()
583 .unwrap_or_else(PoisonError::into_inner)
584 .extend(&paths);
585 let active = self.active_tool_names();
586 self.rebuild_system_prompt(&active);
587 }
588
589 pub async fn reload(&self) -> Result<(), SessionOpError> {
596 {
598 let mut registry = lock(&self.inner.registry);
599 registry.clear();
600 registry.flush();
601 }
602 let runner = self.runner();
603 if runner.has_handlers("session_shutdown") {
604 runner
605 .emit(serde_json::json!({"type": "session_shutdown", "reason": "reload"}))
606 .await;
607 }
608 let snapshot = {
609 let mut settings = lock(&self.inner.settings);
610 settings.reload();
611 settings.snapshot()
612 };
613 self.inner
614 .agent
615 .set_steering_mode(map_queue_mode(snapshot.steering_mode()));
616 self.inner
617 .agent
618 .set_follow_up_mode(map_queue_mode(snapshot.follow_up_mode()));
619 let trusted = lock(&self.inner.settings).is_project_trusted();
620 self.inner
621 .resources
622 .write()
623 .unwrap_or_else(PoisonError::into_inner)
624 .reload(&snapshot, trusted);
625 self.refresh_tool_registry(None, true);
626 self.bind_extensions("reload", None).await;
627 Ok(())
628 }
629
630 pub(crate) fn link_router(&self, router: &Arc<crate::services::HostRouter>) {
632 let _ = self.inner.agents.router.set(Arc::downgrade(router));
633 }
634
635 pub(crate) fn link_runtime(&self, runtime: std::sync::Weak<crate::runtime::RuntimeCore>) {
637 let _ = self.inner.runtime.set(runtime);
638 }
639
640 pub(crate) fn runtime_core(&self) -> Option<Arc<crate::runtime::RuntimeCore>> {
641 self.inner.runtime.get().and_then(std::sync::Weak::upgrade)
642 }
643
644 pub(crate) fn from_inner(inner: Arc<Inner>) -> Self {
645 Self { inner }
646 }
647
648 #[must_use]
650 pub fn runner(&self) -> ExtensionRunner {
651 self.inner
652 .runner
653 .read()
654 .unwrap_or_else(PoisonError::into_inner)
655 .clone()
656 }
657
658 #[must_use]
660 pub fn subscribe(&self) -> EventStream {
661 self.inner.hub.subscribe()
662 }
663
664 #[must_use]
666 pub fn hub(&self) -> &EventHub {
667 &self.inner.hub
668 }
669
670 pub(crate) fn emit(&self, event: AgentSessionEvent) {
671 self.inner.hub.publish(event);
672 }
673
674 #[must_use]
676 pub fn ui(&self) -> UiBridge {
677 self.inner.ui.clone()
678 }
679
680 #[must_use]
682 pub fn agent(&self) -> &Agent {
683 &self.inner.agent
684 }
685
686 pub fn with_manager<R>(&self, f: impl FnOnce(&mut SessionManager) -> R) -> R {
688 f(&mut lock(&self.inner.manager))
689 }
690
691 pub fn with_settings<R>(&self, f: impl FnOnce(&mut SettingsManager) -> R) -> R {
693 f(&mut lock(&self.inner.settings))
694 }
695
696 #[must_use]
698 pub fn models(&self) -> &ModelRegistry {
699 &self.inner.models
700 }
701
702 #[must_use]
704 pub fn cwd(&self) -> &Path {
705 &self.inner.cwd
706 }
707
708 #[must_use]
710 pub fn agent_dir(&self) -> &Path {
711 &self.inner.agent_dir
712 }
713
714 #[must_use]
716 pub fn mode(&self) -> ExtensionMode {
717 self.inner.mode
718 }
719
720 #[must_use]
722 pub fn model(&self) -> Option<Model> {
723 let model = self.inner.agent.model();
724 (model.provider != "unknown" || model.id != "unknown").then_some(model)
725 }
726
727 #[must_use]
729 pub fn thinking_level(&self) -> ThinkingLevel {
730 self.inner.agent.thinking_level()
731 }
732
733 #[must_use]
735 pub fn is_streaming(&self) -> bool {
736 lock(&self.inner.book).run_active
737 }
738
739 #[must_use]
741 pub fn is_compacting(&self) -> bool {
742 let cancels = lock(&self.inner.cancels);
743 cancels.compaction.is_some()
744 || cancels.auto_compaction.is_some()
745 || cancels.branch_summary.is_some()
746 }
747
748 #[must_use]
750 pub fn is_idle(&self) -> bool {
751 !self.is_streaming() && !self.is_compacting()
752 }
753
754 #[must_use]
756 pub fn is_retrying(&self) -> bool {
757 lock(&self.inner.cancels).retry.is_some()
758 }
759
760 #[must_use]
762 pub fn is_bash_running(&self) -> bool {
763 !lock(&self.inner.cancels).bash.is_empty()
764 }
765
766 #[must_use]
768 pub fn retry_attempt(&self) -> u32 {
769 lock(&self.inner.book).retry_attempt
770 }
771
772 #[must_use]
774 pub fn messages(&self) -> Vec<AgentMessage> {
775 self.inner.agent.messages()
776 }
777
778 #[must_use]
780 pub fn session_file(&self) -> Option<PathBuf> {
781 lock(&self.inner.manager)
782 .session_file()
783 .map(Path::to_path_buf)
784 }
785
786 #[must_use]
788 pub fn session_id(&self) -> String {
789 lock(&self.inner.manager).session_id().to_owned()
790 }
791
792 #[must_use]
794 pub fn session_name(&self) -> Option<String> {
795 lock(&self.inner.manager).session_name()
796 }
797
798 #[must_use]
800 pub fn steering_messages(&self) -> Vec<String> {
801 lock(&self.inner.book).steering.clone()
802 }
803
804 #[must_use]
806 pub fn follow_up_messages(&self) -> Vec<String> {
807 lock(&self.inner.book).follow_up.clone()
808 }
809
810 #[must_use]
812 pub fn pending_message_count(&self) -> usize {
813 let book = lock(&self.inner.book);
814 book.steering.len() + book.follow_up.len()
815 }
816
817 #[must_use]
819 pub fn clear_queue(&self) -> (Vec<String>, Vec<String>) {
820 let (steering, follow_up) = {
821 let mut book = lock(&self.inner.book);
822 (
823 std::mem::take(&mut book.steering),
824 std::mem::take(&mut book.follow_up),
825 )
826 };
827 self.inner.agent.clear_all_queues();
828 self.emit_queue_update();
829 (steering, follow_up)
830 }
831
832 pub(crate) fn emit_queue_update(&self) {
833 let (steering, follow_up) = {
834 let book = lock(&self.inner.book);
835 (book.steering.clone(), book.follow_up.clone())
836 };
837 self.emit(AgentSessionEvent::QueueUpdate {
838 steering,
839 follow_up,
840 });
841 }
842
843 #[must_use]
845 pub fn last_assistant_text(&self) -> Option<String> {
846 let messages = self.messages();
847 let last = messages.iter().rev().find_map(|message| match message {
848 AgentMessage::Assistant(assistant)
849 if !(assistant.stop_reason == oino_ai::StopReason::Aborted
850 && assistant.content.is_empty()) =>
851 {
852 Some(assistant)
853 }
854 _ => None,
855 })?;
856 let text: String = last
857 .content
858 .iter()
859 .filter_map(|block| match block {
860 oino_ai::AssistantContent::Text(text) => Some(text.text.as_str()),
861 _ => None,
862 })
863 .collect();
864 let trimmed = text.trim();
865 (!trimmed.is_empty()).then(|| trimmed.to_owned())
866 }
867
868 #[must_use]
870 pub fn steering_mode(&self) -> oino_agent::QueueMode {
871 self.inner.agent.steering_mode()
872 }
873
874 #[must_use]
876 pub fn follow_up_mode(&self) -> oino_agent::QueueMode {
877 self.inner.agent.follow_up_mode()
878 }
879
880 pub fn set_steering_mode(&self, mode: oino_agent::QueueMode) {
882 self.inner.agent.set_steering_mode(mode);
883 let result = self.with_settings(|s| s.set_steering_mode(unmap_queue_mode(mode)));
884 if let Err(error) = result {
885 tracing::warn!("saving steeringMode failed: {error}");
886 }
887 }
888
889 pub fn set_follow_up_mode(&self, mode: oino_agent::QueueMode) {
891 self.inner.agent.set_follow_up_mode(mode);
892 let result = self.with_settings(|s| s.set_follow_up_mode(unmap_queue_mode(mode)));
893 if let Err(error) = result {
894 tracing::warn!("saving followUpMode failed: {error}");
895 }
896 }
897
898 pub fn set_scoped_models(&self, scope: Vec<oino_models::resolver::ScopedModel>) {
900 *lock(&self.inner.scoped_models) = scope;
901 }
902
903 #[must_use]
905 pub fn scoped_models(&self) -> Vec<oino_models::resolver::ScopedModel> {
906 lock(&self.inner.scoped_models).clone()
907 }
908
909 pub fn refresh_context(&self) {
912 let messages = lock(&self.inner.manager)
913 .build_session_projection()
914 .messages;
915 self.inner.agent.set_messages(messages);
916 }
917
918 #[must_use]
920 pub fn entries(&self) -> Vec<SessionEntry> {
921 lock(&self.inner.manager).entries().to_vec()
922 }
923
924 #[must_use]
926 pub fn run_state(&self) -> RunState {
927 if self.is_compacting() {
928 RunState::Compacting
929 } else if self.is_retrying() {
930 RunState::Retrying
931 } else if self.is_streaming() {
932 RunState::Running
933 } else if !lock(&self.inner.arbiter).waiting_on.is_empty() {
934 RunState::Waiting
935 } else {
936 RunState::Idle
937 }
938 }
939
940 pub(crate) fn set_idle_if_idle(&self) {
941 if self.is_idle() {
942 self.inner.idle.send_replace(true);
943 }
944 }
945
946 pub(crate) fn mark_busy(&self) {
947 self.inner.idle.send_replace(false);
948 }
949
950 pub async fn wait_for_idle(&self) {
952 if self.is_idle() {
953 return;
954 }
955 let mut idle = self.inner.idle.subscribe();
956 loop {
957 if self.is_idle() && *idle.borrow() {
958 return;
959 }
960 if idle.changed().await.is_err() {
961 return;
962 }
963 }
964 }
965
966 pub async fn abort(&self) {
968 {
969 let mut book = lock(&self.inner.book);
970 if book.run_active {
971 book.abort_requested = true;
972 }
973 if book.is_before_settle {
974 book.abort_during_before_settle = true;
975 }
976 }
977 self.abort_retry();
978 self.abort_compaction();
979 self.abort_branch_summary();
980 self.inner.agent.abort();
981 self.wait_for_idle().await;
982 }
983
984 pub fn abort_retry(&self) {
986 if let Some(token) = &lock(&self.inner.cancels).retry {
987 token.cancel();
988 }
989 }
990
991 pub fn abort_compaction(&self) {
993 let cancels = lock(&self.inner.cancels);
994 for token in [&cancels.compaction, &cancels.auto_compaction]
995 .into_iter()
996 .flatten()
997 {
998 token.cancel();
999 }
1000 }
1001
1002 pub fn abort_branch_summary(&self) {
1004 if let Some(token) = &lock(&self.inner.cancels).branch_summary {
1005 token.cancel();
1006 }
1007 }
1008
1009 pub fn abort_bash(&self) {
1011 for (_, token) in &lock(&self.inner.cancels).bash {
1012 token.cancel();
1013 }
1014 }
1015
1016 pub async fn dispose(&self) {
1018 Box::pin(self.close_children()).await;
1020 self.abort_retry();
1021 self.abort_compaction();
1022 self.abort_branch_summary();
1023 self.abort_bash();
1024 self.inner.agent.abort();
1025 self.runner().invalidate(None);
1026 self.inner.ui.cancel_all();
1027 {
1028 let mut registry = lock(&self.inner.registry);
1029 registry.clear();
1030 registry.flush();
1031 }
1032 *lock(&self.inner.subscription) = None;
1033 }
1034
1035 pub fn spawn_isolated(
1044 &self,
1045 task: IsolatedTask,
1046 fut: impl Future<Output = ()> + Send + 'static,
1047 ) -> tokio::task::JoinHandle<()> {
1048 let session = self.clone();
1049 tokio::spawn(async move {
1050 if let Err(panic) = AssertUnwindSafe(fut).catch_unwind().await {
1051 session.on_isolated_panic(task, &*panic);
1052 }
1053 })
1054 }
1055
1056 fn on_isolated_panic(&self, task: IsolatedTask, panic: &(dyn Any + Send)) {
1058 let error = panic_message(panic);
1059 tracing::error!(
1060 session = %self.session_id(),
1061 task = task.name(),
1062 "session task panicked: {error}"
1063 );
1064 if task == IsolatedTask::Drive {
1065 self.recover_crashed_run();
1066 }
1067 self.inner.hub.publish(OinoEvent::SessionCrashed { error });
1068 self.inner.hub.publish(OinoEvent::RunState {
1070 state: RunState::Idle,
1071 waiting_on: None,
1072 });
1073 self.set_idle_if_idle();
1074 }
1075
1076 fn recover_crashed_run(&self) {
1081 match self.with_manager(SessionManager::repair_interrupted_turn) {
1082 Ok(report) if !report.closed_tool_calls.is_empty() || report.torn_tail => {
1083 tracing::warn!(
1084 session = %self.session_id(),
1085 "crashed run repaired: {} dangling tool call(s) closed",
1086 report.closed_tool_calls.len()
1087 );
1088 self.refresh_context();
1089 }
1090 Ok(_) => {}
1091 Err(error) => {
1092 tracing::warn!(session = %self.session_id(), "repair after crash failed: {error}");
1093 }
1094 }
1095 {
1096 let mut book = lock(&self.inner.book);
1097 book.run_active = false;
1098 book.abort_requested = false;
1099 book.overflow_recovery_attempted = false;
1100 book.retry_attempt = 0;
1101 book.failed_response = None;
1102 book.is_before_settle = false;
1103 book.abort_during_before_settle = false;
1104 book.emitting_settled = false;
1105 book.boundary_dispatched = false;
1106 book.run_prompt_options = None;
1107 book.tool_result_entries.clear();
1108 }
1109 {
1110 let mut cancels = lock(&self.inner.cancels);
1111 for token in [
1112 &cancels.retry,
1113 &cancels.compaction,
1114 &cancels.auto_compaction,
1115 &cancels.branch_summary,
1116 ]
1117 .into_iter()
1118 .flatten()
1119 {
1120 token.cancel();
1121 }
1122 cancels.retry = None;
1123 cancels.compaction = None;
1124 cancels.auto_compaction = None;
1125 cancels.branch_summary = None;
1126 }
1127 lock(&self.inner.arbiter).waiting_on.clear();
1128 self.inner.agent.recover_orphaned_run();
1129 }
1130
1131 pub async fn set_session_name(&self, name: Option<String>) -> Result<(), SessionOpError> {
1137 let current = {
1138 let mut manager = lock(&self.inner.manager);
1139 manager
1140 .append_session_info(name.as_deref())
1141 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
1142 manager.session_name()
1143 };
1144 self.emit(AgentSessionEvent::SessionInfoChanged {
1145 name: current.clone(),
1146 });
1147 let mut payload = serde_json::json!({"type": "session_info_changed"});
1148 if let Some(name) = current {
1149 payload["name"] = serde_json::json!(name);
1150 }
1151 self.runner().emit(payload).await;
1152 Ok(())
1153 }
1154
1155 #[allow(
1161 clippy::unused_async,
1162 clippy::unused_async_trait_impl,
1163 reason = "async per the runtime.md AgentSession contract"
1164 )]
1165 pub async fn set_label(
1166 &self,
1167 entry: &EntryId,
1168 label: Option<String>,
1169 ) -> Result<(), SessionOpError> {
1170 lock(&self.inner.manager)
1171 .append_label_change(entry, label.as_deref())
1172 .map(|_| ())
1173 .map_err(|e| SessionOpError::Failed(e.to_string()))
1174 }
1175
1176 pub(crate) fn publish_entry(&self, entry_id: &EntryId) {
1177 let entry = lock(&self.inner.manager).entry(entry_id).cloned();
1178 if let Some(entry) = entry {
1179 self.emit(AgentSessionEvent::EntryAppended { entry });
1180 }
1181 }
1182
1183 pub(crate) fn emit_model_changed(&self, source: crate::events::ModelChangeSource) {
1184 if let Some(model) = self.model() {
1185 self.inner.hub.publish(OinoEvent::ModelChanged {
1186 model,
1187 routed_model: self.routed_model(),
1188 source,
1189 });
1190 }
1191 }
1192}
1193
1194fn map_queue_mode(mode: oino_settings::QueueMode) -> oino_agent::QueueMode {
1195 match mode {
1196 oino_settings::QueueMode::All => oino_agent::QueueMode::All,
1197 oino_settings::QueueMode::OneAtATime => oino_agent::QueueMode::OneAtATime,
1198 }
1199}
1200
1201fn unmap_queue_mode(mode: oino_agent::QueueMode) -> oino_settings::QueueMode {
1202 match mode {
1203 oino_agent::QueueMode::All => oino_settings::QueueMode::All,
1204 oino_agent::QueueMode::OneAtATime => oino_settings::QueueMode::OneAtATime,
1205 }
1206}
1207
1208fn convert_with_block_images(settings: Arc<Mutex<SettingsManager>>) -> oino_agent::ConvertToLlm {
1210 Arc::new(move |messages: &[AgentMessage]| {
1211 let converted = oino_session::convert_to_llm(messages);
1212 if !lock(&settings).settings().image_settings().block_images {
1213 return converted;
1214 }
1215 converted.into_iter().map(block_images).collect()
1216 })
1217}
1218
1219const IMAGE_DISABLED: &str = "Image reading is disabled.";
1220
1221fn block_images(message: oino_ai::Message) -> oino_ai::Message {
1222 use oino_ai::{Message, UserContent, UserContentField};
1223 let strip = |content: Vec<UserContent>| -> Vec<UserContent> {
1224 let mut out: Vec<UserContent> = Vec::new();
1225 for block in content {
1226 let block = match block {
1227 UserContent::Image(_) => UserContent::text(IMAGE_DISABLED),
1228 text @ UserContent::Text(_) => text,
1229 };
1230 let duplicate = matches!((&block, out.last()), (UserContent::Text(a), Some(UserContent::Text(b))) if a.text == IMAGE_DISABLED && b.text == IMAGE_DISABLED);
1231 if !duplicate {
1232 out.push(block);
1233 }
1234 }
1235 out
1236 };
1237 match message {
1238 Message::User(mut user) => {
1239 if let UserContentField::Blocks(blocks) = user.content {
1240 user.content = UserContentField::Blocks(strip(blocks));
1241 }
1242 Message::User(user)
1243 }
1244 Message::ToolResult(mut result) => {
1245 result.content = strip(result.content);
1246 Message::ToolResult(result)
1247 }
1248 other => other,
1249 }
1250}