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
18 changes: 9 additions & 9 deletions crates/hi-agent/src/agent/coding_memory_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ impl crate::Agent {
/// coding facts (verify command, package ownership, stack, test gate) into
/// the session decision log and project memory. Best-effort, no model call.
pub(crate) fn record_coding_facts_turn_end(&mut self, ui: &mut dyn Ui) {
if self.coding_facts_written >= MAX_CODING_FACTS_PER_SESSION {
if self.subagents.coding_facts_written >= MAX_CODING_FACTS_PER_SESSION {
return;
}
if self.last_verify != Some(true) || self.last_changed_files.is_empty() {
if self.report.last_verify != Some(true) || self.workspace.last_changed_files.is_empty() {
return;
}

let wants_tests = self
.last_task_contract
.task.last_task_contract
.as_ref()
.is_some_and(|c| c.wants_tests);
let facts = extract_coding_facts(&CodingFactInput {
changed_files: &self.last_changed_files,
verify_executions: &self.last_turn_telemetry.verification_executions,
changed_files: &self.workspace.last_changed_files,
verify_executions: &self.report.last_turn_telemetry.verification_executions,
wants_tests,
workspace_root: self.runtime.root(),
});
Expand All @@ -33,7 +33,7 @@ impl crate::Agent {
}

// Room under the session cap.
let budget = MAX_CODING_FACTS_PER_SESSION.saturating_sub(self.coding_facts_written) as usize;
let budget = MAX_CODING_FACTS_PER_SESSION.saturating_sub(self.subagents.coding_facts_written) as usize;
let facts: Vec<_> = facts.into_iter().take(budget).collect();
if facts.is_empty() {
return;
Expand All @@ -56,8 +56,8 @@ impl crate::Agent {
}
}
self.decisions = next;
self.coding_facts_written = self
.coding_facts_written
self.subagents.coding_facts_written = self
.subagents.coding_facts_written
.saturating_add(facts.len() as u32);
self.refresh_system_message();

Expand Down Expand Up @@ -85,7 +85,7 @@ impl crate::Agent {
}
// Phase P: re-rank live memory so the next model call sees new bullets
// without waiting for process restart / next session load.
let task = self.last_task_prompt.clone().unwrap_or_default();
let task = self.task.last_task_prompt.clone().unwrap_or_default();
self.refresh_memory_context(&task);
self.refresh_system_message();
let _ = recorded;
Expand Down
8 changes: 4 additions & 4 deletions crates/hi-agent/src/agent/compaction_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ impl crate::Agent {
as too large. Continue from this latest user request; ask for missing details if the \
omitted context is required.]\n\n{input}"
));
self.context_used = 0;
self.report.context_used = 0;
ui.status(
"provider rejected the request as too large; dropped prior conversation context and retrying",
);
Expand Down Expand Up @@ -123,7 +123,7 @@ impl crate::Agent {
);
if freed > 0 {
self.runtime.invalidate_context_after_compaction();
self.context_used = 0;
self.report.context_used = 0;
ui.status(&format!(
"elided ~{}k chars of old tool output before request to fit context",
freed / 1000
Expand Down Expand Up @@ -173,7 +173,7 @@ impl crate::Agent {
the model context window. Continue from this latest user request; ask for missing \
details if the omitted context is required.]\n\n{input}"
));
self.context_used = 0;
self.report.context_used = 0;
dropped_prior_context = true;
ui.status(
"request would exceed the model context window; dropped prior conversation context and retrying",
Expand Down Expand Up @@ -410,7 +410,7 @@ impl crate::Agent {
}

self.runtime.invalidate_context_after_compaction();
self.context_used = 0;
self.report.context_used = 0;
}

/// Run the summarization model call over `slice`, returning the summary text
Expand Down
4 changes: 2 additions & 2 deletions crates/hi-agent/src/agent/curate_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl crate::Agent {
/// re-checks the cap defensively. Like [`update_memory`](Self::update_memory)
/// it builds a throwaway message vec and does NOT record into session history.
pub(crate) async fn curate_turn_end(&mut self, turn_start: usize, ui: &mut dyn Ui) {
if self.auto_skills_written >= MAX_AUTO_SKILLS_PER_SESSION {
if self.subagents.auto_skills_written >= MAX_AUTO_SKILLS_PER_SESSION {
return;
}
// Just this turn's trajectory (user prompt → tool calls → results), with
Expand Down Expand Up @@ -122,7 +122,7 @@ impl crate::Agent {
&skill.body,
) {
Ok(Some(path)) => {
self.auto_skills_written += 1;
self.subagents.auto_skills_written += 1;
ui.status(&format!("✓ curated skill → {}", path.display()));
}
Ok(None) => {} // a skill by this name already exists
Expand Down
8 changes: 4 additions & 4 deletions crates/hi-agent/src/agent/delegate_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl crate::Agent {
false,
);
}
if self.delegate_subagents_used >= MAX_DELEGATE_SUBAGENTS_PER_SESSION {
if self.subagents.delegate_subagents_used >= MAX_DELEGATE_SUBAGENTS_PER_SESSION {
return delegate_tool_outcome(
format!(
"delegate budget exhausted ({MAX_DELEGATE_SUBAGENTS_PER_SESSION} this session); \
Expand All @@ -70,7 +70,7 @@ impl crate::Agent {
false,
);
}
let Some(runner) = self.delegate_runner.clone() else {
let Some(runner) = self.subagents.delegate_runner.clone() else {
return delegate_tool_outcome(
"delegate unavailable: no subagent runner is attached in this context; \
implement it directly instead.",
Expand All @@ -86,8 +86,8 @@ impl crate::Agent {
.map(str::to_string)
.filter(|s| !s.trim().is_empty());

self.delegate_subagents_used += 1;
let n = self.delegate_subagents_used;
self.subagents.delegate_subagents_used += 1;
let n = self.subagents.delegate_subagents_used;
let summary: String = task.chars().take(72).collect();
let ellipsis = if task.chars().count() > 72 { "…" } else { "" };
ui.subagent_note(&format!(
Expand Down
22 changes: 15 additions & 7 deletions crates/hi-agent/src/agent/explore_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl crate::Agent {
hi_tools::ToolStatus::Failed,
);
}
if self.explore_subagents_used >= MAX_EXPLORE_SUBAGENTS_PER_SESSION {
if self.subagents.explore_subagents_used >= MAX_EXPLORE_SUBAGENTS_PER_SESSION {
return explore_tool_outcome(
format!(
"explore budget exhausted ({MAX_EXPLORE_SUBAGENTS_PER_SESSION} subagents this \
Expand All @@ -66,8 +66,8 @@ impl crate::Agent {
hi_tools::ToolStatus::Denied,
);
}
self.explore_subagents_used += 1;
let n = self.explore_subagents_used;
self.subagents.explore_subagents_used += 1;
let n = self.subagents.explore_subagents_used;
// Prominent callout so the user clearly sees a nested agent run start.
let summary: String = task.chars().take(72).collect();
let ellipsis = if task.chars().count() > 72 { "…" } else { "" };
Expand Down Expand Up @@ -165,12 +165,20 @@ impl crate::Agent {
});
explore_tool_outcome(answer, status)
}
Err(err) => explore_tool_outcome(
format!("explore subagent error: {err}"),
hi_tools::ToolStatus::Failed,
),
Err(err) => {
// Mirror parent failed-turn cleanup: kill child backgrounds and
// type an infrastructure outcome so nested escapes don't leak.
let _ = child.finalize_failed_turn();
explore_tool_outcome(
format!("explore subagent error: {err}"),
hi_tools::ToolStatus::Failed,
)
}
}
};
// Always tear down child-owned backgrounds (read-only explore should be
// quiet, but bash-in-readonly denial paths still share the kill API).
child.kill_background_processes();
// Fold the child's token usage into the parent's session totals.
self.add_side_usage(*child.totals());
ui.subagent_note(&format!("↳ explore subagent {n} done"));
Expand Down
34 changes: 17 additions & 17 deletions crates/hi-agent/src/agent/goal_turn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ impl crate::Agent {
// Phase C: same obligation as the interactive settle path — a done-claim
// via update_plan or heuristic advance is not enough without a green seal
// and a non-stalled turn. Skeptic (below) is an extra gate on top.
let verified_clean = matches!(self.last_verify, Some(true));
let verified_clean = matches!(self.report.last_verify, Some(true));
let mut clean_success =
verified_clean && !stalled_unfinished && !stalled_repeating && !hit_step_cap;

Expand All @@ -103,7 +103,7 @@ impl crate::Agent {
// one-line code fix over the trivial-diff exemption — coding-memory
// and skill curation write those after verify by design.
let changed_bytes: u64 = self
.last_file_changes
.workspace.last_file_changes
.iter()
.filter(|change| !crate::verify::is_prose_only_path(&change.path))
.map(|change| match (change.before_len, change.after_len) {
Expand Down Expand Up @@ -149,7 +149,7 @@ impl crate::Agent {
ui.status(&format!("🔍 skeptic objected — retrying: {first}"));
self.refresh_system_message();
self.persist_goal(ui);
self.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Objected);
self.report.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Objected);
return false;
}
SkepticVerdict::Escalate(items) => {
Expand All @@ -171,7 +171,7 @@ impl crate::Agent {
ui.status(&format!(
"🛑 skeptic escalated — step needs your judgment, skipping past it: {first}"
));
self.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Escalated);
self.report.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Escalated);
self.refresh_system_message();
self.persist_goal(ui);
return false;
Expand All @@ -180,19 +180,19 @@ impl crate::Agent {
if let Some(goal) = self.goals.structured.as_mut() {
goal.last_skeptic_status = Some(SkepticStatus::Approved);
}
self.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Approved);
self.report.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Approved);
ui.status("🔍 skeptic approved — advancing");
}
SkepticVerdict::Unavailable(reason) => {
if let Some(goal) = self.goals.structured.as_mut() {
goal.skeptic_unavailable = goal.skeptic_unavailable.saturating_add(1);
goal.last_skeptic_status = Some(SkepticStatus::Unavailable);
}
self.last_turn_telemetry.skeptic_unavailable_count = self
.last_turn_telemetry
self.report.last_turn_telemetry.skeptic_unavailable_count = self
.report.last_turn_telemetry
.skeptic_unavailable_count
.saturating_add(1);
self.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Unavailable);
self.report.last_turn_telemetry.skeptic_last_status = Some(SkepticStatus::Unavailable);
ui.status(&format!(
"⚠ skeptic unavailable — advancing without review: {reason}"
));
Expand Down Expand Up @@ -223,11 +223,11 @@ impl crate::Agent {
let current_pass = verified_at.is_some_and(|(verified_revision, verified)| {
*verified_revision == revision && verified == &digest
});
self.last_changed_files =
self.workspace.last_changed_files =
changes.iter().map(|change| change.path.clone()).collect();
self.last_file_changes = changes;
self.workspace.last_file_changes = changes;
if !current_pass {
self.last_verify = None;
self.report.last_verify = None;
clean_success = false;
verification_invalidated = true;
self.goals.structured = goal_before.clone();
Expand All @@ -238,7 +238,7 @@ impl crate::Agent {
}
}
Err(error) => {
self.last_verify = None;
self.report.last_verify = None;
clean_success = false;
verification_invalidated = true;
self.goals.structured = goal_before.clone();
Expand Down Expand Up @@ -273,11 +273,11 @@ impl crate::Agent {
// A clean read-only turn (investigation, Q&A — no edits, no verify,
// no stall) is neutral: neither advance nor record failure. The sub-goal
// stays active for the next turn, which should do the actual work.
let no_edit_neutral = self.last_verify.is_none()
let no_edit_neutral = self.report.last_verify.is_none()
&& !stalled_unfinished
&& !stalled_repeating
&& !hit_step_cap
&& self.last_changed_files.is_empty();
&& self.workspace.last_changed_files.is_empty();
if no_edit_neutral {
return verification_invalidated;
}
Expand Down Expand Up @@ -337,7 +337,7 @@ impl crate::Agent {
// the counter also changes goal state, which resets the frontend
// drive-stall counter so a long milestone isn't parked mid-build.
if hit_step_cap {
let made_progress = !self.last_changed_files.is_empty();
let made_progress = !self.workspace.last_changed_files.is_empty();
// A milestone that keeps hitting the step cap *while making progress*
// is too big for one turn: decompose it into turn-sized sub-steps
// rather than grind it out over dozens of turns. Snapshot the decision
Expand Down Expand Up @@ -416,13 +416,13 @@ impl crate::Agent {
// the budget is exhausted, the sub-goal (and goal) is marked Failed.
let reason = if hit_step_cap {
"hit the per-turn step cap"
} else if self.last_verify == Some(false) {
} else if self.report.last_verify == Some(false) {
"verification failed and the turn ended without fixing it"
} else if stalled_repeating {
"stalled repeating the same tool call"
} else if stalled_unfinished {
"ended without completing the requested work"
} else if self.last_verify.is_none() && !self.last_changed_files.is_empty() {
} else if self.report.last_verify.is_none() && !self.workspace.last_changed_files.is_empty() {
"ended with unverified workspace changes"
} else {
"verification failed and the turn ended without fixing it"
Expand Down
Loading
Loading