// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Completed turn.
pub struct Turn {
/// All items produced during the turn
items : Array[ThreadItem]
/// The final response text from the agent
final_response : String
/// Token usage information, if available
usage : Usage?
}
///|
/// Alias for `Turn` to describe the result of `run()`.
pub type RunResult = Turn
///|
/// The result of the `run_streamed` method.
#alias(RunStreamedResult)
pub struct StreamedTurn {
/// An async iterator of events produced during the turn
events : @generator.AsyncGenerator[Event]
}
///|
/// An input to send to the agent.
pub type Input = String
///|
/// Represent a thread of conversation with the agent. One thread can have multiple consecutive turns.
struct Thread {
exec : CodexExec
options : CodexOptions
mut id : String?
thread_options : ThreadOptions
}
///|
/// Returns the ID of the thread. Populated after the first turn starts.
pub fn Thread::id(self : Thread) -> String? {
self.id
}
///|
/// Create a new Thread instance.
///
/// # Arguments
/// * `exec` - The CodexExec instance for running commands
/// * `options` - Configuration options for the Codex client
/// * `thread_options` - Options specific to this thread
/// * `id` - Optional thread ID to resume an existing thread
fn Thread::new(
exec : CodexExec,
options : CodexOptions,
thread_options : ThreadOptions,
id? : String,
) -> Thread {
{ exec, options, id, thread_options }
}
///|
/// Provides the input to the agent and streams events as they are produced during the turn.
///
/// # Arguments
/// * `input` - The user input/prompt to send to the agent
/// * `turn_options` - Options for configuring this turn
///
/// # Returns
/// A StreamedTurn containing an async iterator of events
pub async fn Thread::run_streamed(
self : Thread,
input : String,
turn_options? : TurnOptions = TurnOptions::{ output_schema: None },
) -> StreamedTurn {
{ events: self.run_streamed_internal(input, turn_options) }
}
///|
/// Internal implementation of run_streamed that returns the event iterator directly.
async fn Thread::run_streamed_internal(
self : Thread,
input : String,
turn_options : TurnOptions,
) -> @generator.AsyncGenerator[Event] {
let schema_file = if turn_options.output_schema is Some(schema) &&
!(schema is Null) {
Some(create_output_schema_file(schema))
} else {
None
}
let options = self.thread_options
let generator = self.exec.run({
input,
base_url: self.options.base_url,
api_key: self.options.api_key,
thread_id: self.id,
model: options.model,
sandbox_mode: options.sandbox_mode,
working_directory: options.working_directory,
skip_git_repo_check: options.skip_git_repo_check,
output_schema_file: if schema_file is Some(sf) {
Some(sf.schema_path)
} else {
None
},
}) catch {
e => @error.reraise(e)
}
@generator.AsyncGenerator::new(async fn(yield_) {
try {
while generator.next() is Some(item) {
let event = @json.from_json(@json.parse(item)) catch {
error => @error.fail("Failed to parse item: \{item}, error: \{error}")
}
if event is ThreadStarted(thread_id~) {
self.id = Some(thread_id)
}
yield_(event)
}
} catch {
e => {
if schema_file is Some(sf) {
(sf.cleanup)()
}
while generator.returns() is Some(_) {
// Drain remaining items to avoid unhandled promise rejection
}
if !(e is @generator.Return) {
@error.reraise(e)
}
}
}
})
}
///|
/// Provides the input to the agent and returns the completed turn.
///
/// # Arguments
/// * `input` - The user input/prompt to send to the agent
/// * `turn_options` - Options for configuring this turn
///
/// # Returns
/// A Turn containing all items, the final response, and usage information
pub async fn Thread::run(
self : Thread,
input : String,
turn_options? : TurnOptions = TurnOptions::{ output_schema: None },
) -> Turn raise Error {
let generator = self.run_streamed_internal(input, turn_options)
let items : Array[ThreadItem] = []
let mut final_response = ""
let mut usage : Usage? = None
while generator.next() is Some(event) {
match event {
ItemCompleted(item) => {
if item is AgentMessageItem(text~, ..) {
final_response = text
}
items.push(item)
}
TurnCompleted(u) => usage = Some(u)
TurnFailed(error) => {
while generator.returns() is Some(_) {
// Drain remaining items to avoid unhandled promise rejection
}
@error.fail(error.message)
}
ThreadErrorEvent(e) => {
while generator.returns() is Some(_) {
// Drain remaining items to avoid unhandled promise rejection
}
@error.fail(e)
}
_ => ()
}
}
{ items, final_response, usage }
}