///|
/// A completed OpenCode turn.
pub(all) struct Turn {
events : ReadOnlyArray[ThreadEvent]
final_response : String
} derive(Debug, Eq)
///|
/// Alias describing the result returned by `Thread::run`.
pub type RunResult = Turn
///|
/// The result type returned after a streamed callback completes.
pub type StreamedTurn = Unit
///|
/// Alias describing the result returned by `Thread::run_streamed`.
pub type RunStreamedResult = StreamedTurn
///|
/// One structured input entry sent to OpenCode.
pub(all) enum UserInput {
Text(String)
LocalFile(@path.Path)
} derive(Eq)
///|
/// Input accepted by an OpenCode turn.
pub(all) enum Input {
Prompt(String)
UserInputs(Array[UserInput])
} derive(Eq)
///|
/// One persisted OpenCode session.
pub struct Thread {
priv exec : OpenCodeExec
priv mut id_value : String?
priv thread_options : ThreadOptions
}
///|
/// Return the persisted session identifier after the first event arrives.
pub fn Thread::id(self : Thread) -> String? {
self.id_value
}
///|
fn Thread::Thread(
exec : OpenCodeExec,
thread_options : ThreadOptions,
id? : String,
) -> Thread {
{ exec, id_value: id, thread_options }
}
///|
/// Run one turn and deliver typed OpenCode events as they arrive.
pub async fn Thread::run_streamed(
self : Thread,
input : Input,
on_event : async (ThreadEvent) -> Unit,
) -> Unit {
self.run_streamed_internal(input, async fn(event) {
on_event(event)
true
})
}
///|
async fn Thread::run_streamed_internal(
self : Thread,
input : Input,
on_event : async (ThreadEvent) -> Bool,
) -> Unit {
let normalized = normalize_input(input)
let options = self.thread_options
let mut turn_failure : String? = None
let result = Ok(
self.exec.run(
{
input: normalized.prompt,
session_id: self.id_value,
files: normalized.files,
model: options.model,
agent: options.agent,
working_directory: options.working_directory,
variant: options.variant,
title: options.title,
thinking: options.thinking,
},
async fn(event) {
if self.id_value is None && event.session_id() is Some(session_id) {
self.id_value = Some(session_id)
}
let should_continue = on_event(event)
if event is StreamError(error) {
turn_failure = Some(error.message)
false
} else {
should_continue
}
},
),
) catch {
error => Err(error)
}
if turn_failure is Some(message) {
raise TurnFailed(message~)
}
match result {
Ok(_) => ()
Err(error) => raise error
}
}
///|
/// Run one turn and buffer its typed events and final text response.
pub async fn Thread::run(self : Thread, input : Input) -> Turn {
let events : Array[ThreadEvent] = []
let texts : Array[String] = []
self.run_streamed_internal(input, fn(event) {
events.push(event)
if event is Text(text) {
texts.push(text.text)
}
true
})
{ events: ReadOnlyArray::from_array(events), final_response: texts.join("") }
}
///|
priv struct MoonBitInternalNormalizedInput {
prompt : String
files : Array[@path.Path]
}
///|
fn normalize_input(input : Input) -> MoonBitInternalNormalizedInput {
match input {
Prompt(prompt) => { prompt, files: [] }
UserInputs(items) => {
let prompt = items
.iter()
.filter_map(item => {
match item {
Text(text) => Some(text)
LocalFile(_) => None
}
})
.join("\n\n")
let files = items
.iter()
.filter_map(item => {
match item {
Text(_) => None
LocalFile(path) => Some(path)
}
})
.collect()
{ prompt, files }
}
}
}