use std::cell::{Cell, RefCell};
use std::sync::{Mutex, MutexGuard, PoisonError};
#[cfg(feature = "ast")]
use std::time::Duration;
use std::time::Instant;
#[cfg(feature = "ast")]
use harn_vm::agent_events::{AgentEvent, ToolCallStatus, ToolMutationStatus};
#[cfg(feature = "ast")]
pub(crate) const INTERRUPT_POLL: Duration = Duration::from_millis(50);
#[cfg(feature = "ast")]
pub(crate) const PROGRESS_HEARTBEAT: Duration = Duration::from_secs(1);
static WORKSPACE_WRITES: Mutex<()> = Mutex::new(());
pub(crate) struct WorkspaceWrites(#[allow(dead_code)] MutexGuard<'static, ()>);
pub(crate) fn workspace_writes() -> WorkspaceWrites {
WorkspaceWrites(
WORKSPACE_WRITES
.lock()
.unwrap_or_else(PoisonError::into_inner),
)
}
#[cfg(feature = "ast")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Interrupt {
Cancelled,
DeadlineExceeded,
}
#[cfg(feature = "ast")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Phase {
ServerStart,
Indexing,
Resolving,
Applying,
}
#[cfg(feature = "ast")]
impl Phase {
fn as_str(self) -> &'static str {
match self {
Self::ServerStart => "server_start",
Self::Indexing => "indexing",
Self::Resolving => "resolving",
Self::Applying => "applying",
}
}
}
#[cfg_attr(
not(feature = "ast"),
expect(dead_code, reason = "only the code index reads call progress state")
)]
#[derive(Clone, Debug)]
pub(crate) struct CallAddress {
pub session_id: String,
pub tool_call_id: String,
pub tool_name: String,
}
impl CallAddress {
pub(crate) fn current() -> Option<Self> {
let session_id = harn_vm::agent_sessions::current_session_id()?;
let tool_call_id = harn_vm::agent_sessions::current_tool_call_id()?;
let tool_name =
harn_vm::tool_call_cancellations::lookup(&session_id, &tool_call_id)?.tool_name;
Some(Self {
session_id,
tool_call_id,
tool_name,
})
}
}
#[cfg_attr(
not(feature = "ast"),
expect(dead_code, reason = "only the code index reads call progress state")
)]
#[derive(Clone, Debug)]
pub(crate) struct HostCallContext {
call: Option<CallAddress>,
budget: Option<Instant>,
started: Instant,
last_report: Cell<Option<Instant>>,
}
thread_local! {
static CURRENT: RefCell<Option<HostCallContext>> = const { RefCell::new(None) };
}
pub(crate) struct HostCallGuard(Option<HostCallContext>);
impl Drop for HostCallGuard {
fn drop(&mut self) {
let previous = self.0.take();
CURRENT.with(|slot| *slot.borrow_mut() = previous);
}
}
impl HostCallContext {
pub(crate) fn new(call: Option<CallAddress>) -> Self {
Self {
call,
budget: None,
started: Instant::now(),
last_report: Cell::new(None),
}
}
#[cfg(feature = "ast")]
pub(crate) fn current() -> Self {
CURRENT
.with(|slot| slot.borrow().clone())
.unwrap_or_else(|| Self::new(None))
}
pub(crate) fn enter(self) -> HostCallGuard {
HostCallGuard(CURRENT.with(|slot| slot.borrow_mut().replace(self)))
}
#[cfg(feature = "ast")]
pub(crate) fn with_budget(mut self, budget: Duration) -> Self {
let until = Instant::now() + budget;
self.budget = Some(self.budget.map_or(until, |earlier| earlier.min(until)));
self
}
#[cfg(feature = "ast")]
pub(crate) fn interrupted(&self) -> Option<Interrupt> {
if harn_vm::op_interrupt::cancel_requested() {
return Some(Interrupt::Cancelled);
}
let expired = harn_vm::op_interrupt::requested()
|| self.budget.is_some_and(|budget| Instant::now() >= budget);
expired.then_some(Interrupt::DeadlineExceeded)
}
#[cfg(feature = "ast")]
pub(crate) fn commit<T>(
&self,
detail: &str,
write: impl FnOnce(&WorkspaceWrites) -> T,
) -> Result<T, Interrupt> {
let writes = workspace_writes();
if let Some(interrupt) = self.interrupted() {
return Err(interrupt);
}
self.report(Phase::Applying, detail);
Ok(write(&writes))
}
#[cfg(feature = "ast")]
pub(crate) fn report(&self, phase: Phase, detail: &str) {
let now = Instant::now();
self.last_report.set(Some(now));
let elapsed_ms = now.duration_since(self.started).as_millis() as u64;
tracing::debug!(
target: "harn_hostlib::host_call",
phase = phase.as_str(),
detail,
elapsed_ms,
"host call progress",
);
let Some(call) = &self.call else {
return;
};
harn_vm::agent_events::emit_event(&AgentEvent::ToolCallUpdate {
session_id: call.session_id.clone(),
tool_call_id: call.tool_call_id.clone(),
tool_name: call.tool_name.clone(),
status: ToolCallStatus::InProgress,
raw_output: None,
error: None,
duration_ms: None,
execution_duration_ms: None,
error_category: None,
mutation_status: ToolMutationStatus::Unknown,
changed_paths: None,
data: Some(serde_json::json!({
"progress": {
"phase": phase.as_str(),
"detail": detail,
"elapsed_ms": elapsed_ms,
"cancellable": true,
},
})),
health: None,
executor: None,
parsing: None,
raw_input: None,
raw_input_partial: None,
audit: None,
});
}
#[cfg(feature = "ast")]
pub(crate) fn heartbeat(&self, phase: Phase, detail: &str) {
let quiet = self
.last_report
.get()
.is_none_or(|last| last.elapsed() >= PROGRESS_HEARTBEAT);
if quiet {
self.report(phase, detail);
}
}
}