Skip to main content

oxicode_agent/runtime/
mod.rs

1//! Shared stateful coding runtimes for the `coding-omp-v1` behavior pack
2//! (see `docs/designs/2026-08-31-omp-compatible-behavior-pack-design.md`,
3//! "Coding extensions"): the [`ShellSession`]/[`EvalKernel`]/[`DebugService`]
4//! contracts plus the bundled reference implementations —
5//! [`PersistentShellSession`], [`PythonEvalKernel`]/[`JavaScriptEvalKernel`],
6//! and [`DapDebugService`]. Until a host wires them into
7//! `BehaviorSessionServices`, the SDK behavior installer reports those
8//! extensions as degraded in its manifest.
9
10pub mod dap;
11pub mod eval_kernel;
12pub mod shell;
13
14pub use dap::{DapClient, DapDebugService};
15pub use eval_kernel::{JavaScriptEvalKernel, PythonEvalKernel};
16pub use shell::PersistentShellSession;
17
18use async_trait::async_trait;
19use std::time::Duration;
20
21/// Output of one command in a persistent shell session.
22#[derive(Debug, Clone, Default)]
23pub struct ShellOutput {
24    /// Captured stdout (bounded by the host).
25    pub stdout: String,
26    /// Captured stderr (bounded by the host).
27    pub stderr: String,
28    /// Process exit code.
29    pub exit_code: i32,
30    /// True when the host applied an output bound and elided bytes.
31    pub truncated: bool,
32}
33
34/// Persistent shell session contract ("Shell session" extension).
35///
36/// Required behavior: persistent command environment across calls,
37/// cancellation, bounded output, explicit reset (design table row 3).
38#[async_trait]
39pub trait ShellSession: Send + Sync + std::fmt::Debug {
40    /// Execute `command` in the persistent environment.
41    async fn execute(&self, command: &str, timeout: Duration) -> Result<ShellOutput, String>;
42    /// Cancel the currently running command, if any.
43    fn cancel(&self);
44    /// Reset to a fresh environment; the working directory returns to the
45    /// workspace root.
46    async fn reset(&self) -> Result<(), String>;
47}
48
49/// Language of an eval kernel session.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum EvalLanguage {
52    /// Persistent Python kernel.
53    Python,
54    /// Persistent JavaScript (Bun) kernel.
55    JavaScript,
56}
57
58/// Output of one persistent-kernel evaluation.
59#[derive(Debug, Clone, Default)]
60pub struct EvalOutput {
61    /// The kernel's result/repr value, if any.
62    pub result: String,
63    /// Captured stdout (bounded by the host).
64    pub stdout: String,
65    /// Captured stderr (bounded by the host).
66    pub stderr: String,
67    /// Structured error, when the evaluation failed.
68    pub error: Option<String>,
69    /// True when the host applied an output bound and elided bytes.
70    pub truncated: bool,
71}
72
73/// Persistent eval kernel contract ("Eval kernel" extension).
74///
75/// Required behavior: persistent Python/Bun state across calls, bounded
76/// execution, explicit reset (design table row 4).
77#[async_trait]
78pub trait EvalKernel: Send + Sync + std::fmt::Debug {
79    /// Language this kernel evaluates.
80    fn language(&self) -> EvalLanguage;
81    /// Evaluate `code` in the persistent kernel state.
82    async fn execute(&self, code: &str, timeout: Duration) -> Result<EvalOutput, String>;
83    /// Drop all kernel state; the next execute starts fresh.
84    async fn reset(&self) -> Result<(), String>;
85}
86
87/// Debug service contract ("Debug service" extension): a real DAP session
88/// lifecycle (design table row 5).
89///
90/// Requests use DAP command names (`setBreakpoints`, `continue`, `next`,
91/// `variables`, ...) with raw JSON payloads — typed methods arrive with the
92/// first real implementation.
93#[async_trait]
94pub trait DebugService: Send + Sync + std::fmt::Debug {
95    /// Launch or attach a session per the DAP launch/attach config; returns
96    /// a session id.
97    async fn start(&self, config: &serde_json::Value) -> Result<String, String>;
98    /// Issue a DAP request against the session; returns the raw response
99    /// payload.
100    async fn request(
101        &self,
102        session: &str,
103        command: &str,
104        args: &serde_json::Value,
105    ) -> Result<serde_json::Value, String>;
106    /// Terminate the session and clean up the adapter process.
107    async fn terminate(&self, session: &str) -> Result<(), String>;
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use std::sync::Arc;
114
115    #[derive(Debug)]
116    struct NoopShell;
117
118    #[async_trait]
119    impl ShellSession for NoopShell {
120        async fn execute(&self, _command: &str, _timeout: Duration) -> Result<ShellOutput, String> {
121            Ok(ShellOutput {
122                stdout: String::new(),
123                stderr: String::new(),
124                exit_code: 0,
125                truncated: false,
126            })
127        }
128        fn cancel(&self) {}
129        async fn reset(&self) -> Result<(), String> {
130            Ok(())
131        }
132    }
133
134    #[tokio::test]
135    async fn shell_session_contract_is_object_safe() {
136        let shell: Arc<dyn ShellSession> = Arc::new(NoopShell);
137        let out = shell.execute("true", Duration::from_secs(1)).await.unwrap();
138        assert_eq!(out.exit_code, 0);
139        shell.cancel();
140        shell.reset().await.unwrap();
141    }
142}