///|
/// Configuration shared by every OpenCode CLI session opened by this adapter.
pub struct OpenCodeAgentOptions {
opencode_path_override : @path.Path?
config : @sdk.OpenCodeConfigObject?
resume_thread_id : String?
model : String?
agent : String?
variant : String?
title : String?
thinking : Bool
extra_env : Map[String, String]
}
///|
/// Creates OpenCode adapter options with the CLI SDK's defaults.
pub fn OpenCodeAgentOptions::OpenCodeAgentOptions(
opencode_path_override? : @path.Path,
config? : @sdk.OpenCodeConfigObject,
resume_thread_id? : String,
model? : String,
agent? : String,
variant? : String,
title? : String,
thinking? : Bool = false,
extra_env? : Map[String, String] = Map([]),
) -> OpenCodeAgentOptions {
{
opencode_path_override,
config,
resume_thread_id,
model,
agent,
variant,
title,
thinking,
extra_env,
}
}
///|
/// Raised when execution is requested after a session has closed.
pub suberror OpenCodeAdapterError {
SessionClosed
} derive(Debug)
///|
priv struct OpenCodeAgentSession {
thread : @sdk.Thread
workspace_root : @path.Path
mutex : @async.Mutex
mut closed : Bool
}
///|
fn request_input(
workspace_root : @path.Path,
request : @coding.CodingAgentRequest,
) -> @sdk.Input {
if request.context_files.is_empty() {
return @sdk.Prompt(request.instruction)
}
let inputs : Array[@sdk.UserInput] = [@sdk.Text(request.instruction)]
for path in request.context_files {
inputs.push(
@sdk.LocalFile(
if path.is_absolute() {
path
} else {
workspace_root.join(path)
},
),
)
}
@sdk.UserInputs(inputs)
}
///|
fn merged_environment(
options : OpenCodeAgentOptions,
context : @coding.CodingAgentOpenContext,
) -> Map[String, String] {
let environment = @env.get_env_vars()
for key, value in options.extra_env {
environment[key] = value
}
// Run context is caller-owned, so it overrides adapter defaults.
for key, value in context.environment {
environment[key] = value
}
environment
}
///|
impl @coding.CodingAgentSession for OpenCodeAgentSession with fn id(self) {
match self.thread.id() {
Some(id) =>
Some(@coding.SessionId::SessionId(id) catch { _ => return None })
None => None
}
}
///|
impl @coding.CodingAgentSession for OpenCodeAgentSession with fn execute(
self,
request,
) {
self.mutex.acquire()
if self.closed {
self.mutex.release()
raise OpenCodeAdapterError::SessionClosed
}
let result = Ok(self.thread.run(request_input(self.workspace_root, request))) catch {
error => Err(error)
}
self.mutex.release()
let turn = match result {
Ok(turn) => turn
Err(error) => raise error
}
@coding.CodingAgentResponse::CodingAgentResponse(
status=@coding.Succeeded,
summary=Some(turn.final_response),
continuation_id=self.thread.id(),
changed_files=[],
)
}
///|
impl @coding.CodingAgentSession for OpenCodeAgentSession with fn close(self) {
self.mutex.acquire()
self.closed = true
self.mutex.release()
}
///|
/// Creates a coding agent backed by the OpenCode CLI SDK.
pub fn opencode_agent(
id : @coding.CodingAgentId,
options : OpenCodeAgentOptions,
) -> @coding.CodingAgent {
@coding.CodingAgent::CodingAgent(id, async fn(context) {
@async.pause()
let client = @sdk.OpenCode::OpenCode(
options=@sdk.OpenCodeOptions::OpenCodeOptions(
opencode_path_override?=options.opencode_path_override,
config?=options.config,
env=merged_environment(options, context),
),
)
let thread_options = @sdk.ThreadOptions::ThreadOptions(
model?=options.model,
agent?=options.agent,
working_directory=context.workspace.root,
variant?=options.variant,
title?=options.title,
thinking=options.thinking,
)
let thread = match options.resume_thread_id {
Some(thread_id) => client.resume_thread(thread_id, options=thread_options)
None => client.start_thread(options=thread_options)
}
let session = OpenCodeAgentSession::{
thread,
workspace_root: context.workspace.root,
mutex: @async.Mutex::Mutex(),
closed: false,
}
(session : &@coding.CodingAgentSession)
})
}