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}