oino_core/session/
bash.rs1use oino_ai::{AgentMessage, BashExecutionMessage};
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6use tokio_util::sync::CancellationToken;
7
8use super::{AgentSession, SessionOpError, lock};
9use crate::events::AgentSessionEvent;
10
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct BashResult {
15 pub output: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub exit_code: Option<i32>,
20 pub cancelled: bool,
22 pub truncated: bool,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub full_output_path: Option<String>,
27}
28
29impl From<oino_tools::BashOutcome> for BashResult {
30 fn from(outcome: oino_tools::BashOutcome) -> Self {
31 Self {
32 output: outcome.output,
33 exit_code: outcome.exit_code,
34 cancelled: outcome.cancelled,
35 truncated: outcome.truncated,
36 full_output_path: outcome.full_output_path,
37 }
38 }
39}
40
41impl AgentSession {
42 pub async fn execute_bash(
50 &self,
51 command: String,
52 exclude_from_context: bool,
53 id: Option<String>,
54 ) -> Result<BashResult, SessionOpError> {
55 let runner = self.runner();
56 if runner.has_handlers("user_bash") {
57 let payload = json!({"type": "user_bash", "command": command, "excludeFromContext": exclude_from_context, "cwd": self.cwd().display().to_string()});
58 match runner.emit_user_bash(payload).await {
59 Ok(Some(result)) => {
60 let result: BashResult = serde_json::from_value(result)
61 .map_err(|e| SessionOpError::Failed(e.to_string()))?;
62 self.record_bash_result(&command, &result, exclude_from_context);
63 return Ok(result);
64 }
65 Ok(None) => {}
66 Err(error) => return Err(SessionOpError::Failed(error)),
67 }
68 }
69 let settings = lock(&self.inner.settings).snapshot();
70 let resolved = match settings.shell_command_prefix() {
71 Some(prefix) if !prefix.is_empty() => format!("{prefix}\n{command}"),
72 _ => command.clone(),
73 };
74 let options = oino_tools::ShellRunOptions {
75 shell_path: settings.shell_path().map(|p| p.display().to_string()),
76 agent_dir: self.agent_dir().display().to_string(),
77 process_env: None,
78 };
79 let cancel = CancellationToken::new();
80 let slot = {
81 let mut cancels = lock(&self.inner.cancels);
82 cancels.next_bash += 1;
83 let slot = cancels.next_bash;
84 cancels.bash.push((slot, cancel.clone()));
85 slot
86 };
87 let session = self.clone();
88 let mut on_chunk = move |delta: &str| {
89 session.emit(AgentSessionEvent::BashExecutionUpdate {
90 id: id.clone(),
91 delta: delta.to_owned(),
92 });
93 };
94 let env = oino_tools::LocalEnv::new();
95 let cwd = self.cwd().display().to_string();
96 let outcome =
97 oino_tools::run_shell_command(&env, &resolved, &cwd, &options, &mut on_chunk, &cancel)
98 .await;
99 lock(&self.inner.cancels).bash.retain(|(id, _)| *id != slot);
100 let result: BashResult = outcome.map_err(|e| SessionOpError::Failed(e.0))?.into();
101 self.record_bash_result(&command, &result, exclude_from_context);
102 Ok(result)
103 }
104
105 pub fn record_bash_result(
107 &self,
108 command: &str,
109 result: &BashResult,
110 exclude_from_context: bool,
111 ) {
112 let message = BashExecutionMessage {
113 command: command.to_owned(),
114 output: result.output.clone(),
115 exit_code: result.exit_code,
116 cancelled: result.cancelled,
117 truncated: result.truncated,
118 full_output_path: result.full_output_path.clone(),
119 timestamp: oino_agent::now_ms(),
120 exclude_from_context: exclude_from_context.then_some(true),
121 };
122 if self.is_streaming() {
123 lock(&self.inner.book).pending_bash.push(message);
124 return;
125 }
126 if let Err(error) =
127 lock(&self.inner.manager).append_message(AgentMessage::BashExecution(message))
128 {
129 tracing::error!("persisting bash execution failed: {error}");
130 }
131 self.refresh_context();
132 }
133
134 pub(crate) fn flush_pending_bash_messages(&self) {
136 let pending = std::mem::take(&mut lock(&self.inner.book).pending_bash);
137 if pending.is_empty() {
138 return;
139 }
140 {
141 let mut manager = lock(&self.inner.manager);
142 for message in pending {
143 if let Err(error) = manager.append_message(AgentMessage::BashExecution(message)) {
144 tracing::error!("persisting bash execution failed: {error}");
145 }
146 }
147 }
148 self.refresh_context();
149 }
150}