feat(opencode): remplace le profil Ollama HTTP par OpenCode
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -124,6 +124,8 @@ pub struct CodexExecSession {
|
||||
cwd: String,
|
||||
/// Project/workspace roots that must be writable in Codex's CLI sandbox.
|
||||
writable_roots: Vec<String>,
|
||||
/// Variables d'environnement préparées au lancement (ex. `CODEX_HOME` isolé).
|
||||
env: Vec<(String, String)>,
|
||||
/// Id de conversation **du moteur** Codex, capté au premier tour, `None` avant.
|
||||
conversation_id: Mutex<Option<String>>,
|
||||
/// Plan de sandbox OS **par lancement** (lot LP4-4), porté dans chaque
|
||||
@ -146,6 +148,7 @@ impl CodexExecSession {
|
||||
cwd: impl Into<String>,
|
||||
seed_conversation_id: Option<String>,
|
||||
writable_roots: Vec<String>,
|
||||
env: Vec<(String, String)>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Self {
|
||||
@ -154,6 +157,7 @@ impl CodexExecSession {
|
||||
command: command.into(),
|
||||
cwd: cwd.into(),
|
||||
writable_roots,
|
||||
env,
|
||||
conversation_id: Mutex::new(seed_conversation_id),
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
@ -196,7 +200,7 @@ impl CodexExecSession {
|
||||
command: self.command.clone(),
|
||||
args,
|
||||
cwd: self.cwd.clone(),
|
||||
env: Vec::new(),
|
||||
env: self.env.clone(),
|
||||
stdin: None,
|
||||
sandbox: self.sandbox.clone(),
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ use domain::SessionId;
|
||||
use super::claude::ClaudeSdkSession;
|
||||
use super::codex::CodexExecSession;
|
||||
use super::openai_compat::OpenAiCompatibleSession;
|
||||
use super::opencode::OpenCodeSession;
|
||||
|
||||
const PROJECT_ROOT_ARG: &str = "__ideaProjectRoot";
|
||||
const REQUESTER_ARG: &str = "__ideaRequester";
|
||||
@ -135,6 +136,7 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
ctx: &PreparedContext,
|
||||
cwd: &ProjectPath,
|
||||
session: &SessionPlan,
|
||||
env: &[(String, String)],
|
||||
sandbox: Option<&SandboxPlan>,
|
||||
) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
|
||||
let adapter = profile.structured_adapter.ok_or_else(|| {
|
||||
@ -176,9 +178,19 @@ impl AgentSessionFactory for StructuredSessionFactory {
|
||||
cwd,
|
||||
seed,
|
||||
vec![ctx.project_root.clone()],
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
)),
|
||||
StructuredAdapter::OpenCode => Arc::new(OpenCodeSession::new(
|
||||
id,
|
||||
profile.command.clone(),
|
||||
profile.args.clone(),
|
||||
cwd,
|
||||
env.to_vec(),
|
||||
plan,
|
||||
enforcer,
|
||||
)?),
|
||||
StructuredAdapter::OpenAiCompatible => {
|
||||
let config = profile.chat_http.clone().ok_or_else(|| {
|
||||
AgentSessionError::Start(format!(
|
||||
|
||||
@ -24,6 +24,7 @@ pub mod codex;
|
||||
pub mod conformance;
|
||||
pub mod factory;
|
||||
pub mod openai_compat;
|
||||
pub mod opencode;
|
||||
pub mod process;
|
||||
|
||||
/// Tests bout-en-bout de l'enforcement Landlock sur le chemin structuré (lot LP4-4),
|
||||
@ -36,6 +37,7 @@ pub use codex::CodexExecSession;
|
||||
pub use conformance::FakeCli;
|
||||
pub use factory::StructuredSessionFactory;
|
||||
pub use openai_compat::OpenAiCompatibleSession;
|
||||
pub use opencode::OpenCodeSession;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@ -377,6 +379,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
@ -468,7 +471,7 @@ mod tests {
|
||||
let mut expected: HashMap<&str, bool> = HashMap::new();
|
||||
expected.insert("claude", true);
|
||||
expected.insert("codex", true);
|
||||
expected.insert("openai-compatible", true);
|
||||
expected.insert("opencode", true);
|
||||
expected.insert("gemini", false);
|
||||
expected.insert("aider", false);
|
||||
|
||||
@ -502,7 +505,14 @@ mod tests {
|
||||
// Claude : la session démarre et respecte le contrat via le fake CLI.
|
||||
let claude = structured_profile(StructuredAdapter::Claude, &fake.command());
|
||||
let session = factory
|
||||
.start(&claude, &prepared_ctx(), &cwd(), &SessionPlan::None, None)
|
||||
.start(
|
||||
&claude,
|
||||
&prepared_ctx(),
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("start Claude ok");
|
||||
let content = drain_final(session.as_ref()).await;
|
||||
@ -512,7 +522,14 @@ mod tests {
|
||||
let fake_cx = FakeCli::printing(&codex_script());
|
||||
let codex = structured_profile(StructuredAdapter::Codex, &fake_cx.command());
|
||||
let session_cx = factory
|
||||
.start(&codex, &prepared_ctx(), &cwd(), &SessionPlan::None, None)
|
||||
.start(
|
||||
&codex,
|
||||
&prepared_ctx(),
|
||||
&cwd(),
|
||||
&SessionPlan::None,
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.expect("start Codex ok");
|
||||
let content_cx = drain_final(session_cx.as_ref()).await;
|
||||
@ -541,6 +558,7 @@ mod tests {
|
||||
&prepared_ctx(),
|
||||
&temp_cwd("factory-openai"),
|
||||
&SessionPlan::None,
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@ -571,7 +589,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let session = factory
|
||||
.start(&codex, &ctx, &cwd(), &SessionPlan::None, None)
|
||||
.start(&codex, &ctx, &cwd(), &SessionPlan::None, &[], None)
|
||||
.await
|
||||
.expect("start Codex ok");
|
||||
let content = drain_final(session.as_ref()).await;
|
||||
@ -605,6 +623,7 @@ mod tests {
|
||||
&SessionPlan::Resume {
|
||||
conversation_id: "repris-42".to_owned(),
|
||||
},
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@ -940,6 +959,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -975,6 +995,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1005,6 +1026,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1030,6 +1052,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1056,6 +1079,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1262,6 +1286,7 @@ mod tests {
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1341,6 +1366,7 @@ mod tests {
|
||||
&SessionPlan::Resume {
|
||||
conversation_id: "cx-resume".to_owned(),
|
||||
},
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@ -1364,6 +1390,7 @@ mod tests {
|
||||
&SessionPlan::Assign {
|
||||
conversation_id: "ignored-by-engine".to_owned(),
|
||||
},
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
@ -1389,7 +1416,7 @@ mod tests {
|
||||
)
|
||||
.expect("profil valide");
|
||||
match factory
|
||||
.start(&tui, &prepared_ctx(), &cwd(), &SessionPlan::None, None)
|
||||
.start(&tui, &prepared_ctx(), &cwd(), &SessionPlan::None, &[], None)
|
||||
.await
|
||||
{
|
||||
Err(AgentSessionError::Start(_)) => {}
|
||||
@ -1413,6 +1440,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
));
|
||||
@ -1526,6 +1554,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1577,6 +1606,7 @@ mod tests {
|
||||
"/",
|
||||
None,
|
||||
vec!["/project/root".to_owned()],
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@ -1616,6 +1646,7 @@ mod tests {
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
vec!["/project/root".to_owned()],
|
||||
Vec::new(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
315
crates/infrastructure/src/session/opencode.rs
Normal file
315
crates/infrastructure/src/session/opencode.rs
Normal file
@ -0,0 +1,315 @@
|
||||
//! [`OpenCodeSession`] — adapter structuré OpenCode + Ollama.
|
||||
//!
|
||||
//! OpenCode est piloté comme host process local : IdeA génère un `opencode.json`
|
||||
//! isolé dans le run dir, OpenCode lance le bridge MCP `idea`, puis chaque tour est
|
||||
//! un `opencode run --format json <prompt>`. L'adapter ne connaît que le contrat
|
||||
//! JSONL minimal observé/cadré : `step_start`, `text`, `step_finish`.
|
||||
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
use domain::ports::{AgentSession, AgentSessionError, ReplyEvent, ReplyStream};
|
||||
use domain::sandbox::{SandboxEnforcer, SandboxPlan};
|
||||
use domain::SessionId;
|
||||
|
||||
/// Un événement OpenCode parsé depuis stdout JSONL.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ParsedEvent {
|
||||
/// Début d'étape OpenCode.
|
||||
StepStart,
|
||||
/// Fragment texte assistant.
|
||||
Text(String),
|
||||
/// Fin d'étape OpenCode.
|
||||
StepFinish,
|
||||
/// Evénement JSON valide mais hors contrat minimal.
|
||||
Ignored,
|
||||
}
|
||||
|
||||
/// Parse une ligne JSONL OpenCode.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`AgentSessionError::Decode`] si la ligne non vide n'est pas du JSON valide.
|
||||
pub fn parse_jsonl_event(line: &str) -> Result<ParsedEvent, AgentSessionError> {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(ParsedEvent::Ignored);
|
||||
}
|
||||
let value: Value = serde_json::from_str(trimmed)
|
||||
.map_err(|e| AgentSessionError::Decode(format!("ligne JSON OpenCode illisible: {e}")))?;
|
||||
match value.get("type").and_then(Value::as_str) {
|
||||
Some("step_start") | Some("step.start") => Ok(ParsedEvent::StepStart),
|
||||
Some("step_finish") | Some("step.finish") => Ok(ParsedEvent::StepFinish),
|
||||
Some("text") => Ok(ParsedEvent::Text(
|
||||
value
|
||||
.get("text")
|
||||
.or_else(|| value.get("content"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_owned(),
|
||||
)),
|
||||
_ => Ok(ParsedEvent::Ignored),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convertit des lignes JSONL OpenCode en événements domaine.
|
||||
///
|
||||
/// Le `Final` est la concaténation ordonnée des événements `text`. Un stdout JSONL
|
||||
/// valide mais sans texte est une erreur typée : une délégation ne peut pas être
|
||||
/// considérée comme réussie sans réponse finale capturable.
|
||||
pub fn parse_jsonl_turn(lines: &[String]) -> Result<Vec<ReplyEvent>, AgentSessionError> {
|
||||
let mut events = Vec::new();
|
||||
let mut final_text = String::new();
|
||||
for line in lines {
|
||||
match parse_jsonl_event(line)? {
|
||||
ParsedEvent::StepStart | ParsedEvent::StepFinish => {
|
||||
events.push(ReplyEvent::Heartbeat);
|
||||
}
|
||||
ParsedEvent::Text(text) => {
|
||||
if !text.is_empty() {
|
||||
final_text.push_str(&text);
|
||||
events.push(ReplyEvent::TextDelta { text });
|
||||
}
|
||||
}
|
||||
ParsedEvent::Ignored => {}
|
||||
}
|
||||
}
|
||||
if final_text.trim().is_empty() {
|
||||
return Err(AgentSessionError::Decode(
|
||||
"OpenCode n'a produit aucun final textuel".to_owned(),
|
||||
));
|
||||
}
|
||||
events.push(ReplyEvent::Final {
|
||||
content: final_text,
|
||||
});
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Découpe une commande utilisateur en argv sans shell implicite.
|
||||
///
|
||||
/// Supporte les guillemets simples/doubles et les antislashs. Les expansions shell,
|
||||
/// pipes et substitutions ne sont pas interprétés.
|
||||
pub fn split_command_prefix(raw: &str) -> Result<Vec<String>, AgentSessionError> {
|
||||
let mut out = Vec::new();
|
||||
let mut cur = String::new();
|
||||
let mut chars = raw.chars().peekable();
|
||||
let mut quote: Option<char> = None;
|
||||
while let Some(ch) = chars.next() {
|
||||
match (quote, ch) {
|
||||
(Some(q), c) if c == q => quote = None,
|
||||
(None, '\'' | '"') => quote = Some(ch),
|
||||
(_, '\\') => {
|
||||
if let Some(next) = chars.next() {
|
||||
cur.push(next);
|
||||
} else {
|
||||
cur.push('\\');
|
||||
}
|
||||
}
|
||||
(None, c) if c.is_whitespace() => {
|
||||
if !cur.is_empty() {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
}
|
||||
}
|
||||
(_, c) => cur.push(c),
|
||||
}
|
||||
}
|
||||
if quote.is_some() {
|
||||
return Err(AgentSessionError::Start(
|
||||
"commande OpenCode invalide: guillemet non fermé".to_owned(),
|
||||
));
|
||||
}
|
||||
if !cur.is_empty() {
|
||||
out.push(cur);
|
||||
}
|
||||
if out.is_empty() {
|
||||
return Err(AgentSessionError::Start(
|
||||
"commande OpenCode vide".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Adapter OpenCode process-backed.
|
||||
pub struct OpenCodeSession {
|
||||
id: SessionId,
|
||||
command: String,
|
||||
prefix_args: Vec<String>,
|
||||
cwd: String,
|
||||
env: Vec<(String, String)>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
}
|
||||
|
||||
impl OpenCodeSession {
|
||||
/// Construit l'adapter. `command_prefix` peut être `opencode`, un chemin absolu,
|
||||
/// ou un wrapper avec arguments; IdeA ajoute ensuite `run --format json`.
|
||||
pub fn new(
|
||||
id: SessionId,
|
||||
command_prefix: impl Into<String>,
|
||||
profile_args: Vec<String>,
|
||||
cwd: impl Into<String>,
|
||||
env: Vec<(String, String)>,
|
||||
sandbox: Option<SandboxPlan>,
|
||||
sandbox_enforcer: Option<Arc<dyn SandboxEnforcer>>,
|
||||
) -> Result<Self, AgentSessionError> {
|
||||
let mut prefix = split_command_prefix(&command_prefix.into())?;
|
||||
let command = prefix.remove(0);
|
||||
prefix.extend(profile_args);
|
||||
Ok(Self {
|
||||
id,
|
||||
command,
|
||||
prefix_args: prefix,
|
||||
cwd: cwd.into(),
|
||||
env,
|
||||
sandbox,
|
||||
sandbox_enforcer,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_args(&self, prompt: &str) -> Vec<String> {
|
||||
let mut args = self.prefix_args.clone();
|
||||
args.extend([
|
||||
"run".to_owned(),
|
||||
"--format".to_owned(),
|
||||
"json".to_owned(),
|
||||
prompt.to_owned(),
|
||||
]);
|
||||
args
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AgentSession for OpenCodeSession {
|
||||
fn id(&self) -> SessionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
fn conversation_id(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn send(&self, prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
cmd.args(self.build_args(prompt))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
if !self.cwd.is_empty() && self.cwd != "/" {
|
||||
cmd.current_dir(&self.cwd);
|
||||
}
|
||||
for (key, value) in &self.env {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
// OpenCode supporte déjà son propre confinement logique; le plan Landlock
|
||||
// structuré reste réservé aux chemins process génériques existants.
|
||||
let _ = (&self.sandbox, &self.sandbox_enforcer);
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| AgentSessionError::Start(format!("{}: {e}", self.command)))?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| AgentSessionError::Io("stdout pipe indisponible".to_owned()))?;
|
||||
let mut stderr_pipe = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| AgentSessionError::Io("stderr pipe indisponible".to_owned()))?;
|
||||
|
||||
let mut lines = BufReader::new(stdout).lines();
|
||||
let mut collected = Vec::new();
|
||||
while let Some(line) = lines
|
||||
.next_line()
|
||||
.await
|
||||
.map_err(|e| AgentSessionError::Io(e.to_string()))?
|
||||
{
|
||||
collected.push(line);
|
||||
}
|
||||
|
||||
let mut stderr_bytes = Vec::new();
|
||||
stderr_pipe
|
||||
.read_to_end(&mut stderr_bytes)
|
||||
.await
|
||||
.map_err(|e| AgentSessionError::Io(e.to_string()))?;
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|e| AgentSessionError::Io(e.to_string()))?;
|
||||
let stderr = String::from_utf8_lossy(&stderr_bytes);
|
||||
if stderr.contains("server unavailable") && stderr.contains("key=idea") {
|
||||
return Err(AgentSessionError::Start(
|
||||
"serveur MCP OpenCode `idea` indisponible".to_owned(),
|
||||
));
|
||||
}
|
||||
if !status.success() {
|
||||
return Err(AgentSessionError::Io(format!(
|
||||
"OpenCode a quitté avec le statut {}: {}",
|
||||
status,
|
||||
stderr.trim()
|
||||
)));
|
||||
}
|
||||
|
||||
let events = parse_jsonl_turn(&collected)?;
|
||||
Ok(Box::new(events.into_iter()))
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn split_command_prefix_handles_quotes_without_shell() {
|
||||
assert_eq!(
|
||||
split_command_prefix(r#""/tmp/my opencode" --flag "two words" 'three words'"#).unwrap(),
|
||||
vec!["/tmp/my opencode", "--flag", "two words", "three words"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jsonl_turn_concatenates_text_and_adds_final() {
|
||||
let events = parse_jsonl_turn(&[
|
||||
r#"{"type":"step_start"}"#.to_owned(),
|
||||
r#"{"type":"text","text":"hel"}"#.to_owned(),
|
||||
r#"{"type":"text","text":"lo"}"#.to_owned(),
|
||||
r#"{"type":"step_finish"}"#.to_owned(),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
events,
|
||||
vec![
|
||||
ReplyEvent::Heartbeat,
|
||||
ReplyEvent::TextDelta {
|
||||
text: "hel".to_owned()
|
||||
},
|
||||
ReplyEvent::TextDelta {
|
||||
text: "lo".to_owned()
|
||||
},
|
||||
ReplyEvent::Heartbeat,
|
||||
ReplyEvent::Final {
|
||||
content: "hello".to_owned()
|
||||
}
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jsonl_turn_rejects_empty_final() {
|
||||
let err = parse_jsonl_turn(&[r#"{"type":"step_start"}"#.to_owned()]).unwrap_err();
|
||||
assert!(matches!(err, AgentSessionError::Decode(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_jsonl_event_rejects_invalid_json() {
|
||||
let err = parse_jsonl_event("{nope").unwrap_err();
|
||||
assert!(matches!(err, AgentSessionError::Decode(_)));
|
||||
}
|
||||
}
|
||||
@ -470,7 +470,7 @@ async fn structured_sandboxed_turn_preserves_conversation_id() {
|
||||
let plan = rw_plan(&run_dir); // plan write-only ⇒ reads/exec du fake non gênés
|
||||
|
||||
let session = factory
|
||||
.start(&profile, &ctx, &cwd, &SessionPlan::None, Some(&plan))
|
||||
.start(&profile, &ctx, &cwd, &SessionPlan::None, &[], Some(&plan))
|
||||
.await
|
||||
.expect("start sandboxé ok");
|
||||
|
||||
|
||||
@ -474,6 +474,7 @@ impl AgentSessionFactory for BlockingReplyFactory {
|
||||
_ctx: &PreparedContext,
|
||||
_cwd: &ProjectPath,
|
||||
_session: &SessionPlan,
|
||||
_env: &[(String, String)],
|
||||
_sandbox: Option<&domain::sandbox::SandboxPlan>,
|
||||
) -> Result<Arc<dyn AgentSession>, AgentSessionError> {
|
||||
Ok(Arc::new(BlockingReplySession {
|
||||
|
||||
Reference in New Issue
Block a user