2026-07-20 10:55:09 +07:00
//! Agent turn engine — runs LLM + tool execution on a background thread.
//!
//! Flow: push user message → spawn OS thread → loop: call blocking LLM
//! client → execute tool calls → push TurnEvents → repeat until done.
2026-07-20 12:02:48 +07:00
use std ::path ::{ Path , PathBuf };
2026-07-20 10:55:09 +07:00
use std ::sync ::atomic ::{ AtomicBool , Ordering };
use std ::sync ::{ Arc , Mutex };
use std ::collections ::VecDeque ;
use tracing ::{ debug , info , warn };
2026-07-20 11:29:33 +07:00
use zesdex_domain ::core ::tool_call ::sanitize_tool_arguments ;
2026-07-20 10:55:09 +07:00
use zesdex_domain ::core ::ChatMessage ;
use zesdex_infrastructure ::llm ::provider ::LlmClient ;
use zesdex_infrastructure ::tools ::{ all_tools , tool_defs , ToolCtx };
use zesdex_infrastructure ::TurnEvent ;
use crate ::state ::AppStateRest ;
/// Spawn an agent turn on a background OS thread.
2026-07-20 15:53:20 +07:00
///
/// Flow: compare-exchange the in-flight flag → snapshot state fields →
/// clone session runtime messages → push user message → spawn OS thread
/// that runs `run_turn`.
#[tracing::instrument(skip(state))]
2026-07-20 10:55:09 +07:00
pub fn spawn_agent_turn ( state : & mut AppStateRest , text : String ) {
2026-07-20 13:52:20 +07:00
// compare_exchange: only mark in-flight if not already running
if state . turn_in_flight_flag
. compare_exchange ( false , true , Ordering ::SeqCst , Ordering ::Relaxed )
. is_err ()
{
return ; // already running
2026-07-20 10:55:09 +07:00
}
let turn_events = state . turn_events . clone ();
let in_flight = state . turn_in_flight_flag . clone ();
let abort = state . abort_flag . clone ();
let session_dir = state . session_dir . clone ();
let workspace_roots = state . workspace_roots . clone ();
2026-07-20 14:30:36 +07:00
// Resolve LLM provider configuration from settings
let provider_name = & state . settings . provider ;
let provider_cfg = state . app_config . providers . get ( provider_name ). cloned ();
let mut api_key = String ::new ();
if let Some ( key ) = state . settings . api_keys . get ( provider_name ) {
api_key = key . clone ();
} else if let Some ( ref cfg ) = provider_cfg {
if let Some ( ref default_key ) = cfg . default_api_key {
api_key = default_key . clone ();
}
if api_key . is_empty () {
if let Some ( ref env_name ) = cfg . api_key_env {
if let Ok ( val ) = std ::env ::var ( env_name ) {
api_key = val ;
}
}
}
}
let model = state . settings . model . clone ();
let api_base = provider_cfg . map ( | cfg | cfg . api_base . clone ());
2026-07-20 10:55:09 +07:00
let mut messages : Vec < ChatMessage > = state
. session_runtime
. as_ref ()
. map ( | rt | rt . messages . clone ())
. unwrap_or_default ();
messages . push ( ChatMessage ::user ( text ));
if let Some ( ref mut rt ) = state . session_runtime {
rt . messages = messages . clone ();
}
2026-07-20 14:30:36 +07:00
info! ( "spawning agent turn with {} messages (model: {})" , messages . len (), model );
2026-07-20 10:55:09 +07:00
std ::thread ::spawn ( move || {
2026-07-20 15:21:23 +07:00
let params = TurnParams {
messages : & mut messages ,
session_dir : & session_dir ,
workspace_roots : & workspace_roots ,
turn_events : & turn_events ,
in_flight : & in_flight ,
abort : & abort ,
2026-07-20 14:30:36 +07:00
api_key ,
model ,
api_base ,
2026-07-20 15:21:23 +07:00
};
run_turn ( params );
2026-07-20 10:55:09 +07:00
});
}
2026-07-20 15:21:23 +07:00
/// Parameters for a turn, grouped to avoid too-many-arguments lint.
2026-07-20 15:53:20 +07:00
///
/// Holds all the references and owned values that `run_turn` needs:
/// message history, turn-event queue, abort/in-flight flags, API credentials,
/// and environment paths.
2026-07-20 15:21:23 +07:00
struct TurnParams < 'a > {
messages : & 'a mut Vec < ChatMessage > ,
session_dir : & 'a Path ,
workspace_roots : & 'a [ PathBuf ],
turn_events : & 'a Arc < Mutex < VecDeque < TurnEvent >>> ,
in_flight : & 'a Arc < AtomicBool > ,
abort : & 'a Arc < AtomicBool > ,
2026-07-20 14:30:36 +07:00
api_key : String ,
model : String ,
api_base : Option < String > ,
2026-07-20 15:21:23 +07:00
}
/// The core agent turn — LLM call → tool execution → repeat.
2026-07-20 15:53:20 +07:00
///
/// Flow: build `LlmClient` → compile tools → prepend system message →
/// loop (max 50 iterations): abort check → stream LLM response →
/// push events → execute tool calls → push results → break on
/// no tool calls or error → emit final `Compacted` + `Done`.
#[tracing::instrument(skip(params))]
2026-07-20 15:21:23 +07:00
fn run_turn ( params : TurnParams ) {
let client = LlmClient ::new ( params . api_key , params . model , params . api_base );
2026-07-20 10:55:09 +07:00
let tools = all_tools ();
let defs = tool_defs ( & tools );
2026-07-20 16:07:10 +07:00
let mut sys_prompt = "You are Zesdex, an AI coding assistant. You have access to various tools via native function calling to help the user. \
2026-07-20 15:21:23 +07:00
For complex problems, use `seq_think` to reason step-by-step. \
2026-07-20 14:47:13 +07:00
For any non-trivial tasks, you MUST prioritize creating a structured plan (using `plan_enter`) and a list of TODOs (using `todowrite`) BEFORE executing any other tools or modifying files. \
2026-07-20 15:21:23 +07:00
For large multi-step operations or delegating tasks, you MUST prioritize using `workflow_run` (to run a yaml workflow script) or `hive_mind` (to orchestrate multiple agents) to complete the task efficiently. \
2026-07-20 14:47:13 +07:00
Use other tools when necessary. If a tool returns an error, fix the issue before retrying. \
2026-07-20 16:07:10 +07:00
Respond conversationally, concisely, and helpfully." . to_string ();
if let Some ( root ) = params . workspace_roots . first () {
let tree = zesdex_infrastructure ::utils ::build_workspace_tree ( root , 800 );
2026-07-20 16:11:15 +07:00
let rich_ctx = zesdex_infrastructure ::utils ::build_rich_context ( root );
2026-07-20 16:07:10 +07:00
sys_prompt . push_str ( " \n\n Workspace structure: \n ``` \n " );
sys_prompt . push_str ( & tree );
2026-07-20 16:11:15 +07:00
sys_prompt . push_str ( " \n ``` \n\n " );
sys_prompt . push_str ( & rich_ctx );
2026-07-20 16:07:10 +07:00
}
let sys_msg = ChatMessage ::system ( sys_prompt );
2026-07-20 15:21:23 +07:00
params . messages . insert ( 0 , sys_msg );
2026-07-20 11:29:33 +07:00
2026-07-20 10:55:09 +07:00
let tool_ctx = ToolCtx ::builder ()
2026-07-20 15:21:23 +07:00
. session_dir ( params . session_dir . to_path_buf ())
. workspaces ( params . workspace_roots . to_vec ())
. turn_events ( Arc ::clone ( params . turn_events ))
2026-07-20 10:55:09 +07:00
. build ();
for iteration in 0 .. 50 {
2026-07-20 15:21:23 +07:00
if params . abort . load ( Ordering ::SeqCst ) {
params . abort . store ( false , Ordering ::SeqCst );
push_event ( params . turn_events , TurnEvent ::SystemNote {
2026-07-20 10:55:09 +07:00
kind : "info" . into (),
message : "Turn aborted by user" . into (),
});
break ;
}
debug! ( "agent turn iteration {iteration}" );
2026-07-20 14:51:16 +07:00
// Stream the LLM response
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::StreamStart );
2026-07-20 14:51:16 +07:00
let result = client . chat_with_tools_streaming (
2026-07-20 15:21:23 +07:00
params . messages ,
2026-07-20 10:55:09 +07:00
Some ( defs . clone ()),
Some ( 0.7 ),
2026-07-20 14:51:16 +07:00
Some ( 4096 ),
| event | {
2026-07-20 15:21:23 +07:00
if params . abort . load ( Ordering ::SeqCst ) {
2026-07-20 14:51:16 +07:00
return false ;
}
match event {
2026-07-20 14:56:03 +07:00
zesdex_domain ::core ::StreamEvent ::Token ( s ) => {
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::StreamToken ( s . clone ()));
2026-07-20 14:51:16 +07:00
}
2026-07-20 14:56:03 +07:00
zesdex_domain ::core ::StreamEvent ::Reasoning ( s ) => {
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::StreamReasoning ( s . clone ()));
2026-07-20 14:56:03 +07:00
}
2026-07-20 14:51:16 +07:00
_ => {}
}
true
},
2026-07-20 15:21:23 +07:00
Some ( params . abort ),
2026-07-20 10:55:09 +07:00
);
match result {
Ok (( assistant_msg , usage )) => {
let content = assistant_msg . content . clone (). unwrap_or_default ();
let tool_calls = assistant_msg . tool_calls . clone (). unwrap_or_default ();
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::StreamDone ( assistant_msg . clone ()));
2026-07-20 14:51:16 +07:00
2026-07-20 10:55:09 +07:00
if let Some (( tokens_in , tokens_out )) = usage {
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::Usage {
2026-07-20 10:55:09 +07:00
tokens_in ,
tokens_out ,
});
}
if tool_calls . is_empty () {
2026-07-20 15:21:23 +07:00
params . messages . push ( ChatMessage ::assistant ( Some ( content )));
2026-07-20 10:55:09 +07:00
break ;
}
2026-07-20 15:21:23 +07:00
params . messages . push ( assistant_msg );
2026-07-20 10:55:09 +07:00
for tc in & tool_calls {
let name = & tc . function . name ;
2026-07-20 11:29:33 +07:00
let args = sanitize_tool_arguments ( & tc . function . arguments );
2026-07-20 10:55:09 +07:00
debug! ( "executing tool: {name}" );
let output = if let Some ( tool ) = tools . iter (). find ( | t | t . name () == name ) {
match tool . run ( & tool_ctx , & args ) {
Ok ( o ) => o ,
Err ( e ) => format! ( "Error: {e} " ),
}
} else {
format! ( "Unknown tool: {name} " )
};
let is_error = output . starts_with ( "Error:" );
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::ToolResult {
2026-07-20 10:55:09 +07:00
tool_call_id : tc . id . clone (),
tool_name : name . clone (),
output : output . clone (),
is_error ,
path : None ,
});
2026-07-20 15:21:23 +07:00
params . messages . push ( ChatMessage ::tool ( tc . id . clone (), output . clone ()));
2026-07-20 10:55:09 +07:00
}
}
Err ( e ) => {
warn! ( "LLM call failed: {e}" );
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::Error ( format! ( "LLM error: {e} " )));
2026-07-20 10:55:09 +07:00
break ;
}
}
}
2026-07-20 12:26:08 +07:00
// Propagate accumulated messages back to session_runtime so the next
// turn starts with the full history (assistant replies + tool results).
2026-07-20 15:21:23 +07:00
push_event ( params . turn_events , TurnEvent ::Compacted ( params . messages . clone ()));
push_event ( params . turn_events , TurnEvent ::Done );
mark_done ( params . in_flight );
2026-07-20 10:55:09 +07:00
}
fn push_event ( queue : & Arc < Mutex < VecDeque < TurnEvent >>> , event : TurnEvent ) {
if let Ok ( mut q ) = queue . lock () {
q . push_back ( event );
}
}
2026-07-20 13:52:20 +07:00
/// Mark the turn as done using lock-free atomic store.
fn mark_done ( flag : & Arc < AtomicBool > ) {
flag . store ( false , Ordering ::SeqCst );
2026-07-20 10:55:09 +07:00
}