use super::model_call::ModelCall;
use super::step_commit::{
DEFAULT_MAX_TOKENS, commit_dispatch, compose_system, read_branch_tip, spawn_branch,
write_dispatch_files, write_meta, write_request,
};
use super::tool_step::{self, run_tool_calls};
use super::{
Resolved, assembler, child_result, drain, driver, model_call, result_deposit, stop_signal,
terminal, tools, transcript,
};
use crate::prompt::inbox::{self, Epitaph};
use crate::prompt::resolve::{ConfigSource, WorkerConfig, resolve_worker};
use crate::prompt::step::{RESPONSE_FILE, STAGING_FILE, StepMeta, step_dir_rel};
use crate::prompt::{Deps, Error};
use brazen::Content;
use std::path::Path;
#[allow(clippy::too_many_arguments)]
pub(in crate::prompt) fn run_exchange(
repo: &Path,
user_message: &str,
fork_point: &str,
name: Option<&str>,
pins: &crate::prompt::PinnedDocs,
cwd: Option<&Path>,
first: &Resolved<'_>,
deps: &Deps<'_>,
) -> Result<String, Error> {
let ts = deps.clock.now_compact();
let short_id = deps.id_gen.short();
let conv_id = format!("{ts}-{short_id}");
let branch_name = conv_id.clone();
let worktree_path = crate::workspace::agent_worktree(repo, &conv_id);
let inbox = inbox::inbox_dir(repo, &conv_id);
let executor_lock = match inbox::try_acquire(&inbox).map_err(|source| Error::ExecutorLock {
path: inbox.clone(),
source,
})? {
Some(guard) => guard,
None => return Ok(branch_name),
};
crate::prompt::seed_cwd(repo, &conv_id, cwd, deps.git)?;
spawn_branch(repo, &worktree_path, &conv_id, fork_point, deps)?;
inbox::deposit(repo, &conv_id, inbox::USER_SENDER, user_message, deps.clock)?;
let mut step_seq: u32 = 1;
let mut seen;
let mut followed: Option<WorkerConfig> = None;
let epitaph = loop {
if step_seq > 1 {
followed = Some(resolve_worker(repo, ConfigSource::Agent(&conv_id), deps)?);
}
let boundary = followed.as_ref().map(WorkerConfig::as_resolved);
let resolved: &Resolved<'_> = boundary.as_ref().unwrap_or(first);
if step_seq == 1 {
write_dispatch_files(&worktree_path, user_message, &resolved.soul, pins)?;
commit_dispatch(&worktree_path, &conv_id, name, pins, resolved, deps)?;
}
seen = drain::drain(&worktree_path, &inbox, &conv_id, deps.git)?.left;
child_result::interpret_pending(repo, &conv_id, &worktree_path, resolved.workflow, deps)?;
let commit_sha = read_branch_tip(&worktree_path, deps)?;
if stop_signal::stopped(deps.stop) {
break Epitaph::Stopped;
}
if terminal::budget_exhausted(
repo,
&conv_id,
&branch_name,
&worktree_path,
&resolved.budgets,
deps,
)? {
break Epitaph::BudgetExhausted;
}
let system_with_goal = compose_system(user_message, name, &resolved.soul);
let call = ModelCall {
adapter: deps.adapter,
sleeper: deps.sleeper,
binary: &resolved.binary,
provider_row: resolved.provider_row,
retry: resolved.retry,
stop: deps.stop,
expect_handshake: resolved.expect_handshake,
};
let messages = assembler::assemble(&worktree_path, resolved.manifest)?;
let tools = tools::compose(
&worktree_path,
resolved.grant.tools,
&messages,
&tools::injected(resolved.grant.role, deps.tool_executor, repo, &conv_id),
)?;
let request = model_call::build_request(
resolved.model_id,
&system_with_goal,
messages,
tools,
DEFAULT_MAX_TOKENS,
);
let request_value =
serde_json::to_value(&request).expect("CanonicalRequest is always serializable");
let step_dir_rel_str = step_dir_rel(&conv_id, step_seq);
write_request(repo, &step_dir_rel_str, &request_value)?;
let request_bytes =
serde_json::to_vec(&request).expect("CanonicalRequest is always serializable");
let started_at = deps.clock.now_iso8601();
let response_path = repo.join(&step_dir_rel_str).join(RESPONSE_FILE);
let call_outcome = model_call::run(&call, &request_bytes, &response_path);
if stop_signal::stopped(deps.stop) {
break Epitaph::Stopped;
}
call_outcome?;
let ended_at = deps.clock.now_iso8601();
write_meta(
repo,
&step_dir_rel_str,
&StepMeta {
commit: commit_sha,
started_at,
ended_at,
},
)?;
let staging_path = repo.join(&step_dir_rel_str).join(STAGING_FILE);
let assistant_content = transcript::commit_assistant(
&worktree_path,
&conv_id,
resolved.model_id,
&staging_path,
deps.git,
)?;
if !assistant_content
.iter()
.any(|b| matches!(b, Content::ToolUse { .. }))
{
let response = result_deposit::terminal_text(&assistant_content);
result_deposit::deposit_terminal(
repo,
&conv_id,
&worktree_path,
Epitaph::FinalResponse,
response.as_deref(),
deps,
)?;
break Epitaph::FinalResponse;
}
let window = run_tool_calls(
repo,
&worktree_path,
&conv_id,
resolved,
&step_dir_rel_str,
&assistant_content,
deps,
)?;
match window {
tool_step::ToolWindow::Stopped => break Epitaph::Stopped,
tool_step::ToolWindow::Held => {
driver::release_then_reprobe(executor_lock, repo, &conv_id, &seen, deps.launcher);
return Ok(branch_name);
}
tool_step::ToolWindow::Completed => {}
}
child_result::run_flush(repo, &conv_id, &worktree_path, resolved.workflow, deps)?;
step_seq += 1;
};
let workflow = followed.as_ref().map_or(first.workflow, |c| &c.workflow);
terminal::conclude(
repo,
&conv_id,
epitaph,
workflow,
executor_lock,
&seen,
deps,
)?;
Ok(branch_name)
}