Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
set it directly in `config.toml` or a task graph (bypassing the `--init` wizard, which
already documents the reservation) a runtime signal that the value is currently inert.

### Fixed

- `zeph-core`: `TurnSummary::tool_calls` was hardcoded to `0` at its only production
construction site, so any consumer of the per-turn completion notification always saw
zero tool calls regardless of actual turn activity (#6273). Added a `turn_tool_calls`
counter to `LifecycleState`, mirroring the existing `turn_llm_requests` pattern: reset
in `begin_turn`, incremented in `check_and_update_quota` for every tool call in a
dispatch batch, and read into `TurnSummary::tool_calls` when the turn's notification
summary is built.

### Changed

- `zeph-core`: collapsed 9 of 10 identified near-identical `CommandError`-wrapping delegate
boilerplate blocks in `agent_access_impl.rs` (#6262) into a single `delegate_cmd!`
declarative macro that forwards an optional argument to an inner async method and maps
its error via `CommandError::new(e.to_string())`. Covers `lsp_status`, `handle_skill`,
`handle_skills`, `handle_feedback_command`, `handle_plan` (scheduler feature),
`handle_experiment`, `list_worktrees`, `clean_worktrees`, and `handle_cocoon` (cocoon
feature). The 10th identified site, `handle_mcp`'s `_` match arm, was intentionally left
inline — it is a match arm inside a larger `match sub.as_str()` block, not a standalone
handler fn, so it does not fit the macro's shape. No behavior change.

## [0.22.1] - 2026-07-15
### Fixed

Expand Down
119 changes: 28 additions & 91 deletions crates/zeph-core/src/agent/agent_access_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ async fn semantic_scan_plugin_add(
Ok(None)
}

/// Delegates an `AgentAccess` command handler to an inner async method, wrapping the
/// inner method's error in [`CommandError`]. Covers the pass-through shape shared by
/// most command handlers below: an optional single argument forwarded verbatim, and
/// the returned `Result<T, E>` mapped to `Result<T, CommandError>` via `e.to_string()`.
macro_rules! delegate_cmd {
($name:ident, $inner:ident $(, $arg:ident : $arg_ty:ty)? => $out:ty) => {
fn $name<'a>(
&'a mut self,
$($arg: $arg_ty)?
) -> Pin<Box<dyn Future<Output = Result<$out, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.$inner($($arg)?)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
};
}

impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {
// ----- /memory -----

Expand Down Expand Up @@ -1056,15 +1075,7 @@ impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {

// ----- /lsp -----

fn lsp_status<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.handle_lsp_status_as_string()
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(lsp_status, handle_lsp_status_as_string => String);

// ----- /recap -----

Expand Down Expand Up @@ -1298,59 +1309,20 @@ impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {

// ----- /skill -----

fn handle_skill<'a>(
&'a mut self,
args: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
let args_owned = args.to_owned();
Box::pin(async move {
self.handle_skill_command_as_string(&args_owned)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_skill, handle_skill_command_as_string, args: &'a str => String);

// ----- /skills -----

fn handle_skills<'a>(
&'a mut self,
args: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
let args_owned = args.to_owned();
Box::pin(async move {
self.handle_skills_as_string(&args_owned)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_skills, handle_skills_as_string, args: &'a str => String);

// ----- /feedback -----

fn handle_feedback_command<'a>(
&'a mut self,
args: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
let args_owned = args.to_owned();
Box::pin(async move {
self.handle_feedback_as_string(&args_owned)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_feedback_command, handle_feedback_as_string, args: &'a str => String);

// ----- /plan -----

#[cfg(feature = "scheduler")]
fn handle_plan<'a>(
&'a mut self,
input: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.dispatch_plan_command_as_string(input)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_plan, dispatch_plan_command_as_string, input: &'a str => String);

#[cfg(not(feature = "scheduler"))]
fn handle_plan<'a>(
Expand All @@ -1362,16 +1334,7 @@ impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {

// ----- /experiment -----

fn handle_experiment<'a>(
&'a mut self,
input: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.handle_experiment_command_as_string(input)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_experiment, handle_experiment_command_as_string, input: &'a str => String);

// ----- /agent, @mention -----

Expand Down Expand Up @@ -1514,16 +1477,7 @@ impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {
// ----- /cocoon -----

#[cfg(feature = "cocoon")]
fn handle_cocoon<'a>(
&'a mut self,
args: &'a str,
) -> Pin<Box<dyn Future<Output = Result<String, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.handle_cocoon_as_string(args)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(handle_cocoon, handle_cocoon_as_string, args: &'a str => String);

#[cfg(not(feature = "cocoon"))]
fn handle_cocoon<'a>(
Expand Down Expand Up @@ -1883,26 +1837,9 @@ impl<C: Channel + Send + 'static> AgentAccess for Agent<C> {

// ----- /worktree -----

fn list_worktrees<'a>(
&'a mut self,
) -> Pin<Box<dyn Future<Output = Result<Option<String>, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.handle_worktree_list_as_string()
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(list_worktrees, handle_worktree_list_as_string => Option<String>);

fn clean_worktrees<'a>(
&'a mut self,
force: bool,
) -> Pin<Box<dyn Future<Output = Result<Option<String>, CommandError>> + Send + 'a>> {
Box::pin(async move {
self.handle_worktree_clean_as_string(force)
.await
.map_err(|e| CommandError::new(e.to_string()))
})
}
delegate_cmd!(clean_worktrees, handle_worktree_clean_as_string, force: bool => Option<String>);

// ----- /cd -----

Expand Down
5 changes: 3 additions & 2 deletions crates/zeph-core/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,8 @@ impl<C: Channel> Agent<C> {
self.services.security.user_provided_urls.write().clear();
// Reset per-turn LLM request counter for the notification gate.
self.runtime.lifecycle.turn_llm_requests = 0;
// Reset per-turn tool-call dispatch counter (feeds TurnSummary::tool_calls).
self.runtime.lifecycle.turn_tool_calls = 0;

// Spec 050 §2: drain pending risk signals from executor layers before advancing.
// Also advance MAGE accumulator (spec 004-16 FR-009) and ingest mapped signals.
Expand Down Expand Up @@ -1391,8 +1393,7 @@ impl<C: Channel> Agent<C> {
let summary = crate::notifications::TurnSummary {
duration_ms,
preview: self.last_assistant_preview(160),
// TODO: wire turn_tool_calls counter once LifecycleState tracks it (Phase 2).
tool_calls: 0,
tool_calls: self.runtime.lifecycle.turn_tool_calls,
llm_requests: self.runtime.lifecycle.turn_llm_requests,
exit_status: if is_error {
crate::notifications::TurnExitStatus::Error
Expand Down
4 changes: 4 additions & 0 deletions crates/zeph-core/src/agent/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ pub(crate) struct LifecycleState {
pub(crate) notifier: Option<crate::notifications::Notifier>,
/// Per-turn LLM request counter. Incremented by `process_response`; reset at turn start.
pub(crate) turn_llm_requests: u32,
/// Per-turn tool-call dispatch counter. Incremented by `check_and_update_quota` for every
/// tool call in a dispatch batch; reset at turn start. Feeds `TurnSummary::tool_calls`.
pub(crate) turn_tool_calls: u32,
/// Timestamp of the last turn that ended with `LlmError::NoProviders`.
///
/// Used to gate `advance_context_lifecycle`: when all providers are down, context preparation
Expand Down Expand Up @@ -1442,6 +1445,7 @@ impl LifecycleState {
bg_metrics_tick: None,
notifier: None,
turn_llm_requests: 0,
turn_tool_calls: 0,
last_no_providers_at: None,
pending_background_completions: VecDeque::new(),
background_completion_rx: None,
Expand Down
8 changes: 7 additions & 1 deletion crates/zeph-core/src/agent/tool_execution/tier_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,10 +1206,16 @@ impl<C: Channel> Agent<C> {
);
return true;
}
let batch_count = u32::try_from(batch_len).unwrap_or(u32::MAX);
self.tool_orchestrator.session_tool_call_count = self
.tool_orchestrator
.session_tool_call_count
.saturating_add(u32::try_from(batch_len).unwrap_or(u32::MAX));
.saturating_add(batch_count);
self.runtime.lifecycle.turn_tool_calls = self
.runtime
.lifecycle
.turn_tool_calls
.saturating_add(batch_count);
false
}

Expand Down
Loading