mod attempt_capture;
mod budget;
pub mod digest_fold;
pub mod run_allowance;
pub mod anthropic_wire;
mod capability_check;
mod claim_check;
mod final_reply;
pub(crate) mod observed_report;
use final_reply::FinalReply;
pub use observed_report::assistant_prose as model_reply_for_history;
mod selected_target;
mod semantic_pins;
pub use claim_check::{
files_changed_between, is_workspace_repo_root, locate_workspace_repo, nested_current_paths,
nested_files_changed_between, snapshot_nested_repos, snapshot_workspace,
snapshot_workspace_subtree, NestedRepoSnapshot, StatusSnapshot, WorkspaceRepoLocation,
};
#[cfg(test)]
pub(crate) use observed_report::tests::model_explanation;
pub(crate) mod compress;
mod context_recovery;
mod crew_attest;
mod crew_tool;
pub(crate) mod cw_overflow;
mod display;
mod generation_bounds;
mod generation_policy;
mod generation_repetition;
mod output_budget;
mod output_limit_recovery;
pub use generation_policy::{
preview_chat_generation, projected_cognition, validate_output_allowance, ChatGenerationPreview,
};
mod git_tool;
pub mod openai_sse;
pub(crate) mod publish_early;
pub(crate) mod self_verify;
pub use publish_early::GUIDANCE as REFACTOR_PUBLISH_GUIDANCE;
pub(crate) mod scratchpad;
pub(crate) mod semantic;
pub(crate) mod experiential;
pub(crate) mod scheduled;
pub(crate) mod content_spill;
pub(crate) mod plan_exec;
pub(crate) mod observability;
pub(crate) mod tool_recovery;
mod driver;
mod file_change_display;
pub use file_change_display::{file_change_style, write_file_change_row, FileChangePresentation};
#[cfg(feature = "markdown-syntect")]
mod syntax_foreground;
#[cfg(feature = "markdown")]
mod markdown;
#[cfg(not(feature = "markdown"))]
mod markdown {
use std::io::{self, Write};
#[derive(Debug, Clone, Copy)]
pub struct RenderOpts {
pub color: bool,
pub cols: usize,
}
pub fn render_markdown(src: &str, _opts: RenderOpts) -> String {
src.to_string()
}
pub struct MarkdownStreamWriter<W: Write> {
out: W,
wrote: bool,
ended_nl: bool,
}
impl<W: Write> MarkdownStreamWriter<W> {
pub fn new(out: W, _opts: RenderOpts) -> Self {
Self {
out,
wrote: false,
ended_nl: true,
}
}
pub fn push(&mut self, delta: &str) -> io::Result<()> {
if let Some(&last) = delta.as_bytes().last() {
self.wrote = true;
self.ended_nl = last == b'\n';
}
self.out.write_all(delta.as_bytes())
}
pub fn finish(&mut self) -> io::Result<()> {
if self.wrote && !self.ended_nl {
self.out.write_all(b"\n")?;
}
self.out.flush()
}
pub fn into_inner(self) -> W {
self.out
}
}
}
mod adjudicate;
mod artifact_hooks;
mod artifact_read;
mod disposition_voice;
mod mcp;
mod memory_fetch;
mod note_sink;
mod observation;
mod operating_mode;
mod permissions;
mod plan_mode;
pub mod smart_harness;
mod prompt_intake;
mod prompt_read;
mod recall;
mod tool_args;
mod steering;
mod report;
mod resume;
mod responses_compaction;
mod responses_wire_validation;
mod send_budget;
mod deny;
mod routing;
mod tool_search;
pub(crate) mod tools;
mod transcript;
mod trim;
mod untrusted;
mod warmup;
pub use adjudicate::{adjudicate_decisions, AdjudicationFailure};
pub use artifact_hooks::{
record_manual_compaction_checkpoint, record_memory_compaction_checkpoint,
record_observed_head_transition, record_prompt_comprehension_manifest,
record_retry_revert_file, record_turn_outcome,
};
pub use artifact_read::{
artifact_read_tool_definition, ArtifactPage, ArtifactReadContext, ArtifactReadRecord,
ArtifactSource, PromptArtifactSink, SessionArtifactStore, StoreArtifactStore,
};
pub use compress::{
compress_user_initiated, compress_user_initiated_for_task, CompressCounters, CompressState,
FloorTrend, ManualCompressOutcome, ManualCompressPolicy, SummarizeFn, SummarizeFuture,
Summarizer, CONTINUATION_PREFIX, PLAN_APPROVAL_PREFIX, SUMMARY_END_MARKER, SUMMARY_PREFIX,
};
pub use content_spill::{
SessionSpillStore, SpillCid, SpillCidError, SpillProvenance, SpillRecordV1, SpillScope,
SpillStore,
};
pub use crew_attest::{crew_authz, crew_step_up_policy, CrewAuthz, Presence};
pub use crew_tool::{compose_roster_tool_definition, crew_tool_definition, CrewRunner};
pub use cw_overflow::{parse_context_window_error, recover_context_window_400};
pub use display::{
fmt_token_gauge, fmt_tokens_compact, gauge_level, interactive_recovery, newt_line,
print_harness_notice, print_list_item, print_newt, reply_cols, set_mouse_recovery,
set_spill_lines, set_spill_summary, set_time_marker_secs, Fold, GaugeLevel, Hidden, Recovery,
ThinkingFold, NEWT_ORANGE_CT, REPLY_MARKER,
};
pub use driver::{
HeadlessCodeSearch, InstantiatedFeatures, TurnDriver, TurnDriverConfig, TurnDriverError,
TurnOutcome, TurnStatus,
};
pub use experiential::{
experience_block, ExperienceStore, SessionExperienceStore, EXPERIENCE_TOP_K,
};
pub use git_tool::{check_git_read_scope, git_tool_definition, GitTool};
pub use markdown::{render_markdown, MarkdownStreamWriter, RenderOpts};
pub use mcp::{
classify_mcp_effect, leash_mcp_call, LeasedMcpCall, LeashDenied, McpEffect, McpGrant, McpTools,
NoMcp,
};
pub use observability::{
classify_reqwest, error_class, round_parse_signal, BehaviorSignal, DispatchError, Enforcement,
ErrorClass, OutputAllowance, ParseSignal, SolveObservation, ToolCallDialect,
};
pub use plan_exec::{run_plan, run_plan_with_reground, NoReground, PlanRun, Reground};
pub use prompt_intake::{
discussion_request, AdjudicationCandidate, AdjudicationRefusal, AdjudicationVerdict, AtomicAsk,
ClarificationRejection, DecisionLock, DecisionSource, DecisionStatus, DispositionLexicon,
DispositionSource, PromptComprehensionManifest, PromptDisposition, PromptIntake,
MAX_ADJUDICATION_BATCH,
};
#[cfg(test)]
pub(crate) use prompt_read::response_repository_policy_tokens;
pub use prompt_read::{
prompt_read_tool_definition, PromptReadContext, PromptSource, SessionPromptSource,
SessionPromptStore, StorePromptSource,
};
pub use scheduled::{
fresh_multi_step_plan, plan_block, plan_reseat_pointer, step_change_line, PlanSnapshot,
SessionStepLedger, Step, StepLedger, StepStatus,
};
pub use scratchpad::{
scratchpad_state_block, working_memory_head, ScratchpadStore, SessionScratchpadStore,
};
pub use self_verify::{
enabled as self_verify_enabled, outcomes_enabled as verify_outcomes_requested,
verification_gate_present, verification_receipt,
};
pub use semantic::{
chunk_source, code_search_tool_definition, cosine, format_index_status, format_search_hits,
format_search_model, format_search_preview, format_search_rejects, gather_code_files,
gather_with_manifest, index_files, plan_gather, render_code_evidence, retrieve_evidence,
retrieve_evidence_steered, retrieve_ranked, retrieve_ranked_with_cap, CodeChunk, CodeSearch,
Cut, CutClass, Embedder, EmbeddingsClient, EvidenceKind, GatherCaps, GatherManifest,
IndexStatus, RankedHit, RejectReason, RetrievalResult, RetrievalSteer, SemanticIndex,
SessionSemanticIndex,
};
#[cfg(feature = "markdown-table-formatter")]
pub fn tidy_markdown_tables(src: &str) -> String {
markdown_table_formatter::format_tables(src)
}
#[cfg(not(feature = "markdown-table-formatter"))]
pub fn tidy_markdown_tables(src: &str) -> String {
src.to_string()
}
#[cfg(test)]
#[path = "mod_tests/tidy_tables_tests.rs"]
mod tidy_tables_tests;
pub use budget::get_context_remaining_tool_definition;
pub(crate) use disposition_voice::DispositionVoices;
pub use memory_fetch::{
memory_fetch_tool_definition, MemAddr, MemPayload, MemorySource, StoreMemorySource,
};
pub use note_sink::{save_note_tool_definition, NoteNudge, NoteSink};
pub use observation::{ShellObservation, SHELL_OBSERVATION_PREFIX};
pub use operating_mode::{select_operating_mode_tool_definition, OperatingModeControl};
pub use permissions::{
append_denial, load_denials, widen_caveats, ChoiceSpec, DenialKind, HumanQuestionOutcome,
PermissionAction, PermissionDecision, PermissionGate, PermissionRecord, PermissionRequest,
PersistentDenial, BOUND_REASON_PREFIX,
};
pub use plan_mode::{
plan_verdict, PlanDraft, PlanDraftSink, PlanEntry, PlanModeControl, PlanVerdict, PresentedPlan,
PLAN_APPROVAL_CHOICES,
};
pub use recall::{recall_tool_definition, RecallSource, StoreRecallSource};
pub use resume::resume_context_tool_definition;
pub use send_budget::{initial_context_input_budget, is_truncation_suspect};
pub use steering::{ReplaceRejected, Rev, SessionSteeringInbox, SteeringInbox};
pub use tools::{
execute_tool, execute_tool_with_offload, execute_tool_with_offload_and_prompt_and_artifacts,
exit_plan_mode_result, filter_advertised_tools, filter_tools_for_disposition,
full_access_requested, ocap_disabled, persona_tool_allowed, plan_phase_clamp,
preserve_mcp_resource_url_affinity, run_command_dialect_sentence, set_max_output_tokens,
set_output_cap_chars_per_token, set_output_head_tokens, shell_dialect_sentence, tool_allowed,
tool_definitions, venv_cmd_prefix, ExposureSettings, MCP_RESOURCE_URL_PREFIXES_META_KEY,
};
pub use transcript::{
transcript_lines, transcript_lines_styled, TranscriptLine, TranscriptRole, TranscriptStyle,
};
pub use trim::{ollama_usage, openai_usage, trim_for_summary};
pub use untrusted::{wrap_internal_summary, wrap_untrusted};
pub use warmup::warmup_if_cold;
use crate::retry::{with_backoff_notify_error, RetryPolicy};
use compress::{
compression_trigger, CompressAction, CompressRequest, CompressTrigger,
CompressionTriggerLimits, RefusalReason,
};
use crossterm::{
execute,
style::{Color as CtColor, Print, ResetColor, SetForegroundColor},
};
#[cfg(test)]
use display::turn_heartbeat_line;
use display::{
emit_compression_notice, emit_overflow_notice, print_debug, print_retry_indicator, print_trace,
TurnHeartbeat,
};
use send_budget::estimate_responses_request_tokens;
use send_budget::{
authoritative_request_budget, calibrate_down, calibrate_up, capped_accepted_prompt_tokens,
count_guard_has_headroom, emit_accepted, emit_context_window_400,
estimate_responses_request_real_tokens, exposure_budget_tokens,
full_message_request_pressure_tokens, initial_send_budget, num_ctx_input_ceiling,
preflight_full_message_request, preflight_irreducible_request, preflight_responses_request,
recovered_input_budget, resolve_responses_budget, responses_context_remaining_report,
};
use std::io::{self, Write as _};
use tools::{is_hallucination, merged_tool_definitions};
use trim::{
estimate_request_tokens, estimate_tokens, estimate_value_tokens, merge_round_usage,
protected_prompt_head_len, PromptTracker,
};
fn tui_retry_policy(endpoint: &str) -> RetryPolicy {
if crate::owned_hosts::inference_is_local(false, Some(endpoint)) {
RetryPolicy::for_local_inference()
} else {
RetryPolicy::for_hosted_inference()
}
}
fn inference_progress_label(
model: &str,
attempt: u32,
attempts: u32,
idle_timeout_secs: u64,
) -> String {
format!(
"waiting for {model} · attempt {attempt}/{attempts} · {idle_timeout_secs}s idle timeout…"
)
}
fn ollama_auth_headers(api_key: Option<&str>) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
if let Some(key) = api_key.map(str::trim).filter(|k| !k.is_empty()) {
if let Ok(mut value) = reqwest::header::HeaderValue::from_str(&format!("Bearer {key}")) {
value.set_sensitive(true);
headers.insert(reqwest::header::AUTHORIZATION, value);
}
}
headers
}
#[cfg(test)]
#[path = "mod_tests/first_request_budget_after_switch_tests.rs"]
mod first_request_budget_after_switch_tests;
enum ResponsesCompaction {
HarnessFailure(String),
Compacted,
NotFired,
Refused,
AppendOnlyRefused,
OverBudgetAfterFence(responses_compaction::PostBridgeBudgetExceeded),
BridgeError,
SpillCommitFailed(crate::agentic::content_spill::SpillError),
}
fn refusal_bail_message(reason: Option<RefusalReason>, current: usize, model: &str) -> String {
match reason {
Some(RefusalReason::AppendOnly) => format!(
"context (~{current} tokens) exceeds the model's input budget and the \
append-only context manager never rewrites recorded turns — select \
`/context manager standard` to re-enable compaction, or start a new \
conversation / ask a more focused question"
),
_ => format!(
"context (~{current} tokens) exceeds the model's input budget and \
auto-compression is disabled after repeated ineffective passes — \
start a new conversation or ask a more focused question, or run \
`newt tunings reset {model}` if this model's learned budget looks wrong"
),
}
}
#[derive(Debug, Clone)]
enum CompactionRejection {
HarnessFailure(String),
BridgeClassification,
CompressorRefused,
AppendOnlyRefused,
NoProgress,
PostBridgeBudgetExceeded(responses_compaction::PostBridgeBudgetExceeded),
SpillCommit(crate::agentic::content_spill::SpillError),
}
impl std::fmt::Display for CompactionRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::HarnessFailure(error) => write!(f, "smart harness projection: {error}"),
Self::BridgeClassification => write!(
f,
"proactive compaction could not classify the request (a forbidden system item)"
),
Self::CompressorRefused => write!(
f,
"proactive compaction refused: the protected head alone exceeds the input budget"
),
Self::AppendOnlyRefused => write!(
f,
"proactive compaction refused: the append-only context manager never rewrites \
recorded turns — select `/context manager standard` to re-enable compaction"
),
Self::NoProgress => write!(
f,
"proactive compaction made no progress: nothing left to reclaim under the budget"
),
Self::PostBridgeBudgetExceeded(e) => {
write!(f, "proactive compaction fit the compressor budget but {e}")
}
Self::SpillCommit(e) => write!(
f,
"proactive compaction could not commit its staged spill batch \
({e:?}) — refusing to install a request that names an uncommitted spill"
),
}
}
}
impl ResponsesCompaction {
fn rejection(self) -> Option<CompactionRejection> {
match self {
Self::HarnessFailure(error) => Some(CompactionRejection::HarnessFailure(error)),
Self::Compacted => None,
Self::NotFired => Some(CompactionRejection::NoProgress),
Self::Refused => Some(CompactionRejection::CompressorRefused),
Self::AppendOnlyRefused => Some(CompactionRejection::AppendOnlyRefused),
Self::BridgeError => Some(CompactionRejection::BridgeClassification),
Self::OverBudgetAfterFence(e) => Some(CompactionRejection::PostBridgeBudgetExceeded(e)),
Self::SpillCommitFailed(e) => Some(CompactionRejection::SpillCommit(e)),
}
}
}
pub(crate) type CompactionStageBuffer =
std::sync::Mutex<Vec<crate::agentic::content_spill::StagedSpill>>;
#[allow(clippy::too_many_arguments)]
async fn compact_responses_input(
observed_facts: Option<&semantic_pins::Projection>,
worktree_session: Option<&crate::worktree_adoption::WorktreeSession>,
input: &mut Vec<serde_json::Value>,
instructions: Option<&str>,
tools: Option<&[serde_json::Value]>,
actionable_budget: Option<usize>,
compaction_budget: usize,
cal: f32,
estimation: crate::tokens::TokenEstimation,
task: &str,
summary_input_cap_floor_chars: usize,
rewrites_history: bool,
compaction_store: Option<&dyn crate::agentic::content_spill::SpillStore>,
summarizer: Option<&SummarizeFn>,
compress_state: &mut CompressState,
color: bool,
smart_harness: Option<&smart_harness::SmartHarness>,
overflow_recovery: bool,
) -> ResponsesCompaction {
let with_task = |mut candidate: Vec<serde_json::Value>| {
if let Some(hint) =
worktree_session.and_then(crate::worktree_adoption::WorktreeSession::task_hint)
{
candidate.retain(|item| !compress::is_continuation_message(item));
candidate.push(serde_json::json!({"role":"user", "content":
format!("{} {}", compress::CONTINUATION_PREFIX, workflow_guidance(hint))}));
}
candidate
};
if let Some(harness) = smart_harness {
let max_bytes =
match smart_harness::projection_byte_budget(input, compaction_budget, estimation) {
Ok(max_bytes) => max_bytes,
Err(error) => return ResponsesCompaction::HarnessFailure(error.to_string()),
};
let projected = match harness.project(input, max_bytes).await {
Ok(messages) => with_task(messages),
Err(error) => return ResponsesCompaction::HarnessFailure(error.to_string()),
};
let required = estimate_responses_request_real_tokens(
instructions,
&projected,
tools,
estimation,
cal,
);
if let Some(budget) = actionable_budget.filter(|budget| required > *budget) {
return ResponsesCompaction::OverBudgetAfterFence(
responses_compaction::PostBridgeBudgetExceeded {
actionable_budget: budget,
post_bridge_estimate: required,
framing_overhead: 0,
},
);
}
if projected == *input {
return ResponsesCompaction::NotFired;
}
*input = projected;
return ResponsesCompaction::Compacted;
}
let (host_notes, conversation): (Vec<_>, Vec<_>) = input
.iter()
.cloned()
.partition(|item| observed_facts.is_some_and(|facts| facts.owns_note(item)));
let messages = match responses_compaction::responses_input_to_compaction(&conversation) {
Ok(m) => m,
Err(_) => return ResponsesCompaction::BridgeError,
};
let mut chat = host_notes.clone();
if let Some(ins) = instructions {
chat.push(serde_json::json!({ "role": "system", "content": ins }));
}
chat.extend(responses_compaction::compaction_to_chat(&messages));
let mut candidate_state = compress_state.clone();
let stage_buffer: CompactionStageBuffer = std::sync::Mutex::new(Vec::new());
let request = CompressRequest {
messages: &chat,
budget: compaction_budget,
max_messages: None,
replay_protected_tail_len: context_recovery::protected_tool_tail(&chat),
task,
hard_budget: true,
authoritative: true,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: compaction_store.map(|_| &stage_buffer),
};
let compression = if overflow_recovery {
context_recovery::compress(request, summarizer, &mut candidate_state, smart_harness).await
} else {
smart_harness::compress(request, summarizer, &mut candidate_state, smart_harness).await
};
let outcome = match compression {
Ok(outcome) => outcome,
Err(error) => return ResponsesCompaction::HarnessFailure(error.to_string()),
};
if outcome.action == CompressAction::Refused {
return match outcome.refusal {
Some(RefusalReason::AppendOnly) => ResponsesCompaction::AppendOnlyRefused,
_ => ResponsesCompaction::Refused,
};
}
if !outcome.fired {
return ResponsesCompaction::NotFired;
}
let compacted = &outcome.messages;
let protected = usize::from(instructions.is_some()) + host_notes.len();
let rebuilt_chat = &compacted[protected..];
let rebuilt_msgs = responses_compaction::chat_to_compaction(rebuilt_chat);
let with_notes = |mut items: Vec<serde_json::Value>| {
let mut result = host_notes.clone();
result.append(&mut items);
result
};
let rebuilt_input = with_task(with_notes(responses_compaction::compaction_to_responses(
&rebuilt_msgs,
)));
if let Some(budget) = actionable_budget {
let est_tokens = |items: &[serde_json::Value]| {
estimate_responses_request_real_tokens(instructions, items, tools, estimation, cal)
};
let post_bridge = est_tokens(&rebuilt_input);
let pre_bridge = est_tokens(&with_notes(
responses_compaction::rebuild_unfenced_for_estimate(&rebuilt_msgs),
));
if let Err(exceeded) =
responses_compaction::check_post_bridge_budget(budget, pre_bridge, post_bridge)
{
return ResponsesCompaction::OverBudgetAfterFence(exceeded);
}
}
if let Some(store) = compaction_store {
let staged = match stage_buffer.into_inner() {
Ok(s) => s,
Err(_) => {
return ResponsesCompaction::SpillCommitFailed(
content_spill::SpillError::PoisonedStore,
)
}
};
if !staged.is_empty() {
if let Err(e) = store.commit_batch(&staged) {
return ResponsesCompaction::SpillCommitFailed(e);
}
}
}
*input = rebuilt_input;
*compress_state = candidate_state;
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
ResponsesCompaction::Compacted
}
pub type RecoverCw400 = fn(&anyhow::Error, &str, &str) -> Option<u32>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoundObservation {
Accepted {
prompt_tokens: u32,
estimated_tokens: usize,
},
SuspectedOverflow { prompt_tokens: u32 },
ContextWindow400 { context_window: u32 },
ThinkingOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolOutputStream {
Stdout,
Stderr,
}
pub trait LiveToolOutput: Send + Sync {
fn start(&self, generation: u64);
fn write(&self, generation: u64, stream: ToolOutputStream, chunk: &[u8]);
fn finish(&self, generation: u64);
fn abandon(&self, generation: u64);
}
pub trait CompletedSpillRenderer: Send + Sync {
fn retain_completed(&self, _output: &str) -> Option<u64> {
None
}
fn recovery_hint(&self, id: u64) -> String {
format!("/spill open {id}")
}
fn render_completed(&self, output: &str, width: usize, max_height: usize) -> usize;
fn render_file_change(
&self,
_raw_output: &str,
output: &str,
_change: std::sync::Arc<FileChangePresentation>,
width: usize,
max_height: usize,
) -> usize {
self.render_completed(output, width, max_height)
}
fn is_active(&self) -> bool;
fn erase(&self);
fn discard(&self) {}
}
struct CompletedSpillDismissGuard<'a>(&'a Option<std::sync::Arc<dyn CompletedSpillRenderer>>);
impl Drop for CompletedSpillDismissGuard<'_> {
fn drop(&mut self) {
if let Some(renderer) = self.0 {
renderer.discard();
}
}
}
pub struct ChatCtx<'a> {
pub shell_dialect: &'static str,
pub command_budget: crate::RunCommandBudget,
pub smart_harness: Option<&'a smart_harness::SmartHarness>,
pub url: &'a str,
pub model: &'a str,
pub kind: crate::BackendKind,
pub api_key: Option<&'a str>,
pub messages: &'a [crate::MemMessage],
pub task: &'a str,
pub workspace: &'a str,
pub default_command_cwd: Option<&'a std::path::Path>,
pub worktree_session: Option<&'a crate::worktree_adoption::WorktreeSession>,
pub color: bool,
pub markdown: bool,
pub tool_offload: bool,
pub spill_store: Option<&'a dyn crate::agentic::content_spill::SpillStore>,
pub disclosure: Option<&'a crate::ocap::DisclosureFilter>,
pub compaction_store: Option<&'a dyn crate::agentic::content_spill::SpillStore>,
pub scratchpad: bool,
pub scratchpad_store: Option<&'a dyn crate::agentic::scratchpad::ScratchpadStore>,
pub code_search: Option<crate::agentic::semantic::CodeSearch<'a>>,
pub where_is: Option<&'a crate::where_is::WhereIsIndex>,
pub nav: Option<crate::navigator::NavToolCtx<'a>>,
pub exposure: crate::agentic::tools::ExposureSettings,
pub experience_store: Option<&'a dyn crate::agentic::experiential::ExperienceStore>,
pub step_ledger: Option<&'a dyn crate::agentic::scheduled::StepLedger>,
pub caveats: &'a crate::caveats::Caveats,
pub persona_tools: Option<&'a [String]>,
pub cognition: Option<crate::role_profile::Cognition>,
pub chat_completions_capability: crate::model_card::ChatCompletionsCapability,
pub responses_capability: crate::model_card::ResponsesCapability,
pub openai_api: crate::OpenAiApi,
pub output_allowance: Option<u32>,
pub overflow_retry: crate::config::OverflowRetry,
pub attempt_ledger: Option<&'a std::sync::Mutex<crate::attempts::AttemptLedger>>,
pub reasoning_replay_scope: crate::model_card::ReasoningReplayScope,
pub emits_leading_reasoning: bool,
pub max_tool_rounds: usize,
pub workflow_grace_rounds: usize,
pub narration_nudge_cap: usize,
pub action_nudges: bool,
pub prompt_disposition: PromptDisposition,
pub prompt_intake: Option<&'a PromptIntake>,
pub tool_output_lines: usize,
pub debug: bool,
pub trace: bool,
pub num_ctx: Option<u32>,
pub connect_timeout_secs: u64,
pub inference_timeout_secs: u64,
pub mid_loop_trim_threshold: usize,
pub compaction_trigger_policy: crate::CompactionTriggerPolicy,
pub mid_loop_trim_tokens: Option<usize>,
pub max_ok_input: Option<u32>,
pub build_check_cmd: Option<String>,
pub safe_context: Option<u32>,
pub recover_cw_400: Option<RecoverCw400>,
pub note_sink: Option<&'a mut dyn NoteSink>,
pub note_nudge: Option<&'a mut NoteNudge>,
pub recall_source: Option<&'a dyn RecallSource>,
pub memory_source: Option<&'a dyn MemorySource>,
pub summarizer: Option<&'a SummarizeFn>,
pub compress_state: Option<&'a mut CompressState>,
pub tool_events: Option<&'a mut Vec<crate::ToolEvent>>,
pub phantom_reaches: Option<&'a mut Vec<crate::PhantomReach>>,
pub end_reason: Option<&'a mut Option<crate::TurnEndReason>>,
pub round_cap_hit: Option<&'a mut bool>,
pub solve_obs: Option<&'a mut observability::SolveObservation>,
pub permission_gate: Option<&'a mut dyn PermissionGate>,
pub on_round_usage: Option<&'a mut dyn FnMut(RoundObservation)>,
pub estimate_ratio: Option<f32>,
pub estimation: crate::tokens::TokenEstimation,
pub summary_input_cap_floor_chars: usize,
pub input_ceiling_pct: u32,
pub low_budget_pct: usize,
pub exec_floor: Option<&'a crate::caveats::Scope<String>>,
pub write_ledger: Option<&'a std::cell::RefCell<crate::verify_gate::WriteLedger>>,
pub attribution: Option<&'a std::cell::RefCell<crate::attribution::AttributionLedger>>,
pub cancel: Option<&'a std::sync::atomic::AtomicBool>,
pub run_allowance: Option<&'a run_allowance::RunAllowance>,
pub live_tool_output: Option<std::sync::Arc<dyn LiveToolOutput>>,
pub completed_spill_renderer: Option<std::sync::Arc<dyn CompletedSpillRenderer>>,
pub verify_outcomes: bool,
pub git_tool: Option<&'a dyn GitTool>,
pub crew_runner: Option<&'a dyn CrewRunner>,
pub rewrites_history: bool,
pub operating_mode_control: Option<&'a dyn OperatingModeControl>,
pub plan_mode_control: Option<&'a dyn PlanModeControl>,
pub plan_draft_sink: Option<&'a dyn PlanDraftSink>,
pub steering: Option<&'a dyn SteeringInbox>,
}
fn record_organic_note_use(
name: &str,
note_sink: &Option<&mut dyn NoteSink>,
note_nudge: &mut Option<&mut NoteNudge>,
) {
if name == "save_note" && note_sink.is_some() {
if let Some(n) = note_nudge.as_deref_mut() {
n.note_saved();
}
}
}
fn append_clean_build_warning(
messages: &mut Vec<serde_json::Value>,
clean_build: &mut crate::loop_watch::CleanBuildWatch,
args: &serde_json::Value,
harness: Option<&smart_harness::SmartHarness>,
) -> anyhow::Result<()> {
if let Some(warning) = clean_build.observe(
args.get("command")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
) {
let text = format!("{} {warning}", compress::LOOP_GUIDANCE_PREFIX);
if let Some(harness) = harness {
harness.host_message(&text)?;
}
messages.push(serde_json::json!({"role":"user", "content":text}));
}
Ok(())
}
fn record_completed_tool_event(
tool_events: &mut Option<&mut Vec<crate::ToolEvent>>,
name: &str,
args: &serde_json::Value,
ok: bool,
execution: Option<crate::ExecOutcome>,
tool_t0: std::time::Instant,
) {
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent {
execution,
..crate::ToolEvent::from_call(
name,
args,
ok,
u64::try_from(tool_t0.elapsed().as_millis()).ok(),
)
});
}
}
fn record_phantom_reach(
phantom_reaches: &mut Option<&mut Vec<crate::PhantomReach>>,
name: &str,
args: &serde_json::Value,
result: &str,
ok: bool,
advertise_team: bool,
) {
if let Some(pr) = phantom_reaches.as_deref_mut() {
if let Some(resolution) = tools::classify_phantom_reach(name, args, result, ok)
.or_else(|| tools::classify_gated_off_reach(name, advertise_team))
{
pr.push(crate::PhantomReach {
name_as_called: name.to_string(),
resolution,
active_context_features: Vec::new(),
});
}
}
}
fn ledger_note_write(
write_ledger: Option<&std::cell::RefCell<crate::verify_gate::WriteLedger>>,
name: &str,
args: &serde_json::Value,
workspace: &str,
) {
let Some(led) = write_ledger else {
return;
};
if name != "write_file" && name != "edit_file" {
return;
}
if let Some(p) = args.get("path").and_then(|v| v.as_str()) {
let abs = lexical_normalize(&std::path::Path::new(workspace).join(p));
led.borrow_mut().note_before_write(abs);
}
}
fn ledger_note_attribution(
attribution: Option<&std::cell::RefCell<crate::attribution::AttributionLedger>>,
model: &str,
name: &str,
args: &serde_json::Value,
ok: bool,
) {
let Some(ledger) = attribution else {
return;
};
if !ok || is_read_only_call(name, args) {
return;
}
ledger.borrow_mut().record(
model,
crate::build_info::harness_name(),
crate::build_info::PACKAGE_VERSION,
);
}
struct AttributionEpoch<'a> {
attribution: Option<&'a std::cell::RefCell<crate::attribution::AttributionLedger>>,
git_tool: Option<&'a dyn GitTool>,
confirmed_before: usize,
model: &'a str,
name: &'a str,
finished: bool,
}
impl<'a> AttributionEpoch<'a> {
fn new(
attribution: Option<&'a std::cell::RefCell<crate::attribution::AttributionLedger>>,
git_tool: Option<&'a dyn GitTool>,
model: &'a str,
name: &'a str,
) -> Self {
Self {
attribution,
git_tool,
confirmed_before: git_tool.map_or(0, GitTool::confirmed_commit_count),
model,
name,
finished: false,
}
}
fn consume_confirmed(&self) -> bool {
let confirmed = self
.git_tool
.is_some_and(|tool| tool.confirmed_commit_count() > self.confirmed_before);
if confirmed {
if let Some(ledger) = self.attribution {
let mut ledger = ledger.borrow_mut();
ledger.clear();
if self.name != "git" {
ledger.record(
self.model,
crate::build_info::harness_name(),
crate::build_info::PACKAGE_VERSION,
);
}
}
}
confirmed
}
fn finish(mut self, args: &serde_json::Value, ok: bool) {
if !self.consume_confirmed() {
ledger_note_attribution(self.attribution, self.model, self.name, args, ok);
}
self.finished = true;
}
}
impl Drop for AttributionEpoch<'_> {
fn drop(&mut self) {
if !self.finished {
self.consume_confirmed();
}
}
}
fn drain_steering_into(
steering: Option<&dyn SteeringInbox>,
messages: &mut Vec<serde_json::Value>,
color: bool,
) -> usize {
let Some(inbox) = steering else {
return 0;
};
let pending = inbox.drain_for_round();
for text in &pending {
messages.push(serde_json::json!({ "role": "user", "content": text }));
}
if !pending.is_empty() {
print_debug(
&format!("delivered {} operator steering message(s)", pending.len()),
color,
);
}
pending.len()
}
fn lexical_normalize(path: &std::path::Path) -> std::path::PathBuf {
use std::path::Component;
let mut out = std::path::PathBuf::new();
for comp in path.components() {
match comp {
Component::ParentDir => {
if matches!(out.components().next_back(), Some(Component::Normal(_))) {
out.pop();
} else {
out.push("..");
}
}
Component::CurDir => {}
other => out.push(other.as_os_str()),
}
}
out
}
#[cfg(test)]
#[path = "mod_tests/retry_ledger_tests.rs"]
mod retry_ledger_tests;
fn is_tools_unsupported_error(e: &anyhow::Error) -> bool {
let s = e.to_string().to_lowercase();
s.contains("does not support tools") || s.contains("not support tools")
}
fn is_ollama_tool_xml_error(e: &anyhow::Error) -> bool {
let s = e.to_string().to_lowercase();
s.contains("ollama") && s.contains("xml syntax error")
}
fn ollama_tool_xml_retry_nudge() -> &'static str {
"The previous assistant turn failed inside Ollama's XML tool-call parser. \
Keep using tools, but emit exactly one valid native tool call with well-formed \
arguments now. Do not answer in prose or wrap the call in explanatory XML."
}
fn is_cancelled(cancel: Option<&std::sync::atomic::AtomicBool>) -> bool {
cancel.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
}
fn dismiss_completed_spill(renderer: &Option<std::sync::Arc<dyn CompletedSpillRenderer>>) {
if let Some(renderer) = renderer {
renderer.erase();
}
}
async fn cancelled(cancel: Option<&std::sync::atomic::AtomicBool>) {
match cancel {
None => std::future::pending().await,
Some(flag) => {
while !flag.load(std::sync::atomic::Ordering::Relaxed) {
tokio::time::sleep(std::time::Duration::from_millis(15)).await;
}
}
}
}
async fn cancellable<F: std::future::Future>(
cancel: Option<&std::sync::atomic::AtomicBool>,
fut: F,
) -> Option<F::Output> {
tokio::select! {
biased;
_ = cancelled(cancel) => None,
v = fut => Some(v),
}
}
fn present_synthetic_tool_result<W: std::io::Write>(
display: &mut display::ToolDisplay<W>,
name: &str,
args: &serde_json::Value,
workspace: &std::path::Path,
result: &str,
) {
let (name, detail) =
tools::tool_presentation(name, args, workspace, &crate::caveats::Scope::All);
display.call(&name, &detail);
display.result(result);
}
fn print_synthetic_tool_result(
name: &str,
args: &serde_json::Value,
workspace: &str,
result: &str,
color: bool,
completed_spill_renderer: &Option<std::sync::Arc<dyn CompletedSpillRenderer>>,
) {
dismiss_completed_spill(completed_spill_renderer);
let mut tool_display = display::ToolDisplay::new(
io::stdout(),
color,
display::term_cols(),
display::spill_lines(),
display::spill_summary(),
);
present_synthetic_tool_result(
&mut tool_display,
name,
args,
std::path::Path::new(workspace),
result,
);
}
fn legacy_caps(animate: bool) -> crate::tty::LineCaps {
if animate {
crate::tty::LineCaps::Own
} else {
crate::tty::LineCaps::None
}
}
fn announce_tool_activity(name: &str) {
if crate::lifecycle::observed() {
crate::lifecycle::emit(crate::lifecycle::LifecycleEvent::ToolActivity {
tool: name.to_string(),
});
}
}
macro_rules! no_progress_gate {
(
$runtime:expr, $steer_allowed:expr, $messages:expr, $end_reason:expr,
$solve_obs:expr, $usage:expr, $halluc:expr, $smart:expr, ($workspace:expr, $read:expr, $evidence:expr, $disclosure:expr, $claims:expr, $verification:expr), |$h:ident| $record:expr
) => {
match $runtime.no_progress_verdict($steer_allowed) {
NoProgress::Continue => {}
NoProgress::Steer(text) => {
$messages.push(serde_json::json!({ "role": "user", "content": text }));
}
NoProgress::Stop => {
let notice = finalize_final_text(FinalReply::harness($runtime.no_progress_notice()), $workspace, $read, $evidence, $disclosure, $claims, $verification);
if let Some($h) = $smart {
$record;
$h.outcome(crate::TurnEndReason::NoProgress, ¬ice)?;
}
if let Some(slot) = &mut $end_reason {
**slot = Some(crate::TurnEndReason::NoProgress);
}
observability::observe_harness_reply(&mut $solve_obs);
return Ok((notice, false, $usage, $halluc));
}
}
};
}
pub async fn chat_complete(
ctx: ChatCtx<'_>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
chat_complete_with_prompt(ctx, None, None, mcp).await
}
pub async fn chat_complete_with_prompt(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
chat_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
None,
None,
mcp,
)
.await
}
pub async fn chat_complete_with_prompt_and_artifacts(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
artifact_source: Option<&dyn artifact_read::ArtifactSource>,
artifact_sink: Option<&dyn artifact_read::PromptArtifactSink>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
validate_output_allowance(ctx.output_allowance, ctx.num_ctx)?;
if ctx.kind == crate::BackendKind::Anthropic {
return anthropic_chat_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
artifact_source,
artifact_sink,
mcp,
)
.await;
}
if ctx.kind == crate::BackendKind::Openai {
if ctx.openai_api == crate::OpenAiApi::Responses {
return openai_responses_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
artifact_source,
artifact_sink,
mcp,
)
.await;
}
return openai_chat_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
artifact_source,
artifact_sink,
mcp,
)
.await;
}
let ChatCtx {
shell_dialect,
command_budget,
verify_outcomes,
smart_harness,
url,
model,
kind: _,
api_key,
messages: mem_messages,
task,
workspace,
default_command_cwd,
worktree_session,
color,
markdown: _,
tool_offload,
spill_store,
disclosure,
compaction_store,
scratchpad,
scratchpad_store,
code_search,
where_is,
nav,
exposure,
experience_store,
step_ledger,
caveats,
persona_tools,
cognition: _,
chat_completions_capability: _,
responses_capability: _,
openai_api: _,
output_allowance,
overflow_retry: _,
attempt_ledger,
reasoning_replay_scope: _,
max_tool_rounds,
workflow_grace_rounds,
narration_nudge_cap,
action_nudges,
prompt_disposition,
prompt_intake,
tool_output_lines,
debug,
trace,
num_ctx,
connect_timeout_secs,
inference_timeout_secs,
mid_loop_trim_threshold,
compaction_trigger_policy,
mid_loop_trim_tokens,
max_ok_input,
build_check_cmd,
safe_context,
recover_cw_400,
mut note_sink,
mut note_nudge,
recall_source,
memory_source,
summarizer,
compress_state,
mut tool_events,
mut phantom_reaches,
mut end_reason,
mut round_cap_hit,
mut solve_obs,
mut permission_gate,
mut on_round_usage,
estimate_ratio,
estimation,
summary_input_cap_floor_chars,
rewrites_history,
input_ceiling_pct,
low_budget_pct,
exec_floor,
write_ledger,
attribution,
cancel,
run_allowance,
live_tool_output,
completed_spill_renderer,
git_tool,
crew_runner,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
steering,
emits_leading_reasoning,
} = ctx;
let _completed_spill_guard = CompletedSpillDismissGuard(&completed_spill_renderer);
let action_nudges = action_nudges && prompt_disposition == PromptDisposition::Act;
if let Some(harness) = smart_harness {
harness.start_turn()?;
}
let spill_store = smart_harness.map(|h| h.spill_store()).or(spill_store);
let tool_offload = tool_offload || smart_harness.is_some();
let smart_verify = action_nudges && prompt_disposition == PromptDisposition::Act;
let action_nudges = action_nudges && smart_harness.is_none();
let max_tool_rounds = prompt_disposition.tool_round_limit(max_tool_rounds);
let mut local_compress_state = CompressState::new();
let compress_state = match compress_state {
Some(s) => s,
None => &mut local_compress_state,
};
let auth = ollama_auth_headers(api_key);
let client = reqwest::Client::builder()
.default_headers(auth)
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let chat_url = format!("{}/api/chat", url.trim_end_matches('/'));
let retry = tui_retry_policy(url);
let advertise_save_note = note_sink.is_some();
let advertise_recall = recall_source.is_some();
let advertise_memory_fetch = memory_source.is_some();
let advertise_scratchpad = scratchpad_store.is_some() && scratchpad;
let advertise_code_search = code_search.is_some();
let advertise_experiential = experience_store.is_some();
let advertise_scheduled = step_ledger.is_some();
let advertise_git = false;
let advertise_team = crew_runner.is_some();
let advertise_operating_mode = operating_mode_control.is_some();
let advertise_plan_mode = plan_mode_control.is_some();
let advertise_plan_mode_active =
plan_mode_control.is_some_and(|control| control.is_plan_mode());
let mem_messages = observed_report::replay_messages(mem_messages);
let mut messages: Vec<serde_json::Value> = mem_messages
.iter()
.map(|m| serde_json::json!({"role": m.role.as_str(), "content": m.content}))
.collect();
if let Some(harness) = smart_harness {
messages = harness.initial_messages(messages)?;
}
if let (Some(session), Some(context)) = (worktree_session, turn_prompt_context) {
session
.report
.lock()
.expect("report state")
.begin(context.active().root_prompt_id());
}
let ephemeral_prompt = prompt_read::headless_prompt_fallback(turn_prompt_context, task);
let turn_prompt_context = turn_prompt_context.or(ephemeral_prompt.as_ref());
let prompt_context =
prompt_read::PromptReadContext::new(turn_prompt_context, task, prompt_source);
let artifact_context = turn_prompt_context
.map(|turn| artifact_read::ArtifactReadContext::from_turn(turn, artifact_source));
let active_task = prompt_context.active_text();
prompt_read::ensure_active_prompt_card(&mut messages, prompt_context, prompt_intake);
let attempt_turn = turn_prompt_context
.map(|turn| turn.active_operator_prompt().id().to_string())
.unwrap_or_default();
let fallback_ledger = std::sync::Mutex::new(crate::attempts::AttemptLedger::default());
let attempt_ledger = attempt_ledger.or_else(|| run_allowance.map(|_| &fallback_ledger));
let attempts = attempt_ledger.map(|ledger| attempt_capture::AttemptScope {
ledger,
turn: &attempt_turn,
model,
backend: url,
cancel,
run_allowance,
});
if note_sink.is_some() {
if let Some(line) = note_nudge.as_deref_mut().and_then(NoteNudge::begin_turn) {
if let Some(harness) = smart_harness {
harness.host_message(&line)?;
messages.push(serde_json::json!({"role":"user", "content":line}));
} else {
append_nudge_line(&mut messages, &line);
}
}
}
let mut accumulated_usage: Option<crate::TokenUsage> = None;
let mut hallucination_count: u32 = 0;
let result_aware = self_verify::enabled() && verify_outcomes;
let mut repeat_calls = RepeatCallGuard::default();
let plan_at_turn_start = step_ledger
.map(|ledger| ledger.snapshot())
.unwrap_or_default();
let mut read_history = tools::ReadHistory::default();
let mut verification = self_verify::VerificationLedger::for_turn(task, result_aware);
let turn_claims =
claim_check::TurnClaims::capture(workspace, &caveats.fs_read, worktree_session)
.with_evidence(observed_report::review::Evidence {
sink: artifact_sink,
context: artifact_context,
spill: spill_store,
});
let mut clean_build = crate::loop_watch::CleanBuildWatch::default();
let mut overflow_retries: u32 = 0;
let mut suspicious_empty_retries: u32 = 0;
let mut cw_retries: u32 = 0;
let mut output_recovery = output_limit_recovery::Recovery::default();
let mut tools_supported = true;
let mut tools_unsupported_notified = false;
let mut effective_input_ceiling =
num_ctx_input_ceiling(num_ctx, input_ceiling_pct, output_allowance);
let ollama_options = ollama_options(num_ctx, output_allowance);
observability::observe_output_allowance(
&mut solve_obs,
output_allowance,
output_allowance.is_some(),
);
let mut send_budget: Option<usize> =
initial_send_budget(max_ok_input, safe_context, effective_input_ceiling);
let mut send_budget_authoritative = safe_context.is_some() || effective_input_ceiling.is_some();
let tools = merged_tool_definitions(
mcp,
advertise_save_note,
advertise_recall,
advertise_memory_fetch,
advertise_git.then_some(&caveats.fs_read),
advertise_team,
advertise_scratchpad,
advertise_code_search,
advertise_experiential,
advertise_scheduled,
advertise_operating_mode,
advertise_plan_mode,
advertise_plan_mode_active,
command_budget,
shell_dialect,
);
let tools = filter_advertised_tools(tools, persona_tools);
let tools = filter_tools_for_disposition(
smart_harness::advertise(tools, smart_harness),
prompt_disposition,
);
let (mut tools, hidden_tools) = crate::agentic::tools::select_exposed(
tools,
&exposure,
exposure_budget_tokens(send_budget, safe_context),
&std::collections::BTreeSet::new(),
estimation,
);
let mut tool_tokens = estimate_value_tokens(&tools, estimation);
let mut cal = compress_state.calibration.ratio(estimate_ratio);
let mut tool_tokens_real = calibrate_up(tool_tokens, cal);
preflight_irreducible_request(
&messages,
Some(&tools),
authoritative_request_budget(send_budget, send_budget_authoritative, mid_loop_trim_tokens),
cal,
estimation,
model,
)?;
let animate = color && thinking_stream_enabled();
let mut prompt_tracker = PromptTracker::new();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let mut read_only_rounds: usize = 0;
let mut narration_nudges: usize = 0;
let mut readonly_completion_retried = false;
let mut pending_plan_nudges: usize = 0;
let mut stale_file_nudges: usize = 0;
let mut unverified_exec_blocker_nudges: usize = 0;
let mut run_command_denial_observed = false;
let mut pending_rerun: Option<tools::PendingRerun> = None;
let mut capability_evidence = capability_check::Evidence::default();
let mut probe_correction_used = false;
let nudge_classifier = crate::NudgeClassifier::load_default();
let action_turn = action_nudges && crate::classifiers::user_turn_invites_action(active_task);
let exec_grounding_turn = action_nudges && prompt_disposition == PromptDisposition::Act;
let workflow_steerer = crate::WorkflowSteerer::load_default();
let mut workflow_runtime = WorkflowRuntimeState {
initiative: crate::initiative::effective_initiative(),
no_progress: crate::initiative::installed_no_progress(),
..Default::default()
};
workflow_runtime.set_progress_horizon(
workflow_steerer.progress_horizon(&workflow_classifier_text(&messages, "")),
);
let mut ollama_xml_retry_nudges: usize = 0;
let mut thinking_only_reported = false;
let mut observed_paths = claim_check::ObservedPaths::default();
let mut observed_resolver = claim_check::workspace_resolver(workspace);
let mut current_tool_round_limit = max_tool_rounds;
let turn_started = std::time::Instant::now();
let mut turn_heartbeat = TurnHeartbeat::default();
let mut selected_source = selected_target::Projection::default();
let mut observed_facts = semantic_pins::Projection::default();
'round_loop: for round in 0..usize::MAX {
let round = round.saturating_sub(output_recovery.skipped_rounds);
selected_source.refresh(
&mut messages,
step_ledger,
workspace,
&caveats.fs_read,
worktree_session,
false,
);
observed_facts.planning = output_budget::guidance(
output_allowance,
step_ledger,
&selected_target::root(workspace, worktree_session),
&caveats.fs_read,
estimation,
);
semantic_pins::refresh(
&mut observed_facts,
smart_harness,
&mut messages,
prompt_context,
selected_source.current(),
&turn_claims,
workspace,
&caveats.fs_read,
)?;
if hidden_tools.append_promoted(&mut tools) {
tool_tokens = estimate_value_tokens(&tools, estimation);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
let turn_elapsed = turn_started.elapsed();
if let Some(line) = turn_heartbeat.notice(turn_elapsed, round, current_tool_round_limit) {
print_newt(&line, color, false);
}
dismiss_completed_spill(&completed_spill_renderer);
if !workflow_runtime.admit_round(
round,
&mut current_tool_round_limit,
workflow_grace_rounds,
) {
break;
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
if drain_steering_into(steering, &mut messages, color) > 0 {
workflow_runtime.note_operator_steering();
}
if round > 0 {
if color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print("…\n"),
ResetColor
)
.ok();
}
}
if round > 0 {
no_progress_gate!(
workflow_runtime,
action_nudges,
messages,
end_reason,
solve_obs,
accumulated_usage,
hallucination_count,
smart_harness,
(
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification
),
|harness| harness.record_messages(&messages)?
);
}
if round > 0 && action_nudges {
if let Some(ptr) = step_ledger.and_then(plan_reseat_pointer) {
messages.push(serde_json::json!({ "role": "user", "content": ptr }));
}
}
let read_only_nudge_after = workflow_runtime.initiative.read_only_nudge_after();
if action_nudges && read_only_rounds >= read_only_nudge_after {
let remaining = (workflow_grace_rounds == 0)
.then_some(current_tool_round_limit.saturating_sub(round + 1));
let delegate_hint = workflow_steerer
.delegate_hint(&workflow_classifier_text(&messages, ""), advertise_team);
messages.push(serde_json::json!({
"role": "user",
"content": read_only_action_nudge(
read_only_rounds,
remaining,
step_ledger,
delegate_hint.as_deref(),
)
}));
read_only_rounds = 0;
}
{
let advertised_tools = tools_supported.then_some(&tools);
let current = full_message_request_pressure_tokens(
prompt_tracker.current(&messages, advertised_tools, cal, estimation),
&messages,
advertised_tools,
cal,
estimation,
);
let message_tokens = estimate_tokens(&messages, estimation);
let has_authoritative_headroom = count_guard_has_headroom(
current,
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
max_ok_input,
);
if let Some(trigger) = compression_trigger(
messages.len(),
current,
message_tokens,
CompressionTriggerLimits {
count_threshold: mid_loop_trim_threshold,
token_threshold: mid_loop_trim_tokens,
send_budget,
tool_tokens: tool_tokens_real,
policy: compaction_trigger_policy,
has_authoritative_headroom,
},
) {
let pipeline_budget = if trigger.hard_budget {
calibrate_down(trigger.budget, cal)
} else {
trigger.budget
};
let token_fired = mid_loop_trim_tokens.is_some_and(|t| t > 0 && current > t);
let outcome = match crate::tty::with_spinner(
legacy_caps(animate),
"compressing context…",
crate::tty::Sink::Stdout,
color,
cancellable(
cancel,
smart_harness::compress(
CompressRequest {
messages: &messages,
budget: pipeline_budget,
max_messages: trigger.max_messages,
replay_protected_tail_len: 0,
task: active_task,
hard_budget: trigger.hard_budget,
authoritative: token_fired || send_budget_authoritative,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
),
),
)
.await
{
Some(o) => o?,
None => {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
))
}
};
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused {
anyhow::bail!("{}", refusal_bail_message(outcome.refusal, current, model));
}
if outcome.fired {
let suffix = if trigger.hard_budget && outcome.tokens_after > pipeline_budget {
", still over budget"
} else {
""
};
emit_compression_notice(
color,
outcome.tokens_before,
outcome.tokens_after,
outcome.action,
suffix,
);
if debug {
print_debug(
&format!(
"compression: {} → {} messages (budget ~{} tokens, \
+~{tool_tokens} tool-schema tokens ride along)",
messages.len(),
outcome.messages.len(),
pipeline_budget,
),
color,
);
}
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
preflight_full_message_request(
candidate,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
pipeline_budget,
round,
trigger.primary_cause.artifact_reason(),
Some(&trigger),
send_budget_authoritative,
compress_state.floor_trend(),
color,
);
}
}
}
let probe_started = std::time::Instant::now();
let (json, round_est_raw): (serde_json::Value, usize) = loop {
preflight_full_message_request(
&messages,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)?;
let round_est_raw =
estimate_request_tokens(&messages, tools_supported.then_some(&tools), estimation);
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
let mut body_no_stream = serde_json::json!({
"model": model,
"messages": messages,
"stream": false,
"tools": tools.clone(),
});
if let Some(options) = &ollama_options {
body_no_stream["options"] = options.clone();
}
if !tools_supported {
if let Some(o) = body_no_stream.as_object_mut() {
o.remove("tools");
}
}
let dispatch = match crate::tty::with_spinner(
legacy_caps(animate),
"thinking…",
crate::tty::Sink::Stdout,
color,
cancellable(
cancel,
with_backoff_notify_error(
&retry,
|| async {
let (resp, attempt) = attempt_capture::send(
attempts,
"primary",
smart_harness::request(
client.post(&chat_url),
&body_no_stream,
smart_harness,
"ollama",
)?,
"request failed",
)
.await?;
let json = smart_harness::response(resp, smart_harness, "Ollama")
.await
.inspect_err(|error| {
attempt_capture::failed(attempt.as_ref(), error);
})?;
attempt_capture::complete(attempt.as_ref(), ollama_usage(&json));
Ok(json)
},
|attempt, delay, error| {
print_retry_indicator(attempt, retry.max_retries, delay, error, color);
},
),
),
)
.await
{
Some(d) => d,
None => {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
))
}
};
match dispatch {
Ok(j) => {
cw_retries = 0;
break (j, round_est_raw);
}
Err(e) => {
let tools_unsupported = is_tools_unsupported_error(&e);
let malformed_xml_tool_call = is_ollama_tool_xml_error(&e);
if tools_supported && tools_unsupported {
tools_supported = false;
if !tools_unsupported_notified {
tools_unsupported_notified = true;
let notice = format!(
"{model} does not support tools — tools disabled for this turn"
);
print_newt(¬ice, color, false);
}
continue;
}
if tools_supported && malformed_xml_tool_call && ollama_xml_retry_nudges < 2 {
ollama_xml_retry_nudges += 1;
print_newt(
&format!(
"{model} produced malformed Ollama XML tool-call syntax — \
retrying with a stricter tool-call nudge"
),
color,
false,
);
let nudge = ollama_tool_xml_retry_nudge();
if let Some(harness) = smart_harness {
harness.host_message(nudge)?;
}
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue;
}
let overflow = cw_overflow::is_context_overflow(&e.to_string());
let refused_tokens = cw_overflow::parse_context_window_error(&e.to_string())
.map(|(tokens, _)| usize::try_from(tokens).unwrap_or(usize::MAX))
.unwrap_or_else(|| calibrate_up(round_est_raw, cal));
if overflow {
cal = compress_state.calibration.overflow(cal);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
if cw_retries < 2 {
let recovered_window = recover_cw_400.and_then(|f| f(&e, model, &today));
if let Some(recovered_budget) = recovered_window
.map(|context_window| {
recovered_input_budget(
context_window,
input_ceiling_pct,
output_allowance,
effective_input_ceiling,
)
})
.or_else(|| {
cw_overflow::core_recover_overflow(
&e.to_string(),
send_budget,
effective_input_ceiling,
)
.map(|cap| cap as usize)
})
.or_else(|| {
overflow.then_some(
send_budget.unwrap_or_else(|| calibrate_up(round_est_raw, cal)),
)
})
.and_then(|budget| {
cw_overflow::recovery_target(
budget,
refused_tokens,
effective_input_ceiling,
)
})
{
if let Some(context_window) = recovered_window {
emit_context_window_400(&mut on_round_usage, context_window);
}
let new_budget = effective_input_ceiling
.map_or(recovered_budget, |c| recovered_budget.min(c));
let new_budget = authoritative_request_budget(
Some(new_budget),
true,
mid_loop_trim_tokens,
)
.unwrap_or(new_budget);
emit_overflow_notice(
color,
&display::OverflowReason::Refused {
request_estimate: Some(
refused_tokens.min(u32::MAX as usize) as u32
),
window: recovered_window,
trim_to: new_budget.min(u32::MAX as usize) as u32,
},
model,
cw_retries + 1,
);
send_budget = Some(new_budget);
effective_input_ceiling = Some(new_budget);
send_budget_authoritative = true;
let compression = context_recovery::compress(
CompressRequest {
messages: &messages,
budget: context_recovery::target(
&messages,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
estimation,
),
max_messages: None,
replay_protected_tail_len:
context_recovery::protected_tool_tail(&messages),
task: active_task,
hard_budget: true,
authoritative: true,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e.context(format!(
"context re-projection failed: {error}"
)));
}
};
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused
|| !outcome.fired
|| outcome.tokens_after >= outcome.tokens_before
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
if outcome.fired {
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
preflight_full_message_request(
candidate,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
round,
"context_window_400",
None,
false,
compress_state.floor_trend(),
color,
);
}
let projected = estimate_request_tokens(
&messages,
tools_supported.then_some(&tools),
estimation,
);
if projected >= round_est_raw
|| calibrate_up(projected, cal) > new_budget
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
Some(calibrate_up(projected, cal)),
)?;
cw_retries += 1;
continue;
}
}
if overflow {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
}
return Err(e);
}
}
};
let round_usage = ollama_usage(&json);
if let Some(prompt_tokens) = json["prompt_eval_count"]
.as_u64()
.and_then(|n| u32::try_from(n).ok())
{
if !is_truncation_suspect(prompt_tokens, num_ctx) {
compress_state
.calibration
.observe(Some(prompt_tokens), round_est_raw);
cal = compress_state.calibration.ratio(estimate_ratio);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
prompt_tracker.record(prompt_tokens, messages.len());
}
accumulated_usage = merge_round_usage(accumulated_usage, round_usage);
let truncation_suspect =
round_usage.is_some_and(|u| is_truncation_suspect(u.input_tokens, num_ctx));
if let (Some(u), Some(budget), false) = (round_usage, send_budget, truncation_suspect) {
let raised = capped_accepted_prompt_tokens(u.input_tokens, effective_input_ceiling);
if raised > budget {
send_budget = Some(raised);
if debug {
print_debug(
&format!(
"send budget raised to ~{raised} tokens (backend accepted \
{}-token prompt)",
u.input_tokens
),
color,
);
}
}
}
let message = &json["message"];
let (probe_content, inline_reasoning) = crate::reasoning::ThinkFilter::filter_complete(
message["content"].as_str().unwrap_or(""),
emits_leading_reasoning,
);
if json["done_reason"] == "length" {
if output_recovery.retry(
message["tool_calls"]
.as_array()
.is_some_and(|calls| !calls.is_empty()),
&mut messages,
smart_harness,
)? {
continue;
}
let text = finalize_final_text(
generation_bounds::output_limit_notice(probe_content, round_usage),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Failed);
}
if let Some(harness) = smart_harness {
harness.outcome(crate::TurnEndReason::Failed, &text)?;
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let native_calls = message["tool_calls"].as_array();
let recovered = if native_calls.map(|t| t.is_empty()).unwrap_or(true) {
tool_recovery::recover_tool_calls(&probe_content)
} else {
tool_recovery::Recovery::default()
};
let tool_calls: Option<&Vec<serde_json::Value>> = match native_calls {
Some(t) if !t.is_empty() => Some(t),
_ if !recovered.calls.is_empty() => Some(&recovered.calls),
_ => None,
};
let has_tools = tool_calls.map(|tc| !tc.is_empty()).unwrap_or(false);
if let Some(obs) = solve_obs.as_deref_mut() {
if let Some(m) = json["model"].as_str().filter(|m| !m.is_empty()) {
obs.served_model = Some(m.to_string());
}
let native = native_calls.is_some_and(|t| !t.is_empty());
if let Some(sig) = observability::round_parse_signal(
round,
!probe_content.is_empty(),
native,
recovered.dialect,
Vec::new(),
) {
obs.parse_signals.push(sig);
}
}
if debug && !recovered.calls.is_empty() {
print_debug(
&format!(
"recovered {} tool call(s) from content (non-native emission)",
recovered.calls.len()
),
color,
);
}
if debug {
let content_excerpt = if probe_content.is_empty() {
"(empty)".to_string()
} else {
let chars: String = probe_content.chars().take(80).collect();
if probe_content.len() > 80 {
format!("{chars}…")
} else {
chars
}
};
let tc_count = tool_calls.map(|tc| tc.len()).unwrap_or(0);
let usage_str = match round_usage {
Some(u) => format!("{} in / {} out", u.input_tokens, u.output_tokens),
None => "no usage".into(),
};
print_debug(
&format!(
"round {round} probe: tool_calls={tc_count} usage=[{usage_str}] content={content_excerpt:?}"
),
color,
);
}
if !has_tools {
if let Some(mut nudge) = capability_evidence.correction(
&probe_content,
&tools,
&mut probe_correction_used,
tools_supported
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !is_cancelled(cancel),
) {
if let Some(harness) = smart_harness {
nudge = harness.correct_answer(&probe_content, &nudge)?;
}
messages.push(serde_json::json!({"role":"assistant", "content":probe_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
accumulated_usage = merge_round_usage(accumulated_usage, round_usage);
continue 'round_loop;
}
if let Some(harness) = smart_harness {
let more = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
let control = harness
.classify(&probe_content, narration_nudge_cap, more, cancel)
.await?;
let control = harness
.verify_answer(
control,
self_verify::Concluding {
messages: &messages,
workspace,
task,
rounds_left: more,
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
smart_verify,
&probe_content,
)
.await?;
let (text, reason) = match control {
smart_harness::Control::Continue(nudge) => {
messages
.push(serde_json::json!({"role":"assistant", "content":probe_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue 'round_loop;
}
smart_harness::Control::Answer => (
FinalReply::from(probe_content.clone()),
crate::TurnEndReason::Completed,
),
smart_harness::Control::Finish { text, reason } => (text, reason),
};
if let Some(slot) = &mut end_reason {
**slot = Some(reason);
}
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(reason, &text)?;
return Ok((text, false, accumulated_usage, hallucination_count));
}
if recovered.tool_shaped {
hallucination_count += 1;
if debug {
print_debug(
"format-hallucination: tool call emitted as unrecoverable text",
color,
);
}
}
if !is_cancelled(cancel)
&& readonly_completion_pending(
prompt_disposition,
&nudge_classifier,
&probe_content,
)
{
let more_rounds = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
if retry_readonly_completion(
&mut messages,
&probe_content,
None,
&mut readonly_completion_retried,
more_rounds,
) {
continue 'round_loop;
}
let out = readonly_completion_handoff(
&probe_content,
&mut end_reason,
more_rounds,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
macro_rules! maybe_nudge_no_tool_content {
($content:expr, $usage:expr) => {{
let content = $content;
if !content.is_empty() {
let nudge_classification = nudge_classifier.classify(content);
let workflow_classifier_text =
workflow_classifier_text(&messages, content);
let workflow_hint = nudge_classification
.is_plan_update()
.then(|| workflow_steerer.plan_update_hint(&workflow_classifier_text))
.flatten();
let classifier_plan_direction = nudge_classification
.is_plan_update()
.then(|| {
nudge_classifier.direction_for(crate::NudgeClass::PlanUpdate)
})
.flatten();
let plan_nudge_hint =
combine_nudge_hints(classifier_plan_direction, workflow_hint.as_deref());
if should_ground_unverified_run_command_blocker(
content,
&tools,
run_command_denial_observed,
exec_grounding_turn,
unverified_exec_blocker_nudges,
workflow_runtime.has_next_round(round, current_tool_round_limit, workflow_grace_rounds),
) {
if debug {
print_debug(
"unsupported run_command blocker — requiring a ground-truth probe",
color,
);
}
messages.push(serde_json::json!({
"role": "assistant",
"content": content
}));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
run_command_ground_truth_nudge()
)
}));
unverified_exec_blocker_nudges += 1;
accumulated_usage = merge_round_usage(accumulated_usage, $usage);
continue 'round_loop;
}
if action_nudges
&& pending_plan_nudges < PENDING_PLAN_NUDGE_CAP
&& workflow_runtime.has_next_round(round, current_tool_round_limit, workflow_grace_rounds)
{
if let Some(nudge) = pending_plan_completion_nudge(
step_ledger,
nudge_classification.is_plan_update(),
plan_nudge_hint.as_deref(),
) {
if debug {
print_debug(
"active plan has unfinished steps — nudging before final answer",
color,
);
}
messages.push(serde_json::json!({
"role": "assistant",
"content": content
}));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX, nudge
)
}));
pending_plan_nudges += 1;
accumulated_usage = merge_round_usage(accumulated_usage, $usage);
continue 'round_loop;
}
}
if stale_file_nudges < STALE_FILE_NUDGE_CAP
&& workflow_runtime.has_next_round(round, current_tool_round_limit, workflow_grace_rounds)
&& action_nudges
&& looks_like_unverified_stale_file_blocker(content)
{
if debug {
print_debug(
"unverified stale-file blocker — nudging to check ground truth",
color,
);
}
messages.push(serde_json::json!({
"role": "assistant",
"content": content
}));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
stale_file_ground_truth_nudge()
),
}));
stale_file_nudges += 1;
accumulated_usage = merge_round_usage(accumulated_usage, $usage);
continue 'round_loop;
}
if narration_nudges < narration_nudge_cap
&& workflow_runtime.has_next_round(round, current_tool_round_limit, workflow_grace_rounds)
&& nudge_classification.is_pending_action()
&& action_turn
{
if debug {
print_debug(
"narrated intent with no tool call — nudging to act and continuing",
color,
);
}
strip_trailing_nudge_exchange(&mut messages);
messages.push(serde_json::json!({
"role": "assistant",
"content": content
}));
let direction = if narration_nudges == 0 {
nudge_classifier
.direction_for(nudge_classification.class)
.map(workflow_guidance)
.unwrap_or_else(narration_action_nudge)
} else {
escalated_narration_action_nudge(
narration_nudges + 1,
narration_nudge_cap,
step_ledger,
)
};
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, direction),
}));
narration_nudges += 1;
accumulated_usage = merge_round_usage(accumulated_usage, $usage);
continue 'round_loop;
}
let accepted_reason = if nudge_classification.is_pending_action()
&& action_turn
{
if !workflow_runtime.has_next_round(round, current_tool_round_limit, workflow_grace_rounds) {
crate::TurnEndReason::NarrationFinalRound
} else {
crate::TurnEndReason::NarrationCapExhausted
}
} else {
crate::TurnEndReason::Completed
};
if debug && accepted_reason != crate::TurnEndReason::Completed {
print_debug(
&format!(
"no-tool narration accepted as final answer ({accepted_reason:?})"
),
color,
);
}
if let Some(slot) = &mut end_reason {
**slot = Some(accepted_reason);
}
}
}};
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
if color {
let native_thinking = json["message"]["thinking"]
.as_str()
.map(str::trim)
.filter(|thinking| !thinking.is_empty())
.map(str::to_string);
let reasoning = match (native_thinking, inline_reasoning.clone()) {
(Some(native), Some(inline)) => Some(format!("{native}\n{inline}")),
(native, inline) => native.or(inline),
};
commit_reasoning_fold(
reasoning,
probe_started.elapsed(),
completed_spill_renderer.as_deref(),
color,
);
}
if probe_content.is_empty() {
let merged = accumulated_usage;
let empty_round_usage = round_usage;
let generated_unusable_output = empty_round_usage
.as_ref()
.map(|u| u.output_tokens > 0)
.unwrap_or(false);
if generated_unusable_output
&& suspicious_empty_retries < SUSPICIOUS_EMPTY_RETRY_CAP
{
if trace {
print_trace(&ollama_response_shape(&json), color);
}
if debug {
let fields = ollama_non_content_fields(&json);
let field_note = if fields.is_empty() {
"no known non-content fields".to_string()
} else {
format!("non-content fields: {}", fields.join(", "))
};
print_debug(
&format!(
"empty assistant content with generated tokens — retrying ({}/{SUSPICIOUS_EMPTY_RETRY_CAP}; {field_note})",
suspicious_empty_retries + 1
),
color,
);
}
if !thinking_only_reported && !ollama_non_content_fields(&json).is_empty() {
thinking_only_reported = true;
if let Some(hook) = on_round_usage.as_deref_mut() {
hook(RoundObservation::ThinkingOnly);
}
}
messages.push(serde_json::json!({
"role": "user",
"content": suspicious_empty_retry_nudge(suspicious_empty_retries, &json)
}));
accumulated_usage = merged;
suspicious_empty_retries += 1;
continue 'round_loop;
}
let overflow_likely = merged
.as_ref()
.zip(safe_context)
.map(|(u, safe)| u.input_tokens >= safe * 85 / 100)
.unwrap_or(false);
if overflow_likely && overflow_retries < 2 {
emit_overflow_notice(
color,
&display::OverflowReason::EmptyReplyNearWindow {
prompt_tokens: merged.as_ref().map_or(0, |u| u.input_tokens),
window: safe_context.unwrap_or(0),
trigger_pct: 85,
},
model,
overflow_retries + 1,
);
let target = calibrate_down(
safe_context
.map(|s| (s as usize).saturating_mul(3) / 4)
.unwrap_or(0)
.saturating_sub(tool_tokens_real),
cal,
);
let compression = smart_harness::compress(
CompressRequest {
messages: &messages,
budget: target,
max_messages: None,
replay_protected_tail_len: 0,
task: active_task,
hard_budget: true,
authoritative: true,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = outcome?;
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.fired {
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
preflight_full_message_request(
candidate,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
target,
round,
"silent_overflow_recovery",
None,
false,
compress_state.floor_trend(),
color,
);
} else if !rewrites_history {
} else {
let fallback = crate::prune::prune(
&messages,
&crate::prune::PruneConfig {
keep_last: 2,
..Default::default()
},
);
if fallback.chars_reclaimed > 0 {
let tokens_before = estimate_tokens(&messages, estimation);
let tokens_after = estimate_tokens(&fallback.messages, estimation);
messages = fallback.messages;
prompt_tracker.invalidate();
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
CompressAction::Pruned,
tokens_before,
tokens_after,
target,
round,
"silent_overflow_structural_fallback",
None,
false,
compress_state.floor_trend(),
color,
);
}
}
accumulated_usage = merged;
overflow_retries += 1;
continue 'round_loop;
}
if overflow_likely {
if let (Some(hook), Some(u)) = (on_round_usage.as_deref_mut(), merged.as_ref())
{
hook(RoundObservation::SuspectedOverflow {
prompt_tokens: u.input_tokens,
});
}
}
if generated_unusable_output {
if trace {
print_trace(&ollama_response_shape(&json), color);
}
if !thinking_only_reported && !ollama_non_content_fields(&json).is_empty() {
if let Some(hook) = on_round_usage.as_deref_mut() {
hook(RoundObservation::ThinkingOnly);
}
}
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Empty);
}
observability::observe_harness_reply(&mut solve_obs);
return Ok((
finalize_final_text(
FinalReply::harness(suspicious_empty_ollama_diagnostic(&json)),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
),
false,
merged,
hallucination_count,
));
}
let msg = "(model returned an empty response — try rephrasing, or check the model with `newt doctor`)";
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Empty);
}
observability::observe_harness_reply(&mut solve_obs);
return Ok((
finalize_final_text(
FinalReply::harness(msg.to_owned()),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
),
false,
merged,
hallucination_count,
));
}
maybe_nudge_no_tool_content!(probe_content.as_str(), None);
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
let probe_content = finalize_final_text(
probe_content,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((probe_content, false, accumulated_usage, hallucination_count));
}
messages.push(message.clone());
let mut round_wrote = false;
let mut round_modified_workspace = false;
let mut round_progress = false;
let mut round_succeeded = false;
let tcs = tool_calls.unwrap();
let extracted: Vec<(Option<&str>, Option<&str>, &serde_json::Value)> = tcs
.iter()
.map(|tc| {
if tc["function"].is_null() {
(None, tc["name"].as_str(), &tc["input"])
} else {
(
None,
tc["function"]["name"].as_str(),
&tc["function"]["arguments"],
)
}
})
.collect();
let validated = match tools::validate_tool_call_batch(&extracted, false) {
Ok(v) => Some(v),
Err(rejection) => {
let reason = rejection.reason().to_string();
if let Some(harness) = smart_harness {
harness.reject_tools(&reason, true)?;
}
for _tc in tcs {
print_synthetic_tool_result(
"(rejected tool-call batch)",
&serde_json::Value::Null,
workspace,
&reason,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(
"(rejected tool-call batch)",
&serde_json::Value::Null,
false,
Some(0),
));
}
smart_harness::push_tool_message(
&mut messages,
serde_json::json!({
"role": "tool",
"content": format!("tool-call batch rejected before execution: {reason}"),
}),
smart_harness,
true,
"(rejected tool-call batch)",
)?;
}
None
}
};
let batch = smart_harness
.zip(validated.as_ref())
.map(|(harness, calls)| harness.tool_batch(calls, &messages))
.transpose()?;
let mut tool_warnings = Vec::new();
if validated.is_some() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
commit_tool_round_narration(
&probe_content,
false,
completed_spill_renderer.as_deref(),
color,
);
}
for (call_index, (_tc, vc)) in tcs.iter().zip(validated.iter().flatten()).enumerate() {
let name = vc.name.as_str();
let args = vc.args.clone();
announce_tool_activity(name);
dismiss_completed_spill(&completed_spill_renderer);
if is_hallucination(name, &args) {
hallucination_count += 1;
}
if let Some(content) =
repeat_calls.cached_read(name, &args, ReadScope { workspace, caveats })
{
print_synthetic_tool_result(
name,
&args,
workspace,
&content,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_cache(name, &args));
}
let offloaded =
maybe_offload_tool_result(name, content, tool_offload, spill_store, disclosure);
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({ "role": "tool", "content": offloaded }),
batch.as_ref(),
call_index,
)?;
continue;
}
if let Some(steer) = repeat_calls.repeat_steer(name, &args) {
print_synthetic_tool_result(
name,
&args,
workspace,
&steer,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(name, &args, false, Some(0)));
}
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({ "role": "tool", "content": steer }),
batch.as_ref(),
call_index,
)?;
continue;
}
let invocation = batch
.as_ref()
.map(|batch| batch.start(call_index, disclosure))
.transpose()?;
if !is_read_only_call(name, &args) {
round_wrote = true;
}
record_organic_note_use(name, ¬e_sink, &mut note_nudge);
let before_change =
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await;
ledger_note_write(write_ledger, name, &args, workspace);
let tool_t0 = std::time::Instant::now();
let execution = std::sync::OnceLock::new();
let retry_authorized = std::sync::OnceLock::new();
let routed_to = std::sync::OnceLock::new();
let governed_pr = std::sync::OnceLock::new();
let report_capture = std::sync::Mutex::new(observed_report::Capture::default());
let command_directory = std::sync::OnceLock::new();
let attribution_epoch = AttributionEpoch::new(attribution, git_tool, model, name);
let result = if tools::is_context_remaining_call(name) {
let report = budget::render_context_budget(
prompt_tracker.current(&messages, Some(&tools), cal, estimation),
effective_input_ceiling,
num_ctx,
input_ceiling_pct,
low_budget_pct,
);
if let Some(invocation) = &invocation {
invocation.host();
invocation.observe(&report, None, spill_store)?;
}
print_synthetic_tool_result(
name,
&args,
workspace,
&report,
color,
&completed_spill_renderer,
);
report
} else {
let tool_evidence = tool_events
.as_deref()
.map(|e| capability_check::Evidence::from_events(e));
let Some(result) = tools::execute_tool_with_collaborators(
name,
&args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
tools::ToolCollaborators {
command_budget,
shell_dialect,
read_history: Some(&mut read_history),
default_command_cwd,
worktree_session,
invocation: invocation.as_ref(),
build_check_cmd: build_check_cmd.as_deref(),
tool_evidence: tool_evidence.as_ref(),
note_sink: note_sink
.as_deref_mut()
.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
prompt_context: Some(prompt_context),
artifact_context,
artifact_sink,
permission_gate: permission_gate
.as_deref_mut()
.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav,
experience_store,
step_ledger,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
spill_store,
persona_tools,
hidden_tools: Some(&hidden_tools),
live_tool_output: live_tool_output.clone(),
completed_spill_renderer: completed_spill_renderer.clone(),
execution: Some(&execution),
retry_authorized: Some(&retry_authorized),
governed_pr: Some(&governed_pr),
report_capture: Some(&report_capture),
command_directory: Some(&command_directory),
routed_to: Some(&routed_to),
pending_rerun: Some(&mut pending_rerun),
},
tool_offload,
prompt_disposition,
cancel,
)
.await?
else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
result
};
let ok = tools::tool_ok(&result, execution.get().copied());
round_succeeded |= ok;
attribution_epoch.finish(&args, ok);
run_command_denial_observed |= run_command_result_is_denial(name, ok, &result);
capability_evidence.record(name, ok, execution.get().copied());
let after_change = if before_change.is_some() {
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await
} else {
None
};
round_modified_workspace |=
workflow_runtime.record_workspace_change(before_change, after_change);
round_progress |= workflow_runtime.record_observation(
name,
&args,
&result,
ok,
execution.get().copied(),
routed_to.get(),
);
repeat_calls.record(
name,
&args,
ok,
&result,
execution.get().copied(),
ReadScope { workspace, caveats },
);
repeat_calls.permit_authorized_retry(name, &args, &retry_authorized);
append_clean_build_warning(
if batch.is_some() {
&mut tool_warnings
} else {
&mut messages
},
&mut clean_build,
&args,
None,
)?;
record_completed_tool_event(
&mut tool_events,
name,
&args,
ok,
execution.get().copied(),
tool_t0,
);
turn_claims.observe_report(
workspace,
&caveats.fs_read,
&report_capture.lock().expect("report capture"),
execution.get().copied(),
governed_pr.get(),
);
if let Some(outcome) = governed_pr.get() {
verification.record_publication_outcome(outcome);
}
if ok {
if let Some(directory) = command_directory.get() {
verification.record_command_directory(directory);
}
}
verification
.observe_routed(
name,
&args,
ok,
execution.get().copied(),
workspace,
routed_to.get(),
)
.await;
record_phantom_reach(
&mut phantom_reaches,
name,
&args,
&result,
ok,
advertise_team,
);
observed_paths.record(&result, &mut observed_resolver);
smart_harness::push_tool_return(
&mut messages,
serde_json::json!({
"role": "tool",
"content": smart_harness::tool_result(name, result, tool_offload, spill_store, disclosure, invocation.as_ref())?
}),
invocation.as_ref(),
)?;
}
for message in tool_warnings {
smart_harness::push_tool_message(
&mut messages,
message,
smart_harness,
true,
"build guidance",
)?;
}
if round_wrote {
read_only_rounds = 0;
} else {
read_only_rounds = read_only_rounds.saturating_add(1);
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
if round_succeeded {
output_recovery.completed_tool_round();
}
workflow_runtime.record_round_outcome(round_modified_workspace, round_progress);
if let Some(text) = pending_plan_approval_handoff(
plan_mode_control,
PlanRoundFacts {
disposition: prompt_disposition,
step_ledger,
plan_at_turn_start: &plan_at_turn_start,
progressed: round_modified_workspace || round_progress,
},
smart_harness,
cancel,
&mut end_reason,
)? {
dismiss_completed_spill(&completed_spill_renderer);
let text = finalize_final_text(
FinalReply::harness(text),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((text, false, accumulated_usage, hallucination_count));
}
}
dismiss_completed_spill(&completed_spill_renderer);
let protected_head = protected_prompt_head_len(&messages, prompt_read::ACTIVE_PROMPT_PREFIX);
let trimmed = trim_for_summary(&messages, protected_head, 6);
let progress = cap_exit_progress(worktree_session, step_ledger, scratchpad_store);
let cap_reason = verification
.cap_exit_reason(
workspace,
smart_harness.is_some() && smart_verify,
current_tool_round_limit,
solve_obs.as_deref_mut(),
)
.await;
if let Some(hit) = &mut round_cap_hit {
**hit = true;
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
let text = cap_exit_fallback(
current_tool_round_limit,
accumulated_usage,
repeat_calls.total_failures(),
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(cap_reason, &text)?;
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let cap = CapExit {
max_tool_rounds: current_tool_round_limit,
accumulated: accumulated_usage,
wasted_calls: repeat_calls.total_failures(),
progress,
observed: observed_paths.into_vec(),
request_budget: authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
calibration: cal,
estimation,
ollama_options,
prompt_measurement: Default::default(),
fell_back: Default::default(),
};
let result = final_summary_ollama(&client, &chat_url, model, trimmed, &cap, attempts).await;
let (text, streamed, usage) = cap.recover_rejection(
result,
&mut solve_obs,
compress_state,
current_tool_round_limit,
cw_retries + 1,
)?;
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
Ok((text, streamed, usage, hallucination_count))
}
fn first_line(s: &str) -> String {
s.lines().next().unwrap_or("").chars().take(200).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum RepeatMemo {
Failure {
first_line: String,
retry_after_grant: bool,
},
NoResult {
reason: String,
},
EvidenceObserved {
subject: String,
advice: &'static str,
},
SkillRead,
ReadRange {
path: String,
content_id: content_addressable::RawContentId,
content: String,
},
}
#[derive(Clone, Copy)]
struct ReadScope<'a> {
workspace: &'a str,
caveats: &'a crate::caveats::Caveats,
}
impl ReadScope<'_> {
fn content_id_of(&self, path: &str) -> Option<content_addressable::RawContentId> {
tools::authorized_read("read_file", path, self.workspace, self.caveats, None)
.ok()
.map(|contents| content_addressable::RawContentId::from_content(contents.as_bytes()))
}
}
#[derive(Default)]
struct RepeatCallGuard {
repeat_memos: std::collections::HashMap<String, RepeatMemo>,
fails_by_tool: std::collections::HashMap<String, usize>,
}
impl RepeatCallGuard {
fn permit_authorized_retry(
&mut self,
name: &str,
args: &serde_json::Value,
ready: &std::sync::OnceLock<()>,
) {
if ready.get().is_some() {
self.repeat_memos.remove(&Self::key(name, args));
}
}
fn key(name: &str, args: &serde_json::Value) -> String {
format!("{name}\u{1}{args}")
}
fn repeat_steer(&self, name: &str, args: &serde_json::Value) -> Option<String> {
let key = Self::key(name, args);
let raw: String = match self.repeat_memos.get(&key)? {
RepeatMemo::Failure {
first_line: prev, ..
} => format!(
"You already called `{name}` with these exact arguments and it failed: {prev}. \
Inspect the error and correct its cause before retrying this call."
),
RepeatMemo::NoResult { .. } if name == "plan_get" => {
scheduled::EMPTY_PLAN_GUIDANCE.to_string()
}
RepeatMemo::NoResult { reason } => format!(
"You already ran `{name}` with these exact arguments this turn and {reason}. \
Don't repeat the identical call — create or update the missing state, change \
the arguments when the tool accepts them, or use a different tool."
),
RepeatMemo::EvidenceObserved { subject, advice } => format!(
"You already observed {subject} with `{name}` and received output. Do NOT repeat \
the identical call — {advice}"
),
RepeatMemo::ReadRange { .. } | RepeatMemo::SkillRead => return None,
};
Some(crate::ocap::redact_session_ingress(&raw))
}
fn successful_fetch_url(name: &str, args: &serde_json::Value, result: &str) -> Option<String> {
if name != "web_fetch" {
return None;
}
let url = args.get("url")?.as_str()?.trim();
if url.is_empty() || result.trim().is_empty() {
None
} else {
Some(url.chars().take(200).collect())
}
}
fn successful_read_only_shell_command(
name: &str,
args: &serde_json::Value,
result: &str,
) -> Option<String> {
if name != "run_command" || result.trim().is_empty() {
return None;
}
let command = args.get("command")?.as_str()?.trim();
if is_read_only_shell_probe(command) {
Some(command.chars().take(200).collect())
} else {
None
}
}
fn no_result_reason(name: &str, result: &str) -> Option<&'static str> {
match name {
"recall" if result.starts_with("no matches in past conversations") => Some(
"it returned no matches (and recall cannot see the current conversation \
— use resume_context for THIS conversation)",
),
"state_get" if result.starts_with("no such key") => {
Some("the key is not set (state_get returned \"no such key\")")
}
"plan_get" if result.starts_with("no active plan") => Some(
"it found no active plan; call update_plan now with a short ordered plan if the \
work has more than one step",
),
_ => None,
}
}
fn classify_repeat_memo(
name: &str,
args: &serde_json::Value,
ok: bool,
result: &str,
execution: Option<crate::ExecOutcome>,
) -> Option<RepeatMemo> {
if !ok {
let lower = result.to_ascii_lowercase();
return Some(RepeatMemo::Failure {
first_line: first_line(result),
retry_after_grant: CONFINEMENT_DENIAL_NEEDLES
.iter()
.any(|needle| lower.contains(needle))
|| (matches!(name, "run_command" | "lifecycle")
&& matches!(
execution,
Some(crate::ExecOutcome::Failed | crate::ExecOutcome::Denied)
)),
});
}
if let Some(reason) = Self::no_result_reason(name, result) {
return Some(RepeatMemo::NoResult {
reason: reason.to_string(),
});
}
if let Some(url) = Self::successful_fetch_url(name, args, result) {
return Some(RepeatMemo::EvidenceObserved {
subject: format!("`{url}`"),
advice: "use the fetched content above, fetch a different URL, inspect local \
files, or answer the user.",
});
}
if let Some(command) = Self::successful_read_only_shell_command(name, args, result) {
return Some(RepeatMemo::EvidenceObserved {
subject: format!("read-only shell probe `{command}`"),
advice: "use the observed output above, change the query, inspect a different \
file, or make the next edit/test decision.",
});
}
if name == "use_skill" && !result.trim().is_empty() {
return Some(RepeatMemo::SkillRead);
}
if let Some(path) = Self::bare_read_file_path(name, args) {
let content_id = content_addressable::RawContentId::from_content(result.as_bytes());
return Some(RepeatMemo::ReadRange {
path,
content_id,
content: result.to_string(),
});
}
None
}
fn bare_read_file_path(name: &str, args: &serde_json::Value) -> Option<String> {
if name != "read_file" || args.get("offset").is_some() || args.get("limit").is_some() {
return None;
}
Some(args.get("path")?.as_str()?.to_string())
}
fn cached_read(
&mut self,
name: &str,
args: &serde_json::Value,
read_scope: ReadScope<'_>,
) -> Option<String> {
if name == "use_skill"
&& matches!(
self.repeat_memos.get(&Self::key(name, args)),
Some(RepeatMemo::SkillRead)
)
{
let skill = args
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
return Some(format!("Skill `{skill}` is loaded in the current context."));
}
let path = Self::bare_read_file_path(name, args)?;
let key = Self::key(name, args);
let Some(RepeatMemo::ReadRange {
content_id,
content,
..
}) = self.repeat_memos.get(&key)
else {
return None;
};
if read_scope.content_id_of(&path).as_ref() == Some(content_id) {
Some(content.clone())
} else {
self.repeat_memos.remove(&key);
None
}
}
fn record(
&mut self,
name: &str,
args: &serde_json::Value,
ok: bool,
result: &str,
execution: Option<crate::ExecOutcome>,
_read_scope: ReadScope<'_>,
) {
if tools::permission_grant_succeeded(name, args, ok, result, execution) {
self.repeat_memos.retain(|_, memo| {
!matches!(
memo,
RepeatMemo::Failure {
retry_after_grant: true,
..
}
)
});
}
if ok && may_change_workspace(name, args) {
self.repeat_memos.retain(|_, memo| {
!matches!(
memo,
RepeatMemo::Failure { .. }
| RepeatMemo::ReadRange { .. }
| RepeatMemo::SkillRead
)
});
}
if name == "update_plan" && ok {
self.repeat_memos.retain(|key, _| {
!key.starts_with("plan_get\u{1}") && !key.starts_with("update_plan\u{1}")
});
}
if !ok {
*self.fails_by_tool.entry(name.to_string()).or_default() += 1;
}
if let Some(memo) = Self::classify_repeat_memo(name, args, ok, result, execution) {
self.repeat_memos.insert(Self::key(name, args), memo);
}
}
fn total_failures(&self) -> usize {
self.fails_by_tool.values().sum()
}
fn release_read_memos(&mut self) {
self.repeat_memos.retain(|_, memo| {
!matches!(memo, RepeatMemo::ReadRange { .. } | RepeatMemo::SkillRead)
});
}
}
#[derive(Debug, Clone, Copy)]
enum ProgressSnapshot {
File(content_addressable::ContentId),
Tree(content_addressable::ContentId),
}
async fn progress_workspace_state(
name: &str,
args: &serde_json::Value,
workspace: &str,
fs_read: &crate::caveats::Scope<String>,
) -> Option<ProgressSnapshot> {
if !matches!(
name,
"run_command" | "write_file" | "edit_file" | "delete_file" | "lifecycle" | "git"
) || !may_change_workspace(name, args)
{
return None;
}
let root = std::path::PathBuf::from(workspace);
let target = matches!(name, "write_file" | "edit_file" | "delete_file")
.then(|| args.get("path").and_then(serde_json::Value::as_str))
.flatten()
.map(|path| lexical_normalize(&root.join(path)));
let fs_read = fs_read.clone();
tokio::task::spawn_blocking(move || {
if let Some(target) = target {
use tools::file_capture::TextSnapshot;
let version = match tools::file_capture::capture(&fs_read, &target) {
TextSnapshot::Present(text) => Some(Some(
content_addressable::RawContentId::from_content(text.as_bytes()),
)),
TextSnapshot::Absent => Some(None),
TextSnapshot::Unavailable(_) => None,
};
if let Some(version) = version {
let bytes = content_addressable::canonical::to_canonical_dagcbor(&(
target.to_string_lossy(),
version,
))
.ok()?;
return Some(ProgressSnapshot::File(
content_addressable::ContentId::from_canonical_bytes(&bytes),
));
}
}
if !crate::caveats::permits_path(&fs_read, &root.to_string_lossy()) {
return None;
}
self_verify::workspace_tree_state(&root).map(ProgressSnapshot::Tree)
})
.await
.ok()
.flatten()
}
#[derive(Debug, Default)]
struct WorkflowRuntimeState {
observed_results: std::collections::HashSet<content_addressable::RawContentId>,
verified_checks: std::collections::HashSet<content_addressable::ContentId>,
observed_trees: std::collections::HashSet<content_addressable::ContentId>,
progress_revision: usize,
last_cap_progress_revision: usize,
rounds_since_progress: Option<usize>,
progress_horizon_rounds: Option<usize>,
initiative: crate::initiative::Initiative,
consecutive_read_only_rounds: usize,
no_progress: crate::initiative::NoProgressRounds,
rounds_without_change: usize,
no_progress_steered: bool,
round_progressed: bool,
}
enum NoProgress {
Continue,
Steer(String),
Stop,
}
impl WorkflowRuntimeState {
fn set_progress_horizon(&mut self, rounds: Option<usize>) {
self.progress_horizon_rounds = rounds;
}
fn progress_horizon(&self) -> usize {
self.progress_horizon_rounds
.unwrap_or(WORKFLOW_RECENT_PROGRESS_ROUNDS)
}
fn record_workspace_change(
&mut self,
before: Option<ProgressSnapshot>,
after: Option<ProgressSnapshot>,
) -> bool {
let (before, after) = match (before, after) {
(Some(ProgressSnapshot::File(before)), Some(ProgressSnapshot::File(after)))
| (Some(ProgressSnapshot::Tree(before)), Some(ProgressSnapshot::Tree(after))) => {
(before, after)
}
_ => return false,
};
self.observed_trees.insert(before);
let changed = before != after && self.observed_trees.insert(after);
if changed {
self.verified_checks.clear();
}
changed
}
fn record_observation(
&mut self,
name: &str,
args: &serde_json::Value,
result: &str,
ok: bool,
execution: Option<crate::ExecOutcome>,
routed_to: Option<&(&'static str, serde_json::Value)>,
) -> bool {
if ok && is_progress_verification(name, args, execution, routed_to) {
let mut call = routed_to
.map(|(name, args)| (*name, args.clone()))
.unwrap_or_else(|| (name, args.clone()));
if let ("build_exec", serde_json::Value::Object(args)) = &mut call {
args.remove("timeout_secs");
args.remove("trim");
args.remove("echo_dropped");
}
return content_addressable::canonical::to_canonical_dagcbor(&call)
.ok()
.is_some_and(|bytes| {
self.verified_checks
.insert(content_addressable::ContentId::from_canonical_bytes(&bytes))
});
}
if execution == Some(crate::ExecOutcome::Passed)
&& (name == "lifecycle" || routed_to.is_some_and(|(name, _)| *name == "build_exec"))
{
return false;
}
if result.trim().is_empty()
|| is_workspace_write_call(name)
|| matches!(
name,
"update_plan" | "state_set" | "state_clear" | "render_report"
)
|| tools::is_context_remaining_call(name)
|| (!ok && workflow_error_fingerprint(result).is_none())
{
return false;
}
let error = (!ok).then(|| workflow_error_fingerprint(result)).flatten();
self.observed_results
.insert(content_addressable::RawContentId::from_content(
error.as_deref().unwrap_or(result).as_bytes(),
))
}
fn note_operator_steering(&mut self) {
self.round_progressed = true;
}
fn no_progress_verdict(&mut self, steer_allowed: bool) -> NoProgress {
if std::mem::take(&mut self.round_progressed) {
self.rounds_without_change = 0;
self.no_progress_steered = false;
} else {
self.rounds_without_change = self.rounds_without_change.saturating_add(1);
}
let (n, cfg) = (self.rounds_without_change, self.no_progress);
if cfg.stop_after > 0 && n >= cfg.stop_after {
return NoProgress::Stop;
}
if steer_allowed && cfg.steer_after > 0 && n >= cfg.steer_after && !self.no_progress_steered
{
self.no_progress_steered = true;
return NoProgress::Steer(format!(
"No progress: {n} rounds produced no new evidence, workspace change, or newly \
verified check. Stop repeating the same observations. Stop varying \
the command. Say what is done and what is not done, then stop."
));
}
NoProgress::Continue
}
fn no_progress_notice(&self) -> String {
format!(
"Stopped: {} consecutive rounds produced no new evidence, workspace change, or \
newly verified check. Work done so far is left in the workspace.",
self.rounds_without_change
)
}
fn record_round_outcome(&mut self, round_wrote: bool, round_progress: bool) {
if round_wrote || round_progress {
self.round_progressed = true;
self.progress_revision = self.progress_revision.saturating_add(1);
}
if round_wrote {
self.consecutive_read_only_rounds = 0;
} else {
self.consecutive_read_only_rounds = self.consecutive_read_only_rounds.saturating_add(1);
}
if round_wrote || round_progress {
self.rounds_since_progress = Some(0);
} else if let Some(rounds) = self.rounds_since_progress.as_mut() {
*rounds = rounds.saturating_add(1);
}
}
fn action_forcing_nudge(
&mut self,
hard_rounds_remaining: Option<usize>,
step_ledger: Option<&dyn scheduled::StepLedger>,
delegate_hint: Option<&str>,
) -> Option<String> {
if self.consecutive_read_only_rounds < self.initiative.read_only_nudge_after() {
return None;
}
let nudge = read_only_action_nudge(
self.consecutive_read_only_rounds,
hard_rounds_remaining,
step_ledger,
delegate_hint,
);
self.consecutive_read_only_rounds = 0;
Some(nudge)
}
fn admit_round(&mut self, round: usize, limit: &mut usize, grace: usize) -> bool {
if round < *limit {
return true;
}
if !self.can_renew(grace) {
return false;
}
self.last_cap_progress_revision = self.progress_revision;
*limit = limit.saturating_add(grace);
round < *limit
}
fn can_renew(&self, grace: usize) -> bool {
grace > 0
&& self.progress_revision > self.last_cap_progress_revision
&& self
.rounds_since_progress
.is_some_and(|rounds| rounds <= self.progress_horizon())
}
fn has_next_round(&self, round: usize, limit: usize, grace: usize) -> bool {
round.saturating_add(1) < limit || self.can_renew(grace)
}
}
fn workflow_error_fingerprint(result: &str) -> Option<String> {
if result.lines().next().is_some_and(|first| {
first.starts_with("error: reading ")
|| first.starts_with("error: writing ")
|| first.starts_with("error: deleting ")
}) {
return None;
}
build_error_fingerprint(result).or_else(|| edit_miss_fingerprint(result))
}
fn build_error_fingerprint(result: &str) -> Option<String> {
let mut pending_error: Option<String> = None;
let mut fingerprints = Vec::new();
for line in result.lines() {
let trimmed = line.trim();
if trimmed.starts_with("error[") || trimmed.starts_with("error:") {
pending_error = Some(normalize_error_line(trimmed));
continue;
}
if let Some(rest) = trimmed.strip_prefix("-->") {
if let Some(error) = pending_error.take() {
let location = rest
.split_whitespace()
.next()
.unwrap_or("")
.trim()
.trim_start_matches("./");
if location.is_empty() {
fingerprints.push(error);
} else {
fingerprints.push(format!("{location} {error}"));
}
if fingerprints.len() >= 3 {
break;
}
}
}
}
if fingerprints.is_empty() {
pending_error.map(|e| e.chars().take(240).collect())
} else {
Some(fingerprints.join(" | ").chars().take(500).collect())
}
}
fn edit_miss_fingerprint(result: &str) -> Option<String> {
let lc = result.to_ascii_lowercase();
if !(lc.contains("old_string")
&& (lc.contains("not found")
|| lc.contains("old string not found")
|| lc.contains("matches 0")
|| lc.contains("no match")))
{
return None;
}
let line = result
.lines()
.find(|line| {
let l = line.to_ascii_lowercase();
l.contains("old_string")
&& (l.contains("not found") || l.contains("matches 0") || l.contains("no match"))
})
.unwrap_or("edit_file old_string not found");
Some(format!("edit_file {}", normalize_error_line(line)))
}
fn normalize_error_line(line: &str) -> String {
let mut out = String::new();
let mut in_ws = false;
for c in line.chars() {
if c.is_whitespace() {
if !in_ws {
out.push(' ');
in_ws = true;
}
} else if c != '`' {
out.push(c);
in_ws = false;
}
}
out.chars().take(240).collect()
}
fn active_step_description(step_ledger: Option<&dyn scheduled::StepLedger>) -> Option<String> {
let snapshot = step_ledger?.snapshot();
snapshot
.steps
.iter()
.find(|step| step.status == StepStatus::Active)
.or_else(|| {
snapshot
.steps
.iter()
.find(|step| step.status != StepStatus::Done)
})
.map(|step| step.description.clone())
}
fn workflow_guidance(direction: impl std::fmt::Display) -> String {
format!(
"{direction} {REFACTOR_PUBLISH_GUIDANCE} Guidance is advisory: follow the latest operator instruction, including \
newer steering. Honor stop or report-only requests. Reconcile changed plans with \
update_plan; preserve completed work."
)
}
const PERMISSION_RECOVERY_GUIDANCE: &str =
"Use an already-authorized alternative if permitted. For an exact requestable grant with \
an available operator, call request_permissions once. Never re-ask for or retry a permission \
the operator declined, or ask again with no operator available. Do not bypass a refusal.";
fn cap_exit_progress(
worktree_session: Option<&crate::worktree_adoption::WorktreeSession>,
step_ledger: Option<&dyn scheduled::StepLedger>,
scratchpad_store: Option<&dyn scratchpad::ScratchpadStore>,
) -> Option<String> {
let plan = step_ledger.and_then(scheduled::plan_block);
let state = scratchpad_store.and_then(scratchpad::scratchpad_state_block);
let task = worktree_session.and_then(crate::worktree_adoption::WorktreeSession::task_hint);
let parts: Vec<String> = [plan, state, task].into_iter().flatten().collect();
(!parts.is_empty()).then(|| parts.join("\n\n"))
}
fn is_read_only_tool(name: &str) -> bool {
crate::navigator::NAV_TOOL_NAMES.contains(&name)
|| matches!(
name,
"list_dir"
| "read_file"
| "find"
| "search"
| "web_fetch"
| "use_skill"
| "save_note"
| "recall"
| "prompt_read"
| "artifact_read"
)
}
fn is_read_only_call(name: &str, args: &serde_json::Value) -> bool {
is_read_only_tool(name)
|| (name == "git"
&& args
.get("op")
.and_then(|op| op.as_str())
.is_some_and(git_tool::is_scoped_read_op))
|| (name == "run_command"
&& args
.get("command")
.and_then(|v| v.as_str())
.is_some_and(is_read_only_shell_probe))
}
pub fn is_workspace_write_call(name: &str) -> bool {
matches!(name, "write_file" | "edit_file")
}
fn is_progress_verification(
name: &str,
args: &serde_json::Value,
execution: Option<crate::ExecOutcome>,
routed_to: Option<&(&'static str, serde_json::Value)>,
) -> bool {
if execution != Some(crate::ExecOutcome::Passed) {
return false;
}
if name == "lifecycle" {
return args
.get("phase")
.and_then(|v| v.as_str())
.and_then(crate::tooling::Phase::from_key)
.is_some_and(crate::tooling::Phase::is_gate);
}
if name != "run_command" {
return false;
}
let Some(("build_exec", routed)) = routed_to.map(|(tool, args)| (*tool, args)) else {
return false;
};
let Some(argv) = routed.get("argv").and_then(|v| v.as_array()) else {
return false;
};
let sub = match argv.get(1).and_then(|v| v.as_str()) {
Some(token) if routing::is_toolchain_selector(token) => {
argv.get(2).and_then(|v| v.as_str())
}
other => other,
};
sub.and_then(crate::tooling::Phase::from_key)
.is_some_and(crate::tooling::Phase::is_gate)
}
fn is_workspace_inert_tool(name: &str) -> bool {
matches!(
name,
"code_search"
| "where_is"
| "tool_search"
| "memory_fetch"
| "re_read"
| "resume_context"
| "experience_recall"
| "experience_record"
| "state_get"
| "state_set"
| "state_clear"
| "plan_get"
| "update_plan"
| "render_report"
| "request_permissions"
| "request_user_input"
| "select_operating_mode"
| "enter_plan_mode"
| "exit_plan_mode"
) || tools::is_context_remaining_call(name)
}
const VERIFICATION_READ_PROGRAMS: &[&str] = &[
"grep",
"rg",
"head",
"tail",
"wc",
"pwd",
"cat",
"ls",
"find",
"stat",
"file",
"tree",
"git status",
"git diff",
"git log",
"git show",
];
pub(crate) fn dispatched_tool_name(name: &str) -> Option<&str> {
match tools::resolve_tool_alias(name) {
Some(tools::AliasOutcome::Correct(_)) => None,
Some(tools::AliasOutcome::Rewrite(canonical)) => Some(canonical),
None => Some(name),
}
}
pub(crate) fn may_change_workspace(name: &str, args: &serde_json::Value) -> bool {
let Some(name) = dispatched_tool_name(name) else {
return false;
};
if name == "run_command" {
return args["command"]
.as_str()
.is_none_or(|command| !is_verification_read_command(command));
}
!is_read_only_call(name, args) && !is_workspace_inert_tool(name)
}
pub(crate) fn is_verification_read_command(command: &str) -> bool {
let command = command.trim();
const SHELL_META: &[char] = &['&', '|', ';', '`', '$', '\n', '>', '<', '(', ')'];
if command.is_empty() || command.contains(SHELL_META) {
return false;
}
let tokens: Vec<String> = command
.split_whitespace()
.map(|t| t.replace(['"', '\'', '\\'], ""))
.collect();
let acts = tokens.iter().any(|t| {
t.starts_with("--output")
|| matches!(
t.as_str(),
"-delete"
| "-exec"
| "-execdir"
| "-ok"
| "-okdir"
| "-fprint"
| "-fprint0"
| "-fprintf"
| "-fls"
)
}) || (tokens[0] == "tree" && tokens.iter().any(|t| t == "-o"));
if acts {
return false;
}
VERIFICATION_READ_PROGRAMS
.iter()
.any(|p| command == *p || command.starts_with(&format!("{p} ")))
}
fn redact_model_facing(disclosure: Option<&crate::ocap::DisclosureFilter>, text: String) -> String {
let text = match disclosure {
Some(filter) => filter.redact(&text),
None => text,
};
crate::ocap::redact_session_ingress(&text)
}
fn maybe_offload_tool_result(
name: &str,
result: String,
tool_offload: bool,
spill_store: Option<&dyn content_spill::SpillStore>,
disclosure: Option<&crate::ocap::DisclosureFilter>,
) -> String {
let out = if matches!(
name,
"run_command" | "lifecycle" | "prompt_read" | "artifact_read" | "memory_fetch"
) {
result
} else {
content_spill::maybe_offload(result, tool_offload, Some(name.to_string()), spill_store)
};
let out = match disclosure {
Some(filter) => filter.redact(&out),
None => out,
};
crate::ocap::redact_session_ingress(&out)
}
#[allow(clippy::too_many_arguments)]
fn record_compaction_artifact(
repeat_calls: &mut RepeatCallGuard,
artifact_sink: Option<&dyn artifact_read::PromptArtifactSink>,
artifact_context: Option<artifact_read::ArtifactReadContext<'_>>,
action: CompressAction,
tokens_before: usize,
tokens_after: usize,
budget: usize,
round: usize,
reason: &str,
trigger: Option<&CompressTrigger>,
send_budget_authoritative: bool,
floor_trend: FloorTrend,
color: bool,
) {
repeat_calls.release_read_memos();
let (Some(sink), Some(context)) = (artifact_sink, artifact_context) else {
return;
};
if let Err(error) = artifact_hooks::record_compaction_checkpoint(
sink,
context,
action,
tokens_before,
tokens_after,
budget,
round,
reason,
trigger,
send_budget_authoritative,
floor_trend,
) {
print_harness_notice(
&format!("warning: failed to record compaction artifact: {error}"),
color,
);
}
}
fn is_read_only_shell_probe(command: &str) -> bool {
let command = command.trim();
if command.is_empty() {
return false;
}
const SHELL_META: &[char] = &['&', '|', ';', '`', '$', '\n', '>', '<', '(', ')'];
if command.contains(SHELL_META) {
return false;
}
let mut tokens = command.split_ascii_whitespace();
let Some(program) = tokens.next() else {
return false;
};
match program {
"grep" | "rg" | "head" | "tail" | "wc" | "pwd" => true,
"sed" => !tokens.any(|t| t == "-i" || t.starts_with("-i")),
_ => false,
}
}
const PENDING_PLAN_NUDGE_CAP: usize = 1;
const SUSPICIOUS_EMPTY_RETRY_CAP: u32 = 2;
const STALE_FILE_NUDGE_CAP: usize = 1;
const UNVERIFIED_EXEC_BLOCKER_NUDGE_CAP: usize = 1;
fn looks_like_unverified_run_command_blocker(content: &str) -> bool {
let lower = content.to_ascii_lowercase();
lower.contains("run_command")
&& [
"permission-denied",
"permission denied",
"exec not granted",
"capability wall",
"run_command is unavailable",
"run_command unavailable",
"can't run",
"cannot run",
]
.iter()
.any(|needle| lower.contains(needle))
}
fn run_command_is_advertised(tools: &serde_json::Value) -> bool {
tools.as_array().is_some_and(|defs| {
defs.iter().any(|def| {
def.pointer("/function/name")
.and_then(serde_json::Value::as_str)
== Some("run_command")
|| (def.get("name").and_then(serde_json::Value::as_str) == Some("run_command"))
})
})
}
const CONFINEMENT_DENIAL_NEEDLES: [&str; 3] = [
"not within the granted authority",
"does not permit",
"capability denied",
];
const OS_PERMISSION_DENIAL_NEEDLES: [&str; 2] = ["permission denied", "permission-denied"];
fn run_command_result_is_denial(tool_name: &str, ok: bool, result: &str) -> bool {
if tool_name != "run_command" || ok {
return false;
}
let lower = result.to_ascii_lowercase();
(lower.contains(tools::ABSENT_BINARY_MARKER) && !lower.contains(tools::NOT_ON_HOST_MARKER))
|| CONFINEMENT_DENIAL_NEEDLES
.iter()
.chain(OS_PERMISSION_DENIAL_NEEDLES.iter())
.any(|needle| lower.contains(needle))
}
fn run_command_ground_truth_nudge() -> &'static str {
"Your capability-wall claim is unsupported: no returned run_command result \
this turn contains an exec denial. If you have not probed the relevant \
command, call run_command now and inspect its returned result. If a probe \
succeeded, ground the answer in that success. Report a denial only when an \
actual returned result contains one; otherwise continue the task."
}
fn should_ground_unverified_run_command_blocker(
content: &str,
tools: &serde_json::Value,
denial_observed: bool,
action_nudges: bool,
nudges_used: usize,
has_next_round: bool,
) -> bool {
action_nudges
&& nudges_used < UNVERIFIED_EXEC_BLOCKER_NUDGE_CAP
&& has_next_round
&& run_command_is_advertised(tools)
&& !denial_observed
&& looks_like_unverified_run_command_blocker(content)
}
#[cfg(test)]
#[path = "mod_tests/exec_grounding_tests.rs"]
mod exec_grounding_tests;
const WORKFLOW_RECENT_PROGRESS_ROUNDS: usize = 3;
fn tail_on_char_boundary(s: &str, max_bytes: usize) -> &str {
let cut = s.len().saturating_sub(max_bytes);
let start = (cut..=s.len())
.find(|&i| s.is_char_boundary(i))
.unwrap_or(0);
&s[start..]
}
#[cfg(test)]
fn looks_like_intent_to_act(content: &str) -> bool {
crate::NudgeClassifier::builtin().is_pending_action(content)
}
fn readonly_completion_pending(
disposition: PromptDisposition,
classifier: &crate::NudgeClassifier,
content: &str,
) -> bool {
matches!(
disposition,
PromptDisposition::Explain | PromptDisposition::Research
) && classifier.classify(content).class == crate::NudgeClass::DeferredAnswer
}
fn retry_readonly_completion(
messages: &mut Vec<serde_json::Value>,
content: &str,
replay: Option<&[serde_json::Value]>,
retried: &mut bool,
more_rounds: bool,
) -> bool {
if *retried || !more_rounds {
return false;
}
*retried = true;
strip_trailing_nudge_exchange(messages);
if let Some(items) = replay {
messages.extend_from_slice(items);
}
messages.push(serde_json::json!({"role": "assistant", "content": content}));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} Your reply promised another step but did not answer the request. \
Continue using only the read-only tools already available, or give the answer \
supported by the evidence. If something is missing, ask a concrete clarification \
or state the unresolved blocker. Do not end with another promise to check.",
compress::LOOP_GUIDANCE_PREFIX
),
}));
true
}
#[allow(clippy::too_many_arguments)]
fn readonly_completion_handoff(
content: &str,
end_reason: &mut Option<&mut Option<crate::TurnEndReason>>,
more_rounds: bool,
workspace: &str,
read_scope: &crate::Scope<String>,
capability_evidence: &capability_check::Evidence,
disclosure: Option<&crate::ocap::DisclosureFilter>,
turn_claims: &claim_check::TurnClaims<'_>,
verification: &self_verify::VerificationLedger,
) -> String {
if let Some(slot) = end_reason {
**slot = Some(if more_rounds {
crate::TurnEndReason::NarrationCapExhausted
} else {
crate::TurnEndReason::NarrationFinalRound
});
}
finalize_final_text(
FinalReply::from(content.to_owned()).with_notice(
"This response appears unfinished. If the request is still unresolved, \
reply `continue` to resume the read-only check, or clarify what evidence you need."
.into(),
),
workspace,
read_scope,
capability_evidence,
disclosure,
turn_claims,
verification,
)
}
struct PlanRoundFacts<'a> {
disposition: PromptDisposition,
step_ledger: Option<&'a dyn StepLedger>,
plan_at_turn_start: &'a scheduled::PlanSnapshot,
progressed: bool,
}
fn pending_plan_approval_handoff(
control: Option<&dyn PlanModeControl>,
round: PlanRoundFacts<'_>,
harness: Option<&smart_harness::SmartHarness>,
cancel: Option<&std::sync::atomic::AtomicBool>,
end_reason: &mut Option<&mut Option<crate::TurnEndReason>>,
) -> anyhow::Result<Option<String>> {
if let Some(control) = control {
let plan_recorded = round.step_ledger.is_some_and(|ledger| {
scheduled::fresh_multi_step_plan(round.plan_at_turn_start, &ledger.snapshot())
});
if !control.exit_requested()
&& round.disposition == PromptDisposition::Plan
&& !round.progressed
&& plan_recorded
{
let _ = control.request_exit();
}
}
if !control.is_some_and(PlanModeControl::exit_requested) {
return Ok(None);
}
if is_cancelled(cancel) {
return smart_harness::cancelled(harness, end_reason).map(Some);
}
let text = "Awaiting approval to leave plan mode.";
if let Some(harness) = harness {
harness.outcome(crate::TurnEndReason::AwaitingOperator, text)?;
}
if let Some(slot) = end_reason {
**slot = Some(crate::TurnEndReason::AwaitingOperator);
}
Ok(Some(text.to_owned()))
}
fn looks_like_unverified_stale_file_blocker(content: &str) -> bool {
const FILE_CUES: &[&str] = &[
"file",
"line reference",
"line references",
"old_string",
"edit_file",
".rs",
".toml",
".md",
];
const STALE_CUES: &[&str] = &[
"modified out from under",
"changed out from under",
"edited out from under",
"modified concurrently",
"changed concurrently",
"stale context",
"contexts are stale",
"context is stale",
"old line references",
"line references are invalid",
"file grew from",
"grew from",
];
const BLOCKER_CUES: &[&str] = &[
"blocked",
"cannot safely",
"can't safely",
"could land in the wrong place",
"corrupt the code",
"restore",
"git checkout",
"revert",
"operator should",
"human should",
"recommendation",
];
let lc = content.to_lowercase();
let tail = tail_on_char_boundary(&lc, 1_200);
FILE_CUES.iter().any(|c| tail.contains(c))
&& STALE_CUES.iter().any(|c| tail.contains(c))
&& BLOCKER_CUES.iter().any(|c| tail.contains(c))
}
fn narration_action_nudge() -> String {
workflow_guidance(
"You described what you were about to do but did not call any tool, so \
nothing actually happened. If you intended to act, emit the tool call now \
(for example edit_file or write_file with the real arguments) — do not just \
describe it. If you are genuinely finished, say so explicitly in one \
sentence.",
)
}
fn post_compaction_continuation(
worktree_session: Option<&crate::worktree_adoption::WorktreeSession>,
step_ledger: Option<&dyn scheduled::StepLedger>,
prompt_context: prompt_read::PromptReadContext<'_>,
) -> String {
let plan_clause = step_ledger
.and_then(scheduled::plan_block)
.map(|plan| {
format!(
" Your saved plan is below. Continue from the `→` step when it still \
matches the operator's request; otherwise reconcile it with update_plan:\n{plan}\n"
)
})
.unwrap_or_default();
let instruction_clause = prompt_context.active_receipt().map_or_else(
|| {
" Re-read the protected [NEWT ACTIVE PROMPT v1] prompt pair (or call \
prompt_read with address `current`) before continuing."
.to_string()
},
|receipt| {
format!(
" The authoritative operator prompt is protected in the [NEWT ACTIVE \
PROMPT v1] metadata/user pair at address {} with model digest {}; call prompt_read \
with address `current` to recover it verbatim before continuing.",
receipt.id(),
receipt.model_digest()
)
},
);
let task_clause = worktree_session
.and_then(crate::worktree_adoption::WorktreeSession::task_hint)
.map(|hint| format!("\n{hint}\n"))
.unwrap_or_default();
workflow_guidance(format!(
"{} You are mid-task: the context above was just compacted, not \
completed.{instruction_clause}{plan_clause}{task_clause} For prompt-rooted work, \
use artifact_read {{\"address\":\"root\"}} before deciding what remains. \
When continuing, your next output should be the next concrete tool call; re-read \
files before editing. To re-anchor on ground truth, check the current git branch and the last few \
commits: earlier work may already be COMMITTED, so a clean working tree does NOT mean \
no work happened. Preserve completed work; \
do not repeat work the log shows is done or silently narrow the task.",
compress::CONTINUATION_PREFIX
))
}
#[allow(clippy::too_many_arguments)]
fn apply_post_compaction_continuation(
worktree_session: Option<&crate::worktree_adoption::WorktreeSession>,
messages: &mut Vec<serde_json::Value>,
narration_nudges: &mut usize,
action: CompressAction,
step_ledger: Option<&dyn scheduled::StepLedger>,
prompt_context: prompt_read::PromptReadContext<'_>,
enabled_mid_turn: bool,
admit: impl FnOnce(&[serde_json::Value]) -> bool,
) {
if !enabled_mid_turn
|| !matches!(
action,
CompressAction::Summarized | CompressAction::StaticFallback
)
{
return;
}
*narration_nudges = 0;
let mut candidate = messages.clone();
candidate.retain(|m| !compress::is_continuation_message(m));
candidate.push(serde_json::json!({
"role": "user",
"content": post_compaction_continuation(worktree_session, step_ledger, prompt_context),
}));
if admit(&candidate) {
*messages = candidate;
}
}
fn escalated_narration_action_nudge(
attempt: usize,
cap: usize,
step_ledger: Option<&dyn scheduled::StepLedger>,
) -> String {
let step_clause = active_step_description(step_ledger)
.map(|step| format!(" Active step: '{step}'."))
.unwrap_or_default();
workflow_guidance(format!(
"Reminder {attempt}/{cap}: you again described an action without calling \
a tool, so nothing has happened.{step_clause} Your NEXT output must be \
exactly one tool call that starts that action (for example read_file, \
edit_file, or run_command with real arguments) — no prose before it. \
If you are blocked, state the one concrete blocker in a single \
sentence instead of announcing more intentions."
))
}
fn stale_file_ground_truth_nudge() -> String {
workflow_guidance(
"You claimed the file changed under you or that your edit context is stale, \
but you did not prove that with ground truth. Before stopping or asking the \
operator to restore/revert anything, run read-only verification: git status \
--short, git diff -- <file>, wc -l <file>, and re-read the exact target \
range. If those checks do not prove an actual concurrent change, continue \
from the verified file contents. Never recommend git checkout/revert unless \
git diff proves unwanted changes and the operator approves.",
)
}
fn workflow_classifier_text(messages: &[serde_json::Value], current_content: &str) -> String {
let mut parts = Vec::new();
let start = messages.len().saturating_sub(12);
for message in &messages[start..] {
if let Some(text) = message_text(message) {
if !text.trim().is_empty() {
parts.push(text);
}
}
}
if !current_content.trim().is_empty() {
parts.push(current_content.to_string());
}
parts.join("\n")
}
fn combine_nudge_hints(first: Option<&str>, second: Option<&str>) -> Option<String> {
let text = [first, second]
.into_iter()
.flatten()
.map(str::trim)
.filter(|hint| !hint.is_empty())
.collect::<Vec<_>>()
.join("\n\n");
(!text.is_empty()).then_some(text)
}
fn message_text(message: &serde_json::Value) -> Option<String> {
match message.get("content")? {
serde_json::Value::String(s) => Some(s.clone()),
serde_json::Value::Array(parts) => {
let text = parts
.iter()
.filter_map(|part| part.get("text").and_then(|text| text.as_str()))
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
}
_ => None,
}
}
fn pending_plan_completion_nudge(
step_ledger: Option<&dyn scheduled::StepLedger>,
needs_plan_update: bool,
workflow_hint: Option<&str>,
) -> Option<String> {
let snapshot = step_ledger?.snapshot();
let total = snapshot.steps.len();
if total == 0 {
return None;
}
let unfinished = snapshot
.steps
.iter()
.filter(|s| s.status != StepStatus::Done)
.count();
if unfinished == 0 {
return None;
}
let active = snapshot
.steps
.iter()
.position(|s| s.status == StepStatus::Active)
.or_else(|| {
snapshot
.steps
.iter()
.position(|s| s.status != StepStatus::Done)
});
let active_clause = active
.map(|index| format!(" Active step {}.", index + 1))
.unwrap_or_default();
let step_word = if unfinished == 1 { "step" } else { "steps" };
let workflow_clause = workflow_hint
.map(str::trim)
.filter(|hint| !hint.is_empty())
.map(|hint| format!("\n\n{hint}"))
.unwrap_or_default();
if needs_plan_update {
Some(workflow_guidance(format!(
"You ended with a findings/next-steps summary while the agent-maintained, advisory plan still has \
{unfinished}/{total} unfinished {step_word}.{active_clause} Your summary says \
immediate prerequisite repair work now blocks the active step. Call update_plan now \
with the full ordered plan: mark completed steps completed, make the immediate \
blocker repair the active step, and keep later feature work pending. Then call the \
next concrete tool for that active repair. Do not repeat the findings summary or \
claim a tool-round limit while this nudge is giving you another round.{workflow_clause}"
)))
} else {
Some(workflow_guidance(format!(
"You ended the turn while the agent-maintained, advisory plan still has {unfinished}/{total} unfinished \
{step_word}.{active_clause} Either call update_plan with completed steps marked \
completed, call the next tool for the active step, or state the concrete blocker. \
Do not hand off by only describing remaining work."
)))
}
}
fn read_only_action_nudge(
read_only_rounds: usize,
hard_rounds_remaining: Option<usize>,
step_ledger: Option<&dyn scheduled::StepLedger>,
delegate_hint: Option<&str>,
) -> String {
let plan_clause = if step_ledger.and_then(plan_reseat_pointer).is_some() {
" You have an active multi-step plan; continue the ACTIVE step when it still \
matches the operator's request."
} else {
""
};
let delegate_clause = delegate_hint
.map(|hint| format!(" {hint}"))
.unwrap_or_default();
let round_clause = hard_rounds_remaining
.map(|remaining| {
format!(
" After this round, at most {remaining} further tool rounds remain under the configured hard limit."
)
})
.unwrap_or_default();
workflow_guidance(format!(
"[{read_only_rounds} read-only rounds so far. Stop AIMLESS exploring and start \
making the change. This is a nudge, not a limit — you may still read, but if \
you have enough context, call edit_file or write_file now. {PERMISSION_RECOVERY_GUIDANCE} \
If you truly cannot proceed yet, state the \
exact blocker. Before edit_file, read the ONE file you are about to change so \
old_string matches exact text; never guess old_string or repeat a failed edit.\
{plan_clause}{delegate_clause}{round_clause}]"
))
}
fn strip_trailing_nudge_exchange(messages: &mut Vec<serde_json::Value>) {
let tail_is_nudge = messages.last().is_some_and(|m| {
m["role"] == "user"
&& m["content"]
.as_str()
.is_some_and(|c| c.starts_with(compress::LOOP_GUIDANCE_PREFIX))
});
if tail_is_nudge {
messages.pop(); if messages.last().is_some_and(|m| m["role"] == "assistant") {
messages.pop(); }
}
}
fn append_nudge_line(messages: &mut Vec<serde_json::Value>, line: &str) {
match messages.last_mut() {
Some(last) if last["role"] == "user" => {
let cur = last["content"].as_str().unwrap_or_default();
last["content"] = serde_json::Value::String(format!("{cur}\n\n{line}"));
}
_ => messages.push(serde_json::json!({"role": "user", "content": line})),
}
}
fn ollama_non_content_fields(json: &serde_json::Value) -> Vec<&'static str> {
let message = &json["message"];
["reasoning", "reasoning_content", "thinking"]
.into_iter()
.filter(|field| {
message[*field]
.as_str()
.map(|value| !value.trim().is_empty())
.unwrap_or(false)
})
.collect()
}
fn ollama_response_shape(json: &serde_json::Value) -> String {
let message = &json["message"];
let message_keys = message
.as_object()
.map(|obj| obj.keys().cloned().collect::<Vec<_>>().join(","))
.unwrap_or_else(|| "<missing>".to_string());
let content_chars = message["content"]
.as_str()
.map(|content| content.chars().count())
.unwrap_or(0);
let tool_calls = message["tool_calls"]
.as_array()
.map(|calls| calls.len())
.unwrap_or(0);
let non_content = ollama_non_content_fields(json);
let non_content = if non_content.is_empty() {
"none".to_string()
} else {
non_content.join(",")
};
format!(
"ollama response shape: message_keys=[{message_keys}] content_chars={content_chars} tool_calls={tool_calls} non_content_fields=[{non_content}] prompt_eval_count={} eval_count={}",
json["prompt_eval_count"]
.as_u64()
.map_or("missing".to_string(), |n| n.to_string()),
json["eval_count"]
.as_u64()
.map_or("missing".to_string(), |n| n.to_string())
)
}
fn suspicious_empty_retry_nudge(retry_index: u32, json: &serde_json::Value) -> String {
if retry_index == 0 {
return "Your previous response produced generated tokens but no assistant-visible content \
and no tool call. Reply with either a tool call or final assistant content."
.to_string();
}
let fields = ollama_non_content_fields(json);
let field_note = if fields.is_empty() {
"hidden/non-content fields".to_string()
} else {
format!("hidden/non-content field(s): {}", fields.join(", "))
};
format!(
"Your previous response again produced generated tokens only in {field_note}, \
with no assistant-visible content and no tool call. Hidden thinking is not an \
action. If you intend to act, emit the exact tool call now; otherwise reply \
with final assistant-visible content. Do not continue with hidden-only reasoning."
)
}
fn suspicious_empty_ollama_diagnostic(json: &serde_json::Value) -> String {
let fields = ollama_non_content_fields(json);
let field_note = if fields.is_empty() {
"no known non-content fields were present".to_string()
} else {
format!("non-content field(s) present: {}", fields.join(", "))
};
format!(
"(model generated output tokens but returned no assistant-visible content or tool calls; {field_note}; rerun with `newt --trace` to capture the response shape)"
)
}
fn cap_exit_nudge(max_tool_rounds: usize, progress: Option<&str>, observed: &[String]) -> String {
let mut nudge = format!(
"You have reached the tool-round limit ({max_tool_rounds} rounds). \
Do NOT call any more tools. Give the operator a concise progress update \
that separates completed and verified work, the current state or blocker, \
and remaining work. The tool loop has stopped at the cap. Report whether \
the operator's objective is complete or what remains; do not infer completion \
merely because tools are disabled. Cite only file paths \
that appear verbatim in the messages above — if the evidence you need \
was in the omitted messages, say so plainly instead of reconstructing \
file names or line numbers from memory. Do not narrate an intention to \
keep working now (for example, \"let me read/edit/verify\"); list those \
actions as remaining work and state that they have not run because the \
round cap stopped further tool calls. Do not reproduce literal `<plan>` \
or `<state>` tags from the working memory below. \
Report an ACTION (an edit made, a test run or passing, a branch created, a \
commit, a push, a PR opened) ONLY if a successful tool result above confirms \
it — if you did not see the tool result, the action did not happen; list it \
as remaining work instead. The workspace's real git state is checked against \
this summary."
);
if !observed.is_empty() {
nudge.push_str(
"\n\nFile paths actually observed in tool results this run \
(these exist — cite from this list):",
);
for p in observed {
nudge.push_str("\n- ");
nudge.push_str(p);
}
}
if let Some(p) = progress {
nudge.push_str(&format!("\n\nYour progress so far:\n{p}"));
}
nudge
}
fn cap_exit_tokens_hint(max_tool_rounds: usize, accumulated: Option<crate::TokenUsage>) -> String {
match accumulated {
Some(u) => format!(
" ({} in / {} out tokens consumed across {max_tool_rounds} rounds)",
u.input_tokens, u.output_tokens,
),
None => String::new(),
}
}
fn cap_exit_advice(max_tool_rounds: usize, wasted_calls: usize) -> &'static str {
if wasted_calls >= max_tool_rounds.max(1) {
"most of those rounds were spent on tool calls that failed — the model \
could not find a working edit/shell path, which is usually a tooling or \
permissions issue rather than too few rounds; check `newt doctor`"
} else {
"increase the tool-round limit for the next attempt, or ask a more focused question"
}
}
fn cap_exit_progress_block(label: &str, progress: Option<&str>) -> String {
match progress {
Some(p) => {
let body = format!("{label}:\n{}", humanize_cap_exit_progress(p));
format!("\n\n{}", body.replace('\n', " \n"))
}
None => String::new(),
}
}
fn humanize_cap_exit_progress(progress: &str) -> String {
progress
.replace("<plan>", "Plan:\n")
.replace("</plan>", "")
.replace("<state>", "Current state:\n")
.replace("</state>", "")
.trim()
.to_string()
}
fn cap_exit_fallback(
max_tool_rounds: usize,
accumulated: Option<crate::TokenUsage>,
wasted_calls: usize,
progress: Option<&str>,
) -> FinalReply {
let tokens_hint = cap_exit_tokens_hint(max_tool_rounds, accumulated);
let advice = cap_exit_advice(max_tool_rounds, wasted_calls);
let salvaged = cap_exit_progress_block("Progress captured before the summary failed", progress);
FinalReply::harness(format!(
"Paused at the tool-round limit ({max_tool_rounds} rounds){tokens_hint}. \
The final summarization request also failed while preparing a progress \
update; {advice}.{salvaged}"
))
}
fn cap_exit_progress_handoff(
max_tool_rounds: usize,
accumulated: Option<crate::TokenUsage>,
content: &str,
has_pending_actions: bool,
progress: Option<&str>,
) -> FinalReply {
let tokens_hint = cap_exit_tokens_hint(max_tool_rounds, accumulated);
let pending = if has_pending_actions {
" The proposed next actions in this update have not run yet."
} else {
""
};
let captured = cap_exit_progress_block("Captured working state", progress);
FinalReply::from(content.to_owned()).with_notice(format!(
"The tool loop reached the tool-round limit ({max_tool_rounds} rounds){tokens_hint}. \
This progress handoff preserves the model's update.{pending}{captured}"
))
}
fn cap_exit_summary_is_action_handoff(content: &str) -> bool {
crate::NudgeClassifier::load_default()
.classify(content)
.is_pending_action()
|| looks_like_unverified_stale_file_blocker(content)
}
fn cap_exit_model_reply(
max_tool_rounds: usize,
accumulated: Option<crate::TokenUsage>,
content: &str,
progress: Option<&str>,
) -> FinalReply {
let pending = cap_exit_summary_is_action_handoff(content);
if pending || progress.is_some() {
cap_exit_progress_handoff(max_tool_rounds, accumulated, content, pending, progress)
} else {
FinalReply::from(content.to_string())
}
}
fn finalize_final_text(
text: impl Into<FinalReply>,
workspace: &str,
read_scope: &crate::Scope<String>,
capability_evidence: &capability_check::Evidence,
disclosure: Option<&crate::ocap::DisclosureFilter>,
turn_claims: &claim_check::TurnClaims<'_>,
verification: &self_verify::VerificationLedger,
) -> String {
let mut reply = text.into();
let source_draft = redact_model_facing(disclosure, reply.model.clone());
reply.model = observed_report::assistant_prose(&source_draft);
reply.annotation(capability_check::annotate_unobserved_probe(
reply.model.clone(),
capability_evidence,
));
reply.annotation(turn_claims.annotate(
reply.model.clone(),
workspace,
verification.claim_directories(),
read_scope,
));
reply.annotation(verification.annotate_cargo_claim(reply.model.clone()));
reply.annotation(verification.annotate_pr_claim(reply.model.clone()));
reply.annotation(verification.annotate_push_claim(reply.model.clone()));
turn_claims.set_notices(workspace, read_scope, &reply.notices);
redact_model_facing(
disclosure,
turn_claims.final_report(
workspace,
read_scope,
&reply.model,
&source_draft,
disclosure,
),
)
}
struct CapExit {
max_tool_rounds: usize,
accumulated: Option<crate::TokenUsage>,
wasted_calls: usize,
progress: Option<String>,
observed: Vec<String>,
request_budget: Option<usize>,
calibration: f32,
estimation: crate::tokens::TokenEstimation,
ollama_options: Option<serde_json::Value>,
prompt_measurement: std::sync::Mutex<Option<context_recovery::PromptMeasurement>>,
fell_back: std::sync::atomic::AtomicBool,
}
impl CapExit {
fn push_nudge(&self, messages: &mut Vec<serde_json::Value>) {
messages.push(serde_json::json!({
"role": "user",
"content": cap_exit_nudge(self.max_tool_rounds, self.progress.as_deref(), &self.observed),
}));
}
fn fits(&self, messages: &[serde_json::Value], model: &str) -> bool {
preflight_full_message_request(
messages,
None,
self.request_budget,
self.calibration,
self.estimation,
model,
)
.is_ok()
}
fn fallback(&self) -> (FinalReply, bool, Option<crate::TokenUsage>) {
self.fell_back
.store(true, std::sync::atomic::Ordering::Relaxed);
(
cap_exit_fallback(
self.max_tool_rounds,
self.accumulated,
self.wasted_calls,
self.progress.as_deref(),
),
false,
self.accumulated,
)
}
fn record_measurement(
&self,
measurement: context_recovery::PromptMeasurement,
) -> anyhow::Result<()> {
let mut observed = self
.prompt_measurement
.lock()
.map_err(|_| anyhow::anyhow!("optional prompt measurement lock poisoned"))?;
if observed
.as_ref()
.is_none_or(|previous| measurement.tokens > previous.tokens)
{
*observed = Some(measurement);
}
Ok(())
}
fn learn_measurement(&self, state: &mut CompressState) -> anyhow::Result<()> {
if let Some(measurement) = *self
.prompt_measurement
.lock()
.map_err(|_| anyhow::anyhow!("optional prompt measurement lock poisoned"))?
{
measurement.learn(state);
}
Ok(())
}
fn recover_rejection(
&self,
result: anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)>,
observations: &mut Option<&mut observability::SolveObservation>,
state: &mut CompressState,
round: usize,
attempt: u32,
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)> {
self.learn_measurement(state)?;
if let Err(error) = &result {
if let Some(rejection) = error.downcast_ref::<context_recovery::OptionalRejection>() {
context_recovery::terminal_optional(
observations,
state,
self.calibration,
round,
attempt,
rejection.estimated_tokens,
)?;
observability::observe_harness_reply(observations);
return Ok(self.fallback());
}
}
if self.fell_back.load(std::sync::atomic::Ordering::Relaxed) {
observability::observe_harness_reply(observations);
}
result
}
async fn finish<Fut>(
&self,
endpoint: &str,
attempts: Option<attempt_capture::AttemptScope<'_>>,
request: impl Fn() -> Fut,
http_error_prefix: &str,
estimated_tokens: usize,
extract: impl FnOnce(serde_json::Value) -> (String, Option<crate::TokenUsage>),
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)>
where
Fut: std::future::Future<Output = anyhow::Result<reqwest::RequestBuilder>>,
{
self.finish_with_decoder(
endpoint,
attempts,
request,
http_error_prefix,
estimated_tokens,
|bytes| Ok(serde_json::from_slice(bytes)?),
extract,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn finish_with_decoder<Fut>(
&self,
endpoint: &str,
attempts: Option<attempt_capture::AttemptScope<'_>>,
request: impl Fn() -> Fut,
http_error_prefix: &str,
estimated_tokens: usize,
decode: fn(&[u8]) -> anyhow::Result<serde_json::Value>,
extract: impl FnOnce(serde_json::Value) -> (String, Option<crate::TokenUsage>),
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)>
where
Fut: std::future::Future<Output = anyhow::Result<reqwest::RequestBuilder>>,
{
let retry = tui_retry_policy(endpoint);
let result = dispatch_with_decoder(
&retry,
attempts,
request,
http_error_prefix,
|_, _, _| {},
None,
decode,
)
.await;
let result = match result {
Err(error)
if crate::retry::classify(&error)
== crate::retry::Retryability::ContextExceeded =>
{
return Err(error.context(context_recovery::OptionalRejection { estimated_tokens }));
}
other => other,
};
if let Err(error) = &result {
if error.downcast_ref::<generation_bounds::Stopped>().is_some() {
let (mut text, streamed, usage) = self.fallback();
generation_bounds::append_notice(&mut text, error);
return Ok((
text,
streamed,
merge_round_usage(usage, observability::reported_usage(error)),
));
}
}
if let Ok((json, attempt)) = result {
let limited = generation_bounds::output_limited(&json);
let (content, usage) = extract(json);
attempt_capture::complete(attempt.as_ref(), usage);
let total = merge_round_usage(self.accumulated, usage);
if limited {
return Ok((
generation_bounds::output_limit_notice(content, usage),
false,
total,
));
}
if !content.is_empty() {
return Ok((
cap_exit_model_reply(
self.max_tool_rounds,
self.accumulated,
&content,
self.progress.as_deref(),
),
false,
total,
));
}
}
Ok(self.fallback())
}
}
fn ollama_options(num_ctx: Option<u32>, num_predict: Option<u32>) -> Option<serde_json::Value> {
let mut options = serde_json::Map::new();
if let Some(num_ctx) = num_ctx {
options.insert("num_ctx".into(), num_ctx.into());
}
if let Some(num_predict) = num_predict {
options.insert("num_predict".into(), num_predict.into());
}
(!options.is_empty()).then_some(serde_json::Value::Object(options))
}
async fn final_summary_ollama(
client: &reqwest::Client,
chat_url: &str,
model: &str,
mut messages: Vec<serde_json::Value>,
cap: &CapExit,
attempts: Option<attempt_capture::AttemptScope<'_>>,
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)> {
cap.push_nudge(&mut messages);
if !cap.fits(&messages, model) {
return Ok(cap.fallback());
}
let mut body = serde_json::json!({
"model": model,
"messages": &messages,
"stream": false,
});
if let Some(options) = &cap.ollama_options {
body["options"] = options.clone();
}
cap.finish(
chat_url,
attempts,
|| async { Ok(client.post(chat_url).json(&body)) },
"Ollama",
estimate_tokens(&messages, cap.estimation),
|json| {
let (content, _reasoning) = crate::reasoning::split_reasoning(
json["message"]["content"].as_str().unwrap_or(""),
);
(content, ollama_usage(&json))
},
)
.await
}
fn openai_chat_wire_messages(
messages: &[serde_json::Value],
) -> anyhow::Result<Vec<serde_json::Value>> {
agent_harness::render::openai_chat_wire_messages(messages).map_err(|error| match error {
agent_harness::Error::Proposal(message) => anyhow::anyhow!(message),
error => error.into(),
})
}
fn prepare_openai_assistant_replay(
message: &serde_json::Value,
clean_content: &str,
replay_scope: crate::model_card::ReasoningReplayScope,
current_user_turn: bool,
) -> serde_json::Value {
let mut assistant = message.clone();
if assistant["role"].as_str().is_none() {
assistant["role"] = serde_json::Value::String("assistant".into());
}
let keep_reasoning = replay_scope == crate::model_card::ReasoningReplayScope::FullHistory
|| (replay_scope == crate::model_card::ReasoningReplayScope::CurrentUserTurn
&& current_user_turn);
if !keep_reasoning {
assistant["content"] = serde_json::Value::String(clean_content.to_string());
if let Some(object) = assistant.as_object_mut() {
object.remove("reasoning_content");
}
}
sanitize_replayed_tool_call_arguments(&mut assistant);
assistant
}
fn sanitize_replayed_tool_call_arguments(assistant: &mut serde_json::Value) {
let Some(calls) = assistant["tool_calls"].as_array_mut() else {
return;
};
for call in calls {
let Some(args) = call["function"]["arguments"].as_str() else {
continue;
};
let trimmed = args.trim();
if trimmed.is_empty() || serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
continue;
}
call["function"]["arguments"] = serde_json::Value::String("{}".to_string());
}
}
async fn count_openai_request(
client: &reqwest::Client,
chat_url: &str,
api_key: Option<&str>,
body: &serde_json::Value,
budget: Option<usize>,
) -> anyhow::Result<Option<crate::backend_probe::TokenCount>> {
if budget.is_none() {
return Ok(None);
}
let endpoint = chat_url
.strip_suffix("/v1/chat/completions")
.ok_or_else(|| anyhow::anyhow!("invalid Chat Completions endpoint for token admission"))?;
let model = body["model"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("token admission requires the assembled request model"))?;
crate::backend_probe::count_chat_tokens(client, endpoint, model, api_key, body).await
}
fn enforce_counted_budget(
count: Option<&crate::backend_probe::TokenCount>,
budget: Option<usize>,
estimated_tokens: usize,
) -> anyhow::Result<()> {
if let (Some(count), Some(budget)) = (count, budget) {
if count.tokens > budget {
let error = observability::DispatchError::context_exceeded(format!(
"pre-dispatch: Context size has been exceeded: measured {} input tokens \
exceed the {budget}-token input budget",
count.tokens,
));
return Err(
anyhow::Error::new(error).context(context_recovery::PromptMeasurement {
tokens: count.tokens,
estimated_tokens,
}),
);
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn final_summary_openai(
clients: (&reqwest::Client, &reqwest::Client),
chat_url: &str,
model: &str,
api_key: Option<&str>,
mut messages: Vec<serde_json::Value>,
generation_policy: generation_policy::GenerationPolicy,
context_window: Option<u32>,
cap: &CapExit,
attempts: Option<attempt_capture::AttemptScope<'_>>,
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)> {
let (count_client, stream_client) = clients;
cap.push_nudge(&mut messages);
let messages = openai_chat_wire_messages(&messages)?;
if !cap.fits(&messages, model) {
return Ok(cap.fallback());
}
let mut body = serde_json::json!({
"model": model,
"messages": &messages,
"stream": true,
"stream_options": {"include_usage": true},
});
generation_policy.apply_to_chat_completions_body(&mut body);
generation_bounds::apply_chat(
&mut body,
chat_url,
generation_policy.output_allowance,
context_window,
cap.request_budget.unwrap_or_else(|| {
calibrate_up(estimate_tokens(&messages, cap.estimation), cap.calibration)
}),
)?;
cap.finish_with_decoder(
chat_url,
attempts,
|| async {
let count =
count_openai_request(count_client, chat_url, api_key, &body, cap.request_budget)
.await?;
let estimated_tokens = estimate_tokens(&messages, cap.estimation);
if let Some(count) = &count {
cap.record_measurement(context_recovery::PromptMeasurement {
tokens: count.tokens,
estimated_tokens,
})?;
}
enforce_counted_budget(count.as_ref(), cap.request_budget, estimated_tokens)?;
let mut req = stream_client.post(chat_url).json(&body);
if let Some(key) = api_key {
req = req.bearer_auth(key);
}
Ok(req)
},
"inference endpoint",
estimate_tokens(&messages, cap.estimation),
smart_harness::decode_openai_response,
|json| {
let (content, _reasoning) = crate::reasoning::split_reasoning(
json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or(""),
);
(content, openai_usage(&json["usage"]))
},
)
.await
}
pub async fn openai_chat_complete(
ctx: ChatCtx<'_>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
openai_chat_complete_with_prompt(ctx, None, None, mcp).await
}
pub async fn openai_chat_complete_with_prompt(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
openai_chat_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
None,
None,
mcp,
)
.await
}
async fn openai_chat_complete_with_prompt_and_artifacts(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
artifact_source: Option<&dyn artifact_read::ArtifactSource>,
artifact_sink: Option<&dyn artifact_read::PromptArtifactSink>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
let ChatCtx {
shell_dialect,
command_budget,
verify_outcomes,
smart_harness,
url,
model,
kind: _,
api_key,
messages: mem_messages,
task,
workspace,
default_command_cwd,
worktree_session,
color,
markdown: _,
tool_offload,
spill_store,
disclosure,
compaction_store,
scratchpad,
scratchpad_store,
code_search,
where_is,
nav,
exposure,
experience_store,
step_ledger,
caveats,
persona_tools,
cognition,
output_allowance,
overflow_retry: _,
attempt_ledger,
chat_completions_capability,
responses_capability: _,
openai_api: _,
reasoning_replay_scope,
max_tool_rounds,
workflow_grace_rounds,
narration_nudge_cap,
action_nudges,
prompt_disposition,
prompt_intake,
tool_output_lines,
debug,
trace,
num_ctx,
connect_timeout_secs,
inference_timeout_secs,
mid_loop_trim_threshold,
compaction_trigger_policy,
mid_loop_trim_tokens,
max_ok_input,
build_check_cmd,
safe_context,
recover_cw_400,
mut note_sink,
mut note_nudge,
recall_source,
memory_source,
summarizer,
compress_state,
mut tool_events,
mut phantom_reaches,
mut end_reason,
mut round_cap_hit,
mut solve_obs,
mut permission_gate,
mut on_round_usage,
estimate_ratio,
estimation,
summary_input_cap_floor_chars,
rewrites_history,
input_ceiling_pct,
low_budget_pct,
exec_floor,
write_ledger,
attribution,
cancel,
run_allowance,
live_tool_output,
git_tool,
crew_runner,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
steering,
completed_spill_renderer,
emits_leading_reasoning,
} = ctx;
let _completed_spill_guard = CompletedSpillDismissGuard(&completed_spill_renderer);
let action_nudges = action_nudges && prompt_disposition == PromptDisposition::Act;
if let Some(harness) = smart_harness {
harness.start_turn()?;
}
let spill_store = smart_harness.map(|h| h.spill_store()).or(spill_store);
let tool_offload = tool_offload || smart_harness.is_some();
let smart_verify = action_nudges && prompt_disposition == PromptDisposition::Act;
let action_nudges = action_nudges && smart_harness.is_none();
let max_tool_rounds = prompt_disposition.tool_round_limit(max_tool_rounds);
let mut generation_window = num_ctx;
let generation_policy = generation_policy::GenerationPolicy::resolve(
cognition,
output_allowance,
chat_completions_capability,
reasoning_replay_scope,
);
observability::observe_output_allowance(
&mut solve_obs,
generation_policy.output_allowance,
generation_policy.max_output_tokens.is_some(),
);
let reasoning_replay_scope = generation_policy.reasoning_replay_scope;
let mut local_compress_state = CompressState::new();
let compress_state = match compress_state {
Some(s) => s,
None => &mut local_compress_state,
};
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let stream_client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.read_timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let chat_url = format!("{}/v1/chat/completions", url.trim_end_matches('/'));
let retry = tui_retry_policy(url);
let advertise_save_note = note_sink.is_some();
let advertise_recall = recall_source.is_some();
let advertise_memory_fetch = memory_source.is_some();
let advertise_scratchpad = scratchpad_store.is_some() && scratchpad;
let advertise_code_search = code_search.is_some();
let advertise_experiential = experience_store.is_some();
let advertise_scheduled = step_ledger.is_some();
let advertise_git = false;
let advertise_team = crew_runner.is_some();
let advertise_operating_mode = operating_mode_control.is_some();
let advertise_plan_mode = plan_mode_control.is_some();
let advertise_plan_mode_active =
plan_mode_control.is_some_and(|control| control.is_plan_mode());
let mem_messages = observed_report::replay_messages(mem_messages);
let mut messages: Vec<serde_json::Value> = mem_messages
.iter()
.map(|m| {
let content = if m.role == crate::Role::Assistant
&& reasoning_replay_scope != crate::model_card::ReasoningReplayScope::FullHistory
{
crate::reasoning::split_reasoning(&m.content).0
} else {
m.content.clone()
};
serde_json::json!({"role": m.role.as_str(), "content": content})
})
.collect();
if let Some(harness) = smart_harness {
messages = harness.initial_messages(messages)?;
}
if let (Some(session), Some(context)) = (worktree_session, turn_prompt_context) {
session
.report
.lock()
.expect("report state")
.begin(context.active().root_prompt_id());
}
let ephemeral_prompt = prompt_read::headless_prompt_fallback(turn_prompt_context, task);
let turn_prompt_context = turn_prompt_context.or(ephemeral_prompt.as_ref());
let prompt_context =
prompt_read::PromptReadContext::new(turn_prompt_context, task, prompt_source);
let artifact_context = turn_prompt_context
.map(|turn| artifact_read::ArtifactReadContext::from_turn(turn, artifact_source));
let active_task = prompt_context.active_text();
prompt_read::ensure_active_prompt_card(&mut messages, prompt_context, prompt_intake);
let attempt_turn = turn_prompt_context
.map(|turn| turn.active_operator_prompt().id().to_string())
.unwrap_or_default();
let fallback_ledger = std::sync::Mutex::new(crate::attempts::AttemptLedger::default());
let attempt_ledger = attempt_ledger.or_else(|| run_allowance.map(|_| &fallback_ledger));
let attempts = attempt_ledger.map(|ledger| attempt_capture::AttemptScope {
ledger,
turn: &attempt_turn,
model,
backend: url,
cancel,
run_allowance,
});
if note_sink.is_some() {
if let Some(line) = note_nudge.as_deref_mut().and_then(NoteNudge::begin_turn) {
if let Some(harness) = smart_harness {
harness.host_message(&line)?;
messages.push(serde_json::json!({"role":"user", "content":line}));
} else {
append_nudge_line(&mut messages, &line);
}
}
}
let mut accumulated_usage: Option<crate::TokenUsage> = None;
let mut hallucination_count: u32 = 0;
let result_aware = self_verify::enabled() && verify_outcomes;
let mut repeat_calls = RepeatCallGuard::default();
let plan_at_turn_start = step_ledger
.map(|ledger| ledger.snapshot())
.unwrap_or_default();
let mut read_history = tools::ReadHistory::default();
let mut verification = self_verify::VerificationLedger::for_turn(task, result_aware);
let turn_claims =
claim_check::TurnClaims::capture(workspace, &caveats.fs_read, worktree_session)
.with_evidence(observed_report::review::Evidence {
sink: artifact_sink,
context: artifact_context,
spill: spill_store,
});
let mut clean_build = crate::loop_watch::CleanBuildWatch::default();
let mut cw_retries: u32 = 0;
let mut output_recovery = output_limit_recovery::Recovery::default();
let mut tools_supported = true;
let mut tools_unsupported_notified = false;
let mut effective_input_ceiling = num_ctx_input_ceiling(
num_ctx,
input_ceiling_pct,
generation_policy.output_allowance,
);
let mut send_budget: Option<usize> =
initial_send_budget(max_ok_input, safe_context, effective_input_ceiling);
let mut send_budget_authoritative = safe_context.is_some() || effective_input_ceiling.is_some();
let tools = merged_tool_definitions(
mcp,
advertise_save_note,
advertise_recall,
advertise_memory_fetch,
advertise_git.then_some(&caveats.fs_read),
advertise_team,
advertise_scratchpad,
advertise_code_search,
advertise_experiential,
advertise_scheduled,
advertise_operating_mode,
advertise_plan_mode,
advertise_plan_mode_active,
command_budget,
shell_dialect,
);
let tools = filter_advertised_tools(tools, persona_tools);
let tools = filter_tools_for_disposition(
smart_harness::advertise(tools, smart_harness),
prompt_disposition,
);
let (tools, hidden_tools) = crate::agentic::tools::select_exposed(
tools,
&exposure,
exposure_budget_tokens(send_budget, safe_context),
&std::collections::BTreeSet::new(),
estimation,
);
let mut tools = crate::agentic::tools::select_openai_compatible_tools(tools, &hidden_tools);
let mut tool_tokens = estimate_value_tokens(&tools, estimation);
let mut cal = compress_state.calibration.ratio(estimate_ratio);
let mut tool_tokens_real = calibrate_up(tool_tokens, cal);
preflight_irreducible_request(
&messages,
Some(&tools),
authoritative_request_budget(send_budget, send_budget_authoritative, mid_loop_trim_tokens),
cal,
estimation,
model,
)?;
let mut prompt_tracker = PromptTracker::new();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let mut observed_paths = claim_check::ObservedPaths::default();
let mut observed_resolver = claim_check::workspace_resolver(workspace);
let mut narration_nudges: usize = 0;
let mut readonly_completion_retried = false;
let mut self_verify_nudges: usize = 0;
let mut verification_nudges: usize = 0;
const SELF_VERIFY_CAP: usize = 2;
let mut pending_plan_nudges: usize = 0;
let mut stale_file_nudges: usize = 0;
let mut unverified_exec_blocker_nudges: usize = 0;
let mut run_command_denial_observed = false;
let mut pending_rerun: Option<tools::PendingRerun> = None;
let mut capability_evidence = capability_check::Evidence::default();
let mut probe_correction_used = false;
let nudge_classifier = crate::NudgeClassifier::load_default();
let action_turn = action_nudges && crate::classifiers::user_turn_invites_action(active_task);
let exec_grounding_turn = action_nudges && prompt_disposition == PromptDisposition::Act;
let workflow_steerer = crate::WorkflowSteerer::load_default();
let mut workflow_runtime = WorkflowRuntimeState {
initiative: crate::initiative::effective_initiative(),
no_progress: crate::initiative::installed_no_progress(),
..Default::default()
};
workflow_runtime.set_progress_horizon(
workflow_steerer.progress_horizon(&workflow_classifier_text(&messages, "")),
);
let mut current_tool_round_limit = max_tool_rounds;
let turn_started = std::time::Instant::now();
let mut turn_heartbeat = TurnHeartbeat::default();
let mut uncorrelatable_batches: u32 = 0;
let mut selected_source = selected_target::Projection::default();
let mut observed_facts = semantic_pins::Projection::default();
'round_loop: for round in 0..usize::MAX {
let round = round.saturating_sub(output_recovery.skipped_rounds);
selected_source.refresh(
&mut messages,
step_ledger,
workspace,
&caveats.fs_read,
worktree_session,
false,
);
observed_facts.planning = output_budget::guidance(
Some(generation_bounds::output_cap(
generation_policy.output_allowance,
None,
0,
)?),
step_ledger,
&selected_target::root(workspace, worktree_session),
&caveats.fs_read,
estimation,
);
semantic_pins::refresh(
&mut observed_facts,
smart_harness,
&mut messages,
prompt_context,
selected_source.current(),
&turn_claims,
workspace,
&caveats.fs_read,
)?;
let round_policy = generation_policy;
if hidden_tools.append_promoted(&mut tools) {
tool_tokens = estimate_value_tokens(&tools, estimation);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
let turn_elapsed = turn_started.elapsed();
if let Some(line) = turn_heartbeat.notice(turn_elapsed, round, current_tool_round_limit) {
print_newt(&line, color, false);
}
dismiss_completed_spill(&completed_spill_renderer);
if !workflow_runtime.admit_round(
round,
&mut current_tool_round_limit,
workflow_grace_rounds,
) {
break;
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
if drain_steering_into(steering, &mut messages, color) > 0 {
workflow_runtime.note_operator_steering();
}
if round > 0 && color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print("…\n"),
ResetColor
)
.ok();
}
if round > 0 {
no_progress_gate!(
workflow_runtime,
action_nudges,
messages,
end_reason,
solve_obs,
accumulated_usage,
hallucination_count,
smart_harness,
(
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification
),
|harness| harness.record_messages(&messages)?
);
}
if round > 0 && action_nudges {
if let Some(ptr) = step_ledger.and_then(plan_reseat_pointer) {
messages.push(serde_json::json!({ "role": "user", "content": ptr }));
}
let remaining = (workflow_grace_rounds == 0)
.then_some(current_tool_round_limit.saturating_sub(round + 1));
if let Some(nudge) = workflow_runtime.action_forcing_nudge(remaining, step_ledger, None)
{
if debug {
print_debug(
&format!(
"initiative[{}]: forcing action, read-only budget spent (round {round})",
workflow_runtime.initiative
),
color,
);
}
messages.push(serde_json::json!({ "role": "user", "content": nudge }));
}
}
{
let advertised_tools = tools_supported.then_some(&tools);
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
let wire_messages = openai_chat_wire_messages(&messages)?;
let current = full_message_request_pressure_tokens(
prompt_tracker.current(&messages, advertised_tools, cal, estimation),
&wire_messages,
advertised_tools,
cal,
estimation,
);
let message_tokens = estimate_tokens(&messages, estimation);
let has_authoritative_headroom = count_guard_has_headroom(
current,
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
max_ok_input,
);
let reasoning_tail_len =
compress::protected_reasoning_tail_len(&messages, reasoning_replay_scope);
if let Some(trigger) = compression_trigger(
compress::compression_message_count(&messages, reasoning_tail_len),
current,
message_tokens,
CompressionTriggerLimits {
count_threshold: mid_loop_trim_threshold,
token_threshold: mid_loop_trim_tokens,
send_budget,
tool_tokens: tool_tokens_real,
policy: compaction_trigger_policy,
has_authoritative_headroom,
},
) {
let pipeline_budget = if trigger.hard_budget {
calibrate_down(trigger.budget, cal)
} else {
trigger.budget
};
let token_fired = mid_loop_trim_tokens.is_some_and(|t| t > 0 && current > t);
let compression = smart_harness::compress(
CompressRequest {
messages: &messages,
budget: pipeline_budget,
max_messages: if reasoning_tail_len > 0 {
None
} else {
trigger.max_messages
},
replay_protected_tail_len: reasoning_tail_len,
task: active_task,
hard_budget: trigger.hard_budget,
authoritative: token_fired || send_budget_authoritative,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = outcome?;
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused {
anyhow::bail!("{}", refusal_bail_message(outcome.refusal, current, model));
}
if outcome.fired {
let suffix = if trigger.hard_budget && outcome.tokens_after > pipeline_budget {
", still over budget"
} else {
""
};
emit_compression_notice(
color,
outcome.tokens_before,
outcome.tokens_after,
outcome.action,
suffix,
);
if debug {
print_debug(
&format!(
"compression: {} → {} messages (budget ~{} tokens, \
+~{tool_tokens} tool-schema tokens ride along)",
messages.len(),
outcome.messages.len(),
pipeline_budget,
),
color,
);
}
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
openai_chat_wire_messages(candidate).is_ok_and(|wire| {
preflight_full_message_request(
&wire,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
})
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
pipeline_budget,
round,
trigger.primary_cause.artifact_reason(),
Some(&trigger),
send_budget_authoritative,
compress_state.floor_trend(),
color,
);
}
}
}
let mut thought_for;
let (json, round_est_raw): (serde_json::Value, usize) = loop {
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
let wire_messages = openai_chat_wire_messages(&messages)?;
preflight_full_message_request(
&wire_messages,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)?;
let round_est_raw = estimate_request_tokens(
&wire_messages,
tools_supported.then_some(&tools),
estimation,
);
let mut body = serde_json::json!({
"model": model,
"messages": wire_messages,
"tools": tools.clone(),
"tool_choice": "auto",
"stream": true,
"stream_options": {"include_usage": true},
});
round_policy.apply_to_chat_completions_body(&mut body);
if !tools_supported {
if let Some(o) = body.as_object_mut() {
o.remove("tools");
o.remove("tool_choice");
}
}
let request_budget = authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
);
let output_cap = generation_bounds::apply_chat(
&mut body,
&chat_url,
round_policy.output_allowance,
generation_window,
request_budget.unwrap_or_else(|| calibrate_up(round_est_raw, cal)),
)?;
observability::observe_output_allowance(&mut solve_obs, Some(output_cap), true);
let measured_prompt_tokens = std::sync::atomic::AtomicUsize::new(0);
let admission_commit_failure = std::sync::OnceLock::new();
let max_attempts = retry.max_retries.saturating_add(1);
let attempts_started = std::cell::Cell::new(0u32);
let spinner_label =
inference_progress_label(model, 1, max_attempts, inference_timeout_secs);
let spinner = if thinking_stream_enabled() {
crate::tty::Spinner::start(&spinner_label, crate::tty::Sink::Stdout, color)
} else {
None
};
let dispatch = with_backoff_notify_error(
&retry,
|| {
let attempt = attempts_started.get().saturating_add(1);
attempts_started.set(attempt);
if let Some(spinner) = spinner.as_ref() {
spinner.set_label(&inference_progress_label(
model,
attempt,
max_attempts,
inference_timeout_secs,
));
}
async {
let mut req = smart_harness::request_with_messages(
stream_client.post(&chat_url),
&body,
smart_harness,
"openai",
&messages,
)?;
if let Some(key) = api_key {
req = req.bearer_auth(key);
}
let counted = count_openai_request(
&client,
&chat_url,
api_key,
&body,
request_budget,
)
.await;
if let (Some(harness), Some(budget)) = (smart_harness, request_budget) {
let committed = match counted.as_ref() {
Ok(Some(count)) => harness.record_token_count(Ok(count), budget),
Err(error) => harness.record_token_count(Err(error), budget),
Ok(None) => Ok(()),
};
if let Err(error) = committed {
admission_commit_failure.get_or_init(|| error);
anyhow::bail!("token-count evidence could not be committed");
}
}
let counted = counted?;
if let Some(count) = &counted {
measured_prompt_tokens
.fetch_max(count.tokens, std::sync::atomic::Ordering::Relaxed);
}
enforce_counted_budget(counted.as_ref(), request_budget, round_est_raw)?;
let (resp, attempt) =
attempt_capture::send(attempts, "primary", req, "request failed")
.await?;
let json = smart_harness::response_with_decoder(
resp,
smart_harness,
"inference endpoint",
smart_harness::decode_openai_response,
)
.await
.inspect_err(|error| {
attempt_capture::failed(attempt.as_ref(), error);
})?;
attempt_capture::complete(attempt.as_ref(), openai_usage(&json["usage"]));
Ok(json)
}
},
|attempt, delay, error| {
if let Some(spinner) = spinner.as_ref() {
spinner.set_label(&format!(
"retry {attempt}/{} in {:.1}s…",
retry.max_retries,
delay.as_secs_f32(),
));
}
print_retry_indicator(attempt, retry.max_retries, delay, error, color);
},
);
let dispatch = cancellable(cancel, dispatch).await;
thought_for = spinner
.as_ref()
.map_or(std::time::Duration::ZERO, crate::tty::Spinner::elapsed);
drop(spinner);
if let Some(error) = admission_commit_failure.into_inner() {
return Err(error);
}
let measured = measured_prompt_tokens.load(std::sync::atomic::Ordering::Relaxed);
if measured > 0 {
compress_state
.calibration
.observe_count(measured, round_est_raw);
cal = compress_state.calibration.ratio(estimate_ratio);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
let Some(dispatch) = dispatch else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
match dispatch {
Ok(j) => {
cw_retries = 0;
break (j, round_est_raw);
}
Err(e) => {
if tools_supported && is_tools_unsupported_error(&e) {
tools_supported = false;
if !tools_unsupported_notified {
tools_unsupported_notified = true;
print_newt(
&format!(
"{model} does not support tools — tools disabled for this session"
),
color,
false,
);
}
continue;
}
let overflow =
crate::retry::classify(&e) == crate::retry::Retryability::ContextExceeded;
let refused_tokens = cw_overflow::parse_context_window_error(&e.to_string())
.map(|(tokens, _)| usize::try_from(tokens).unwrap_or(usize::MAX))
.unwrap_or_else(|| calibrate_up(round_est_raw, cal));
if overflow {
cal = compress_state.calibration.overflow(cal);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
if cw_retries < 2 {
let recovered_window = recover_cw_400.and_then(|f| f(&e, model, &today));
if let Some(recovered_budget) = recovered_window
.map(|context_window| {
recovered_input_budget(
context_window,
input_ceiling_pct,
round_policy.output_allowance,
effective_input_ceiling,
)
})
.or_else(|| request_budget.filter(|budget| measured > *budget))
.or_else(|| {
cw_overflow::core_recover_overflow(
&e.to_string(),
send_budget,
None,
)
.map(|cap| cap as usize)
})
.or_else(|| {
overflow.then_some(
send_budget.unwrap_or_else(|| calibrate_up(round_est_raw, cal)),
)
})
.and_then(|budget| {
cw_overflow::recovery_target(
budget,
refused_tokens,
effective_input_ceiling,
)
})
{
if let Some(context_window) = recovered_window {
generation_window = Some(
generation_window
.map_or(context_window, |old| old.min(context_window)),
);
emit_context_window_400(&mut on_round_usage, context_window);
}
let new_budget = effective_input_ceiling
.map_or(recovered_budget, |c| recovered_budget.min(c));
let new_budget = authoritative_request_budget(
Some(new_budget),
true,
mid_loop_trim_tokens,
)
.unwrap_or(new_budget);
emit_overflow_notice(
color,
&display::OverflowReason::Refused {
request_estimate: Some(
refused_tokens.min(u32::MAX as usize) as u32
),
window: recovered_window,
trim_to: new_budget.min(u32::MAX as usize) as u32,
},
model,
cw_retries + 1,
);
send_budget = Some(new_budget);
effective_input_ceiling = Some(new_budget);
send_budget_authoritative = true;
let compression = context_recovery::compress(
CompressRequest {
messages: &messages,
budget: context_recovery::target(
&messages,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
estimation,
),
max_messages: None,
replay_protected_tail_len:
compress::protected_reasoning_tail_len(
&messages,
reasoning_replay_scope,
)
.max(context_recovery::protected_tool_tail(&messages)),
task: active_task,
hard_budget: true,
authoritative: true,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e.context(format!(
"context re-projection failed: {error}"
)));
}
};
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused
|| !outcome.fired
|| outcome.tokens_after >= outcome.tokens_before
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
if outcome.fired {
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
openai_chat_wire_messages(candidate).is_ok_and(|wire| {
preflight_full_message_request(
&wire,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
})
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
round,
"context_window_400",
None,
false,
compress_state.floor_trend(),
color,
);
}
let projected = estimate_request_tokens(
&openai_chat_wire_messages(&messages)?,
tools_supported.then_some(&tools),
estimation,
);
if projected >= round_est_raw
|| calibrate_up(projected, cal) > new_budget
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
Some(calibrate_up(projected, cal)),
)?;
cw_retries += 1;
continue;
}
}
if overflow {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
}
return Err(e);
}
}
};
let round_usage = openai_usage(&json["usage"]);
if let Some(prompt_tokens) = json["usage"]["prompt_tokens"]
.as_u64()
.and_then(|n| u32::try_from(n).ok())
{
compress_state
.calibration
.observe(Some(prompt_tokens), round_est_raw);
cal = compress_state.calibration.ratio(estimate_ratio);
tool_tokens_real = calibrate_up(tool_tokens, cal);
prompt_tracker.record(prompt_tokens, messages.len());
}
accumulated_usage = merge_round_usage(accumulated_usage, round_usage);
let truncation_suspect = false;
if let (Some(u), Some(budget), false) = (round_usage, send_budget, truncation_suspect) {
let raised = capped_accepted_prompt_tokens(u.input_tokens, effective_input_ceiling);
if raised > budget {
send_budget = Some(raised);
if debug {
print_debug(
&format!(
"send budget raised to ~{raised} tokens (backend accepted \
{}-token prompt)",
u.input_tokens
),
color,
);
}
}
}
let message = &json["choices"][0]["message"];
let (oa_content, inline_reasoning) = crate::reasoning::ThinkFilter::filter_complete(
message["content"].as_str().unwrap_or(""),
emits_leading_reasoning,
);
let separate_reasoning = message["reasoning_content"]
.as_str()
.map(str::trim)
.filter(|s| !s.is_empty());
if debug && (separate_reasoning.is_some() || inline_reasoning.is_some()) {
let n = separate_reasoning
.map(str::len)
.or_else(|| inline_reasoning.as_deref().map(str::len))
.unwrap_or(0);
print_debug(
&format!("reasoning ({n} chars) surfaced to the trace, not the answer"),
color,
);
}
commit_reasoning_fold(
separate_reasoning
.map(str::to_string)
.or_else(|| inline_reasoning.clone()),
thought_for,
completed_spill_renderer.as_deref(),
color,
);
if generation_bounds::output_limited(&json) {
if output_recovery.retry(
message["tool_calls"]
.as_array()
.is_some_and(|calls| !calls.is_empty()),
&mut messages,
smart_harness,
)? {
continue;
}
if let Some(obs) = solve_obs.as_deref_mut() {
obs.behavior_signals
.push(observability::BehaviorSignal::ChatCompletionFinish {
round,
finish_reason: Some("length".into()),
});
let reasoning = separate_reasoning.or(inline_reasoning.as_deref());
if observability::reasoning_overflow_signature(
Some("length"),
oa_content.is_empty(),
reasoning.is_some(),
message["tool_calls"]
.as_array()
.is_some_and(|calls| !calls.is_empty()),
) {
obs.behavior_signals
.push(observability::BehaviorSignal::ReasoningOverflow {
round,
reasoning_overflow_detected: true,
continuation_attempted: false,
continuation_succeeded: false,
finish_reason: "length".into(),
reasoning_tokens_estimate: estimation.tokens_for_chars(
reasoning.map(|text| text.chars().count()).unwrap_or(0),
),
});
}
}
let text = finalize_final_text(
generation_bounds::output_limit_notice(oa_content, round_usage),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Failed);
}
if let Some(harness) = smart_harness {
harness.outcome(crate::TurnEndReason::Failed, &text)?;
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let native_calls = message["tool_calls"].as_array();
let mut recovered = if native_calls.map(|t| t.is_empty()).unwrap_or(true) {
tool_recovery::recover_tool_calls(&oa_content)
} else {
tool_recovery::Recovery::default()
};
let recovered_identities = tool_recovery::assign_ids(
&mut recovered,
attempt_ledger.and_then(|l| l.lock().ok().and_then(|l| l.latest_primary())),
);
let tool_calls: Option<&Vec<serde_json::Value>> = match native_calls {
Some(t) if !t.is_empty() => Some(t),
_ if !recovered.calls.is_empty() => Some(&recovered.calls),
_ => None,
};
let has_tools = tool_calls.map(|tc| !tc.is_empty()).unwrap_or(false);
let finish_reason = json["choices"][0]["finish_reason"].as_str();
if let Some(obs) = solve_obs.as_deref_mut() {
obs.behavior_signals
.push(observability::BehaviorSignal::ChatCompletionFinish {
round,
finish_reason: finish_reason.map(str::to_string),
});
}
if let Some(obs) = solve_obs.as_deref_mut() {
if let Some(m) = json["model"].as_str().filter(|m| !m.is_empty()) {
obs.served_model = Some(m.to_string());
}
let native = native_calls.is_some_and(|t| !t.is_empty());
if let Some(sig) = observability::round_parse_signal(
round,
!oa_content.is_empty(),
native,
recovered.dialect,
recovered_identities.clone(),
) {
obs.parse_signals.push(sig);
}
}
if debug && !recovered.calls.is_empty() {
print_debug(
&format!(
"recovered {} tool call(s) from content (non-native emission)",
recovered.calls.len()
),
color,
);
}
if debug {
let excerpt: String = oa_content.chars().take(80).collect();
let tc_count = tool_calls.map(|tc| tc.len()).unwrap_or(0);
let usage_str = match round_usage {
Some(u) => format!("{} in / {} out", u.input_tokens, u.output_tokens),
None => "no usage".into(),
};
print_debug(
&format!(
"round {round}: tool_calls={tc_count} usage=[{usage_str}] content={excerpt:?}"
),
color,
);
}
if !has_tools {
if let Some(mut nudge) = capability_evidence.correction(
&oa_content,
&tools,
&mut probe_correction_used,
tools_supported
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !is_cancelled(cancel),
) {
if let Some(harness) = smart_harness {
nudge = harness.correct_answer(&oa_content, &nudge)?;
}
messages.push(serde_json::json!({"role":"assistant", "content":oa_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue 'round_loop;
}
if let Some(harness) = smart_harness {
let more = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
let control = harness
.classify(&oa_content, narration_nudge_cap, more, cancel)
.await?;
let control = harness
.verify_answer(
control,
self_verify::Concluding {
messages: &messages,
workspace,
task,
rounds_left: more,
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
smart_verify,
&oa_content,
)
.await?;
let (text, reason) = match control {
smart_harness::Control::Continue(nudge) => {
messages
.push(serde_json::json!({"role":"assistant", "content":oa_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue 'round_loop;
}
smart_harness::Control::Answer => (
FinalReply::from(oa_content.clone()),
crate::TurnEndReason::Completed,
),
smart_harness::Control::Finish { text, reason } => (text, reason),
};
if let Some(slot) = &mut end_reason {
**slot = Some(reason);
}
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(reason, &text)?;
return Ok((text, false, accumulated_usage, hallucination_count));
}
if recovered.tool_shaped {
hallucination_count += 1;
if debug {
print_debug(
"format-hallucination: tool call emitted as unrecoverable text",
color,
);
}
}
let content = oa_content.clone();
if !is_cancelled(cancel)
&& readonly_completion_pending(prompt_disposition, &nudge_classifier, &content)
{
let more_rounds = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
if retry_readonly_completion(
&mut messages,
&content,
None,
&mut readonly_completion_retried,
more_rounds,
) {
continue 'round_loop;
}
let out = readonly_completion_handoff(
&content,
&mut end_reason,
more_rounds,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
if content.is_empty() && debug {
print_debug(
"empty content with no tool calls — model produced nothing",
color,
);
}
let nudge_classification =
(!content.is_empty()).then(|| nudge_classifier.classify(&content));
let workflow_classifier_text = workflow_classifier_text(&messages, &content);
let workflow_hint = nudge_classification
.as_ref()
.filter(|classification| classification.is_plan_update())
.and_then(|_| workflow_steerer.plan_update_hint(&workflow_classifier_text));
let classifier_plan_direction = nudge_classification
.as_ref()
.filter(|classification| classification.is_plan_update())
.and_then(|_| nudge_classifier.direction_for(crate::NudgeClass::PlanUpdate));
let plan_nudge_hint =
combine_nudge_hints(classifier_plan_direction, workflow_hint.as_deref());
if should_ground_unverified_run_command_blocker(
&content,
&tools,
run_command_denial_observed,
exec_grounding_turn,
unverified_exec_blocker_nudges,
workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
),
) {
if debug {
print_debug(
"unsupported run_command blocker — requiring a ground-truth probe",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
run_command_ground_truth_nudge()
)
}));
unverified_exec_blocker_nudges += 1;
continue 'round_loop;
}
if !content.is_empty()
&& pending_plan_nudges < PENDING_PLAN_NUDGE_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_turn
{
let needs_plan_update = nudge_classification
.as_ref()
.is_some_and(|c| c.is_plan_update());
if let Some(nudge) = pending_plan_completion_nudge(
step_ledger,
needs_plan_update,
plan_nudge_hint.as_deref(),
) {
if debug {
print_debug(
"active plan has unfinished steps — nudging before final answer",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge)
}));
pending_plan_nudges += 1;
continue 'round_loop;
}
}
if !content.is_empty()
&& stale_file_nudges < STALE_FILE_NUDGE_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_nudges
&& looks_like_unverified_stale_file_blocker(&content)
{
if debug {
print_debug(
"unverified stale-file blocker — nudging to check ground truth",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
stale_file_ground_truth_nudge()
),
}));
stale_file_nudges += 1;
continue 'round_loop;
}
if !content.is_empty()
&& narration_nudges < narration_nudge_cap
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_turn
&& nudge_classification
.as_ref()
.is_some_and(|c| c.is_pending_action())
{
if debug {
print_debug(
"narrated intent with no tool call — nudging to act and continuing",
color,
);
}
strip_trailing_nudge_exchange(&mut messages);
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
let direction = if narration_nudges == 0 {
nudge_classification
.as_ref()
.and_then(|classification| {
nudge_classifier.direction_for(classification.class)
})
.map(workflow_guidance)
.unwrap_or_else(narration_action_nudge)
} else {
escalated_narration_action_nudge(
narration_nudges + 1,
narration_nudge_cap,
step_ledger,
)
};
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, direction),
}));
narration_nudges += 1;
continue 'round_loop;
}
let mut verification_stop = None;
if verification.result_aware() && action_nudges && !content.is_empty() {
match self_verify::conclude_turn(
self_verify::Concluding {
messages: &messages,
workspace,
task: active_task,
rounds_left: workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
),
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
verification_nudges,
)
.await
{
self_verify::Decision::Nudge(nudge) => {
strip_trailing_nudge_exchange(&mut messages);
messages
.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge),
}));
verification_nudges += 1;
continue 'round_loop;
}
self_verify::Decision::Stop(reason) => verification_stop = Some(reason),
self_verify::Decision::Accept => {}
}
} else if self_verify::enabled()
&& action_nudges
&& self_verify_nudges < SELF_VERIFY_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !content.is_empty()
{
let cmds = self_verify::commands_from_messages(&messages);
let checks = verification
.applicable_checks(workspace, active_task, &cmds)
.await
.unwrap_or_default();
if let Some(nudge) = self_verify::verify_gate_nudge(&checks, &cmds) {
if debug {
print_debug(
"concluding with unrun verification — self-verify nudge",
color,
);
}
strip_trailing_nudge_exchange(&mut messages);
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge),
}));
self_verify_nudges += 1;
continue 'round_loop;
}
}
if !content.is_empty() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
}
let accepted_reason = if content.is_empty() {
crate::TurnEndReason::Empty
} else if nudge_classification
.as_ref()
.is_some_and(|c| c.is_pending_action())
&& action_turn
{
if !workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
) {
crate::TurnEndReason::NarrationFinalRound
} else {
crate::TurnEndReason::NarrationCapExhausted
}
} else {
crate::TurnEndReason::Completed
};
let accepted_reason = verification_stop.unwrap_or(accepted_reason);
if debug && accepted_reason != crate::TurnEndReason::Completed {
print_debug(
&format!("no-tool reply accepted as final answer ({accepted_reason:?})"),
color,
);
}
if let Some(slot) = &mut end_reason {
**slot = Some(accepted_reason);
}
if content.is_empty() {
let out = FinalReply::harness("(model returned an empty response — try rephrasing, or check the model with `newt doctor`)".to_string());
let out = finalize_final_text(
out,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
observability::observe_harness_reply(&mut solve_obs);
return Ok((out, false, accumulated_usage, hallucination_count));
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
let out = finalize_final_text(
content,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
let mut assistant_turn =
prepare_openai_assistant_replay(message, &oa_content, reasoning_replay_scope, true);
if !recovered_identities.is_empty() {
assistant_turn["tool_calls"] =
serde_json::Value::Array(tool_recovery::wire_tool_calls(&recovered.calls));
}
messages.push(assistant_turn);
let mut round_modified_workspace = false;
let mut round_progress = false;
let mut round_succeeded = false;
let tcs = tool_calls.unwrap();
let extracted: Vec<(Option<&str>, Option<&str>, &serde_json::Value)> = tcs
.iter()
.map(|tc| {
if tc["function"].is_null() {
(tc["id"].as_str(), tc["name"].as_str(), &tc["input"])
} else {
(
tc["id"].as_str(),
tc["function"]["name"].as_str(),
&tc["function"]["arguments"],
)
}
})
.collect();
let validated = match tools::validate_tool_call_batch(&extracted, true) {
Ok(v) => {
uncorrelatable_batches = 0;
Some(v)
}
Err(tools::BatchRejection::CorrelationImpossible(reason)) => {
let reask = tools::reask_uncorrelatable(
&mut uncorrelatable_batches,
&reason,
smart_harness,
tool_events.as_deref_mut(),
)?;
if let Some(turn) = messages.last_mut() {
tools::withdraw_tool_calls(turn);
}
messages.push(serde_json::json!({"role": "user", "content": reask}));
continue 'round_loop;
}
Err(tools::BatchRejection::ContentInvalid(reason)) => {
uncorrelatable_batches = 0;
if let Some(harness) = smart_harness {
harness.reject_tools(&reason, true)?;
}
for tc in tcs {
let id = tc["id"].as_str().unwrap_or("");
print_synthetic_tool_result(
"(rejected tool-call batch)",
&serde_json::Value::Null,
workspace,
&reason,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(
"(rejected tool-call batch)",
&serde_json::Value::Null,
false,
Some(0),
));
}
smart_harness::push_tool_message(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": format!("tool-call batch rejected before execution: {reason}"),
}),
smart_harness,
true,
"(rejected tool-call batch)",
)?;
}
None
}
};
let batch = smart_harness
.zip(validated.as_ref())
.map(|(harness, calls)| harness.tool_batch(calls, &messages))
.transpose()?;
let mut tool_warnings = Vec::new();
if validated.is_some() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
commit_tool_round_narration(
&oa_content,
false,
completed_spill_renderer.as_deref(),
color,
);
}
for (call_index, (tc, vc)) in tcs.iter().zip(validated.iter().flatten()).enumerate() {
let id = vc.call_id.as_str();
let name = vc.name.as_str();
let args = vc.args.clone();
announce_tool_activity(name);
dismiss_completed_spill(&completed_spill_renderer);
if debug && tc["function"].is_null() {
print_debug(
&format!(
"tool call in Anthropic-native format inside tool_calls array \
(no `function` key) — name={name:?}"
),
color,
);
}
if trace {
print_trace(
&format!(
"raw tool_call element: {}",
serde_json::to_string(tc).unwrap_or_else(|_| "?".into())
),
color,
);
}
let mcp_handles = mcp.handles(name);
if debug {
print_debug(
&format!("dispatching tool name={name:?} mcp_handles={mcp_handles}"),
color,
);
}
if is_hallucination(name, &args) {
hallucination_count += 1;
}
if let Some(content) =
repeat_calls.cached_read(name, &args, ReadScope { workspace, caveats })
{
print_synthetic_tool_result(
name,
&args,
workspace,
&content,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_cache(name, &args));
}
let offloaded =
maybe_offload_tool_result(name, content, tool_offload, spill_store, disclosure);
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": offloaded,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
if let Some(steer) = repeat_calls.repeat_steer(name, &args) {
print_synthetic_tool_result(
name,
&args,
workspace,
&steer,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(name, &args, false, Some(0)));
}
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": steer,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
let invocation = batch
.as_ref()
.map(|batch| batch.start(call_index, disclosure))
.transpose()?;
record_organic_note_use(name, ¬e_sink, &mut note_nudge);
let before_change =
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await;
ledger_note_write(write_ledger, name, &args, workspace);
let tool_t0 = std::time::Instant::now();
let execution = std::sync::OnceLock::new();
let retry_authorized = std::sync::OnceLock::new();
let routed_to = std::sync::OnceLock::new();
let governed_pr = std::sync::OnceLock::new();
let report_capture = std::sync::Mutex::new(observed_report::Capture::default());
let command_directory = std::sync::OnceLock::new();
let attribution_epoch = AttributionEpoch::new(attribution, git_tool, model, name);
let result = if tools::is_context_remaining_call(name) {
let report = budget::render_context_budget(
prompt_tracker.current(&messages, Some(&tools), cal, estimation),
effective_input_ceiling,
num_ctx,
input_ceiling_pct,
low_budget_pct,
);
if let Some(invocation) = &invocation {
invocation.host();
invocation.observe(&report, None, spill_store)?;
}
print_synthetic_tool_result(
name,
&args,
workspace,
&report,
color,
&completed_spill_renderer,
);
report
} else {
let tool_evidence = tool_events
.as_deref()
.map(|e| capability_check::Evidence::from_events(e));
let Some(result) = tools::execute_tool_with_collaborators(
name,
&args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
tools::ToolCollaborators {
command_budget,
shell_dialect,
read_history: Some(&mut read_history),
default_command_cwd,
worktree_session,
invocation: invocation.as_ref(),
build_check_cmd: build_check_cmd.as_deref(),
tool_evidence: tool_evidence.as_ref(),
note_sink: note_sink
.as_deref_mut()
.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
prompt_context: Some(prompt_context),
artifact_context,
artifact_sink,
permission_gate: permission_gate
.as_deref_mut()
.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav,
experience_store,
step_ledger,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
spill_store,
persona_tools,
hidden_tools: Some(&hidden_tools),
live_tool_output: live_tool_output.clone(),
completed_spill_renderer: completed_spill_renderer.clone(),
execution: Some(&execution),
retry_authorized: Some(&retry_authorized),
governed_pr: Some(&governed_pr),
report_capture: Some(&report_capture),
command_directory: Some(&command_directory),
routed_to: Some(&routed_to),
pending_rerun: Some(&mut pending_rerun),
},
tool_offload,
prompt_disposition,
cancel,
)
.await?
else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
result
};
if debug {
dismiss_completed_spill(&completed_spill_renderer);
let excerpt: String = result.chars().take(120).collect();
print_debug(&format!("tool result: {excerpt:?}"), color);
}
let ok = tools::tool_ok(&result, execution.get().copied());
round_succeeded |= ok;
attribution_epoch.finish(&args, ok);
run_command_denial_observed |= run_command_result_is_denial(name, ok, &result);
capability_evidence.record(name, ok, execution.get().copied());
let after_change = if before_change.is_some() {
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await
} else {
None
};
round_modified_workspace |=
workflow_runtime.record_workspace_change(before_change, after_change);
round_progress |= workflow_runtime.record_observation(
name,
&args,
&result,
ok,
execution.get().copied(),
routed_to.get(),
);
repeat_calls.record(
name,
&args,
ok,
&result,
execution.get().copied(),
ReadScope { workspace, caveats },
);
repeat_calls.permit_authorized_retry(name, &args, &retry_authorized);
append_clean_build_warning(
if batch.is_some() {
&mut tool_warnings
} else {
&mut messages
},
&mut clean_build,
&args,
None,
)?;
record_completed_tool_event(
&mut tool_events,
name,
&args,
ok,
execution.get().copied(),
tool_t0,
);
turn_claims.observe_report(
workspace,
&caveats.fs_read,
&report_capture.lock().expect("report capture"),
execution.get().copied(),
governed_pr.get(),
);
if let Some(outcome) = governed_pr.get() {
verification.record_publication_outcome(outcome);
}
if ok {
if let Some(directory) = command_directory.get() {
verification.record_command_directory(directory);
}
}
verification
.observe_routed(
name,
&args,
ok,
execution.get().copied(),
workspace,
routed_to.get(),
)
.await;
record_phantom_reach(
&mut phantom_reaches,
name,
&args,
&result,
ok,
advertise_team,
);
observed_paths.record(&result, &mut observed_resolver);
smart_harness::push_tool_return(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": smart_harness::tool_result(name, result, tool_offload, spill_store, disclosure, invocation.as_ref())?,
}),
invocation.as_ref(),
)?;
}
for message in tool_warnings {
smart_harness::push_tool_message(
&mut messages,
message,
smart_harness,
true,
"build guidance",
)?;
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
if round_succeeded {
output_recovery.completed_tool_round();
}
workflow_runtime.record_round_outcome(round_modified_workspace, round_progress);
if let Some(text) = pending_plan_approval_handoff(
plan_mode_control,
PlanRoundFacts {
disposition: prompt_disposition,
step_ledger,
plan_at_turn_start: &plan_at_turn_start,
progressed: round_modified_workspace || round_progress,
},
smart_harness,
cancel,
&mut end_reason,
)? {
dismiss_completed_spill(&completed_spill_renderer);
let text = finalize_final_text(
FinalReply::harness(text),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((text, false, accumulated_usage, hallucination_count));
}
}
dismiss_completed_spill(&completed_spill_renderer);
let protected_head = protected_prompt_head_len(&messages, prompt_read::ACTIVE_PROMPT_PREFIX);
let replay_protected_tail_len =
compress::protected_reasoning_tail_len(&messages, reasoning_replay_scope);
let trimmed = trim_for_summary(&messages, protected_head, 6.max(replay_protected_tail_len));
let progress = cap_exit_progress(worktree_session, step_ledger, scratchpad_store);
let cap_reason = verification
.cap_exit_reason(
workspace,
if smart_harness.is_some() {
smart_verify
} else {
action_nudges
},
current_tool_round_limit,
solve_obs.as_deref_mut(),
)
.await;
if let Some(hit) = &mut round_cap_hit {
**hit = true;
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
let text = cap_exit_fallback(
current_tool_round_limit,
accumulated_usage,
repeat_calls.total_failures(),
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(cap_reason, &text)?;
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let cap = CapExit {
max_tool_rounds: current_tool_round_limit,
accumulated: accumulated_usage,
wasted_calls: repeat_calls.total_failures(),
progress,
observed: observed_paths.into_vec(),
request_budget: authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
calibration: cal,
estimation,
ollama_options: None,
prompt_measurement: Default::default(),
fell_back: Default::default(),
};
let summary = final_summary_openai(
(&client, &stream_client),
&chat_url,
model,
api_key,
trimmed,
generation_policy,
generation_window,
&cap,
attempts,
);
let Some(result) = cancellable(cancel, summary).await else {
cap.learn_measurement(compress_state)?;
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let (text, streamed, usage) = cap.recover_rejection(
result,
&mut solve_obs,
compress_state,
current_tool_round_limit,
cw_retries + 1,
)?;
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
Ok((text, streamed, usage, hallucination_count))
}
fn anthropic_headers(
req: reqwest::RequestBuilder,
api_key: Option<&str>,
) -> reqwest::RequestBuilder {
let req = match api_key.filter(|k| !k.is_empty()) {
Some(key) => req.header("x-api-key", key),
None => req,
};
req.header("anthropic-version", crate::backend_probe::ANTHROPIC_VERSION)
}
struct AnthropicDispatch<'a> {
smart_harness: Option<&'a smart_harness::SmartHarness>,
client: &'a reqwest::Client,
stream_client: &'a reqwest::Client,
messages_url: &'a str,
api_key: Option<&'a str>,
retry: &'a RetryPolicy,
color: bool,
markdown: bool,
defer_answer: bool,
retain: Option<&'a std::sync::Arc<dyn CompletedSpillRenderer>>,
}
async fn anthropic_dispatch_round(
d: &AnthropicDispatch<'_>,
body: &serde_json::Value,
messages: &[serde_json::Value],
stream: bool,
cancel: Option<&std::sync::atomic::AtomicBool>,
attempts: Option<attempt_capture::AttemptScope<'_>>,
) -> anyhow::Result<Option<(anthropic_wire::AnthropicRound, bool)>> {
if !stream {
let result = cancellable(
cancel,
with_backoff_notify_error(
d.retry,
|| async {
let req = anthropic_headers(d.client.post(d.messages_url), d.api_key);
let req = match d.smart_harness {
Some(harness) => req
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(harness.prepare_with_messages(body, "anthropic", messages)?),
None => req.json(body),
};
let (resp, attempt) =
attempt_capture::send(attempts, "primary", req, "request failed").await?;
let json = smart_harness::response(resp, d.smart_harness, "inference endpoint")
.await?;
Ok((json, attempt))
},
|attempt, delay, error| {
print_retry_indicator(attempt, d.retry.max_retries, delay, error, d.color);
},
),
)
.await;
return match result {
None => Ok(None),
Some(Ok((json, attempt))) => {
let round = anthropic_wire::parse_messages_reply(&json);
attempt_capture::complete(attempt.as_ref(), round.usage);
Ok(Some((round, false)))
}
Some(Err(e)) => Err(e),
};
}
let mut stream_retries: u32 = 0;
loop {
let resp = match cancellable(
cancel,
with_backoff_notify_error(
d.retry,
|| async {
let req = anthropic_headers(d.stream_client.post(d.messages_url), d.api_key)
.json(body);
let (resp, attempt) =
attempt_capture::send(attempts, "primary", req, "stream request failed")
.await?;
if !resp.status().is_success() {
let status = resp.status();
let (bytes, read_error) = crate::retry::read_response_bytes(resp).await;
let text = String::from_utf8_lossy(&bytes);
let read_diagnostic = read_error
.map(|error| format!("; response body read failed: {error}"))
.unwrap_or_default();
return Err(observability::DispatchError::http_status(format!(
"inference endpoint {status}: {text}{read_diagnostic}"
))
.into());
}
Ok((resp, attempt))
},
|attempt, delay, error| {
print_retry_indicator(attempt, d.retry.max_retries, delay, error, d.color);
},
),
)
.await
{
None => return Ok(None),
Some(r) => r?,
};
let (resp, attempt) = resp;
let mut spinner = crate::tty::Spinner::start_with_caps(
legacy_caps(d.color && thinking_stream_enabled()),
"thinking…",
crate::tty::Sink::Stdout,
d.color,
);
let cols = display::term_cols();
let mut md = (d.markdown && !d.defer_answer).then(|| {
MarkdownStreamWriter::new(
io::stdout(),
RenderOpts {
color: true,
cols: display::reply_cols(cols),
},
)
});
let mut acc = anthropic_wire::SseAccumulator::new();
let anth_budget =
if display::with_migration_notices(thinking_mode) == crate::ThinkingMode::Fold {
display::spill_lines()
} else {
0
};
let mut anth_reason = ReasoningTrickle::default();
let mut started = false;
let mut transport_break: Option<String> = None;
let mut interrupted = false;
let mut carry: Vec<u8> = Vec::new();
let mut resp = resp;
while !acc.is_done() {
match cancellable(cancel, resp.chunk()).await {
None => {
interrupted = true;
break;
}
Some(Ok(Some(chunk))) => {
for action in acc.feed(&decode_chunk(&mut carry, &chunk)) {
match action {
anthropic_wire::StreamAction::TextDelta(t) => {
if d.defer_answer {
if let Some(sp) = spinner.take() {
anth_reason.close(
sp.elapsed(),
d.retain.map(std::sync::Arc::as_ref),
d.color,
);
}
continue;
}
if !started {
if let Some(sp) = spinner.as_ref() {
anth_reason.close(
sp.elapsed(),
d.retain.map(std::sync::Arc::as_ref),
d.color,
);
}
drop(spinner.take());
if d.color {
execute!(
io::stdout(),
SetForegroundColor(NEWT_ORANGE_CT),
Print(display::REPLY_MARKER),
ResetColor,
)
.ok();
} else {
print!("{}", display::REPLY_MARKER);
}
started = true;
}
if let Some(w) = md.as_mut() {
w.push(&t).ok();
} else {
print!("{t}");
io::stdout().flush().ok();
}
}
anthropic_wire::StreamAction::ThinkingDelta(t) => {
if let Some(sp) = spinner.as_ref() {
anth_reason.feed(sp, &t, anth_budget);
}
}
}
}
}
Some(Ok(None)) => break,
Some(Err(e)) => {
transport_break = Some(format!("stream request failed: {e}"));
break;
}
}
}
drop(spinner.take());
if let Some(w) = md.as_mut() {
w.finish().ok();
}
if started && md.is_none() {
println!();
}
let done = acc.is_done();
let round = acc.finish();
let break_error = round.error.clone().or(transport_break);
attempt_capture::finish(
attempt.as_ref(),
if interrupted {
crate::attempts::AttemptState::Cancelled
} else if done && break_error.is_none() {
crate::attempts::AttemptState::Ok
} else {
crate::attempts::AttemptState::Failed
},
round.usage,
);
if let Some(err) = break_error {
let shaped: anyhow::Error = observability::DispatchError::http_status(format!(
"inference endpoint 529: mid-stream error: {err}"
))
.into();
if crate::retry::classify(&shaped) != crate::retry::Retryability::Retry {
return Err(shaped);
}
if round.text.is_empty() {
if stream_retries >= d.retry.max_retries {
return Err(shaped);
}
stream_retries += 1;
let delay = d.retry.delay_for(stream_retries);
print_retry_indicator(stream_retries, d.retry.max_retries, delay, &shaped, d.color);
tokio::time::sleep(delay).await;
continue;
}
print_harness_notice(
&format!("stream broke mid-response ({err}) — keeping the partial answer"),
d.color,
);
return Ok(Some((round, started)));
}
return Ok(Some((round, started)));
}
}
#[allow(clippy::too_many_arguments)]
async fn final_summary_anthropic(
client: &reqwest::Client,
messages_url: &str,
model: &str,
api_key: Option<&str>,
mut messages: Vec<serde_json::Value>,
generation_policy: generation_policy::GenerationPolicy,
cap: &CapExit,
attempts: Option<attempt_capture::AttemptScope<'_>>,
) -> anyhow::Result<(FinalReply, bool, Option<crate::TokenUsage>)> {
cap.push_nudge(&mut messages);
let (system, wire_messages) = anthropic_wire::anthropic_wire_messages(&messages)?;
if !cap.fits(&wire_messages, model) {
return Ok(cap.fallback());
}
let body = anthropic_wire::build_messages_body(
model,
generation_policy
.output_allowance
.unwrap_or_else(anthropic_wire::default_max_tokens),
system.as_deref(),
&wire_messages,
None,
false,
);
cap.finish(
messages_url,
attempts,
|| async { Ok(anthropic_headers(client.post(messages_url), api_key).json(&body)) },
"inference endpoint",
estimate_value_tokens(&body, cap.estimation),
|json| {
let reply = anthropic_wire::parse_messages_reply(&json);
(reply.text, reply.usage)
},
)
.await
}
async fn anthropic_chat_complete_with_prompt_and_artifacts(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
artifact_source: Option<&dyn artifact_read::ArtifactSource>,
artifact_sink: Option<&dyn artifact_read::PromptArtifactSink>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
let ChatCtx {
shell_dialect,
command_budget,
verify_outcomes,
smart_harness,
url,
model,
kind: _,
api_key,
messages: mem_messages,
task,
workspace,
default_command_cwd,
worktree_session,
color,
markdown,
tool_offload,
spill_store,
disclosure,
compaction_store,
scratchpad,
scratchpad_store,
code_search,
where_is,
nav,
exposure,
experience_store,
step_ledger,
caveats,
persona_tools,
cognition,
output_allowance,
overflow_retry: _,
attempt_ledger,
chat_completions_capability,
responses_capability: _,
openai_api: _,
reasoning_replay_scope,
max_tool_rounds,
workflow_grace_rounds,
narration_nudge_cap,
action_nudges,
prompt_disposition,
prompt_intake,
tool_output_lines,
debug,
trace,
num_ctx,
connect_timeout_secs,
inference_timeout_secs,
mid_loop_trim_threshold,
compaction_trigger_policy,
mid_loop_trim_tokens,
max_ok_input,
build_check_cmd,
safe_context,
recover_cw_400,
mut note_sink,
mut note_nudge,
recall_source,
memory_source,
summarizer,
compress_state,
mut tool_events,
mut phantom_reaches,
mut end_reason,
mut round_cap_hit,
mut solve_obs,
mut permission_gate,
mut on_round_usage,
estimate_ratio,
estimation,
summary_input_cap_floor_chars,
rewrites_history,
input_ceiling_pct,
low_budget_pct,
exec_floor,
write_ledger,
attribution,
cancel,
run_allowance,
live_tool_output,
git_tool,
crew_runner,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
steering,
completed_spill_renderer,
emits_leading_reasoning: _,
} = ctx;
let _completed_spill_guard = CompletedSpillDismissGuard(&completed_spill_renderer);
let action_nudges = action_nudges && prompt_disposition == PromptDisposition::Act;
if let Some(harness) = smart_harness {
harness.start_turn()?;
}
let spill_store = smart_harness.map(|h| h.spill_store()).or(spill_store);
let tool_offload = tool_offload || smart_harness.is_some();
let smart_verify = action_nudges && prompt_disposition == PromptDisposition::Act;
let action_nudges = action_nudges && smart_harness.is_none();
let max_tool_rounds = prompt_disposition.tool_round_limit(max_tool_rounds);
let generation_policy = generation_policy::GenerationPolicy::resolve(
cognition,
output_allowance,
chat_completions_capability,
reasoning_replay_scope,
);
let reasoning_replay_scope = generation_policy.reasoning_replay_scope;
let generation_policy::GenerationPolicy {
thinking: _,
reasoning_effort: _,
temperature: _,
top_p: _,
parallel_tool_calls: _,
chat_template_kwargs: _,
max_output_tokens: _,
output_allowance: _,
reasoning_replay_scope: _,
one_bounded_reasoning_continuation: _,
} = generation_policy;
let max_tokens = generation_policy
.output_allowance
.unwrap_or_else(anthropic_wire::default_max_tokens);
observability::observe_output_allowance(&mut solve_obs, Some(max_tokens), true);
let streaming_enabled = smart_harness.is_none()
&& !std::env::var("NEWT_ANTHROPIC_STREAM")
.is_ok_and(|v| v.trim().eq_ignore_ascii_case("off"));
let mut local_compress_state = CompressState::new();
let compress_state = match compress_state {
Some(s) => s,
None => &mut local_compress_state,
};
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let stream_client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.read_timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let messages_url = anthropic_wire::messages_url(url);
let retry = tui_retry_policy(url);
let dispatcher = AnthropicDispatch {
defer_answer: true,
smart_harness,
client: &client,
stream_client: &stream_client,
messages_url: &messages_url,
api_key,
retry: &retry,
color,
markdown,
retain: completed_spill_renderer.as_ref(),
};
let advertise_save_note = note_sink.is_some();
let advertise_recall = recall_source.is_some();
let advertise_memory_fetch = memory_source.is_some();
let advertise_scratchpad = scratchpad_store.is_some() && scratchpad;
let advertise_code_search = code_search.is_some();
let advertise_experiential = experience_store.is_some();
let advertise_scheduled = step_ledger.is_some();
let advertise_git = false;
let advertise_team = crew_runner.is_some();
let advertise_operating_mode = operating_mode_control.is_some();
let advertise_plan_mode = plan_mode_control.is_some();
let advertise_plan_mode_active =
plan_mode_control.is_some_and(|control| control.is_plan_mode());
let mem_messages = observed_report::replay_messages(mem_messages);
let mut messages: Vec<serde_json::Value> = mem_messages
.iter()
.map(|m| {
let content = if m.role == crate::Role::Assistant
&& reasoning_replay_scope != crate::model_card::ReasoningReplayScope::FullHistory
{
crate::reasoning::split_reasoning(&m.content).0
} else {
m.content.clone()
};
serde_json::json!({"role": m.role.as_str(), "content": content})
})
.collect();
if let Some(harness) = smart_harness {
messages = harness.initial_messages(messages)?;
}
if let (Some(session), Some(context)) = (worktree_session, turn_prompt_context) {
session
.report
.lock()
.expect("report state")
.begin(context.active().root_prompt_id());
}
let ephemeral_prompt = prompt_read::headless_prompt_fallback(turn_prompt_context, task);
let turn_prompt_context = turn_prompt_context.or(ephemeral_prompt.as_ref());
let prompt_context =
prompt_read::PromptReadContext::new(turn_prompt_context, task, prompt_source);
let artifact_context = turn_prompt_context
.map(|turn| artifact_read::ArtifactReadContext::from_turn(turn, artifact_source));
let active_task = prompt_context.active_text();
prompt_read::ensure_active_prompt_card(&mut messages, prompt_context, prompt_intake);
let attempt_turn = turn_prompt_context
.map(|turn| turn.active_operator_prompt().id().to_string())
.unwrap_or_default();
let fallback_ledger = std::sync::Mutex::new(crate::attempts::AttemptLedger::default());
let attempt_ledger = attempt_ledger.or_else(|| run_allowance.map(|_| &fallback_ledger));
let attempts = attempt_ledger.map(|ledger| attempt_capture::AttemptScope {
ledger,
turn: &attempt_turn,
model,
backend: url,
cancel,
run_allowance,
});
if note_sink.is_some() {
if let Some(line) = note_nudge.as_deref_mut().and_then(NoteNudge::begin_turn) {
if let Some(harness) = smart_harness {
harness.host_message(&line)?;
messages.push(serde_json::json!({"role":"user", "content":line}));
} else {
append_nudge_line(&mut messages, &line);
}
}
}
let mut accumulated_usage: Option<crate::TokenUsage> = None;
let mut hallucination_count: u32 = 0;
let result_aware = self_verify::enabled() && verify_outcomes;
let mut repeat_calls = RepeatCallGuard::default();
let plan_at_turn_start = step_ledger
.map(|ledger| ledger.snapshot())
.unwrap_or_default();
let mut read_history = tools::ReadHistory::default();
let mut verification = self_verify::VerificationLedger::for_turn(task, result_aware);
let turn_claims =
claim_check::TurnClaims::capture(workspace, &caveats.fs_read, worktree_session)
.with_evidence(observed_report::review::Evidence {
sink: artifact_sink,
context: artifact_context,
spill: spill_store,
});
let mut clean_build = crate::loop_watch::CleanBuildWatch::default();
let mut reasoning_continuation_attempted = false;
let mut reasoning_overflow_signal_index: Option<usize> = None;
let mut pause_turn_retried = false;
let mut cw_retries: u32 = 0;
let mut output_recovery = output_limit_recovery::Recovery::default();
let mut tools_supported = true;
let mut tools_unsupported_notified = false;
let mut effective_input_ceiling =
num_ctx_input_ceiling(num_ctx, input_ceiling_pct, Some(max_tokens));
let mut send_budget: Option<usize> =
initial_send_budget(max_ok_input, safe_context, effective_input_ceiling);
let mut send_budget_authoritative = safe_context.is_some() || effective_input_ceiling.is_some();
let tools = merged_tool_definitions(
mcp,
advertise_save_note,
advertise_recall,
advertise_memory_fetch,
advertise_git.then_some(&caveats.fs_read),
advertise_team,
advertise_scratchpad,
advertise_code_search,
advertise_experiential,
advertise_scheduled,
advertise_operating_mode,
advertise_plan_mode,
advertise_plan_mode_active,
command_budget,
shell_dialect,
);
let tools = filter_advertised_tools(tools, persona_tools);
let tools = filter_tools_for_disposition(
smart_harness::advertise(tools, smart_harness),
prompt_disposition,
);
let (mut tools, hidden_tools) = crate::agentic::tools::select_exposed(
tools,
&exposure,
exposure_budget_tokens(send_budget, safe_context),
&std::collections::BTreeSet::new(),
estimation,
);
let mut tools_anthropic = anthropic_wire::tools_to_anthropic(&tools);
let mut tool_tokens = estimate_value_tokens(&tools, estimation);
let mut cal = compress_state.calibration.ratio(estimate_ratio);
let mut tool_tokens_real = calibrate_up(tool_tokens, cal);
preflight_irreducible_request(
&messages,
Some(&tools),
authoritative_request_budget(send_budget, send_budget_authoritative, mid_loop_trim_tokens),
cal,
estimation,
model,
)?;
let mut prompt_tracker = PromptTracker::new();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
let mut observed_paths = claim_check::ObservedPaths::default();
let mut observed_resolver = claim_check::workspace_resolver(workspace);
let mut narration_nudges: usize = 0;
let mut readonly_completion_retried = false;
let mut self_verify_nudges: usize = 0;
let mut verification_nudges: usize = 0;
const SELF_VERIFY_CAP: usize = 2;
let mut pending_plan_nudges: usize = 0;
let mut stale_file_nudges: usize = 0;
let mut unverified_exec_blocker_nudges: usize = 0;
let mut run_command_denial_observed = false;
let mut pending_rerun: Option<tools::PendingRerun> = None;
let mut capability_evidence = capability_check::Evidence::default();
let mut probe_correction_used = false;
let nudge_classifier = crate::NudgeClassifier::load_default();
let action_turn = action_nudges && crate::classifiers::user_turn_invites_action(active_task);
let exec_grounding_turn = action_nudges && prompt_disposition == PromptDisposition::Act;
let workflow_steerer = crate::WorkflowSteerer::load_default();
let mut workflow_runtime = WorkflowRuntimeState {
initiative: crate::initiative::effective_initiative(),
no_progress: crate::initiative::installed_no_progress(),
..Default::default()
};
workflow_runtime.set_progress_horizon(
workflow_steerer.progress_horizon(&workflow_classifier_text(&messages, "")),
);
let mut current_tool_round_limit = max_tool_rounds;
let turn_started = std::time::Instant::now();
let mut turn_heartbeat = TurnHeartbeat::default();
let mut uncorrelatable_batches: u32 = 0;
let mut selected_source = selected_target::Projection::default();
let mut observed_facts = semantic_pins::Projection::default();
'round_loop: for round in 0..usize::MAX {
let round = round.saturating_sub(output_recovery.skipped_rounds);
selected_source.refresh(
&mut messages,
step_ledger,
workspace,
&caveats.fs_read,
worktree_session,
false,
);
observed_facts.planning = output_budget::guidance(
Some(max_tokens),
step_ledger,
&selected_target::root(workspace, worktree_session),
&caveats.fs_read,
estimation,
);
semantic_pins::refresh(
&mut observed_facts,
smart_harness,
&mut messages,
prompt_context,
selected_source.current(),
&turn_claims,
workspace,
&caveats.fs_read,
)?;
if hidden_tools.append_promoted(&mut tools) {
tools_anthropic = anthropic_wire::tools_to_anthropic(&tools);
tool_tokens = estimate_value_tokens(&tools, estimation);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
let turn_elapsed = turn_started.elapsed();
if let Some(line) = turn_heartbeat.notice(turn_elapsed, round, current_tool_round_limit) {
print_newt(&line, color, false);
}
dismiss_completed_spill(&completed_spill_renderer);
if !workflow_runtime.admit_round(
round,
&mut current_tool_round_limit,
workflow_grace_rounds,
) {
break;
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
if drain_steering_into(steering, &mut messages, color) > 0 {
workflow_runtime.note_operator_steering();
}
if round > 0 && color {
execute!(
io::stdout(),
SetForegroundColor(CtColor::DarkGrey),
Print("…\n"),
ResetColor
)
.ok();
}
if round > 0 {
no_progress_gate!(
workflow_runtime,
action_nudges,
messages,
end_reason,
solve_obs,
accumulated_usage,
hallucination_count,
smart_harness,
(
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification
),
|harness| harness.record_messages(&messages)?
);
}
if round > 0 && action_nudges {
if let Some(ptr) = step_ledger.and_then(plan_reseat_pointer) {
messages.push(serde_json::json!({ "role": "user", "content": ptr }));
}
let remaining = (workflow_grace_rounds == 0)
.then_some(current_tool_round_limit.saturating_sub(round + 1));
if let Some(nudge) = workflow_runtime.action_forcing_nudge(remaining, step_ledger, None)
{
if debug {
print_debug(
&format!(
"initiative[{}]: forcing action, read-only budget spent (round {round})",
workflow_runtime.initiative
),
color,
);
}
messages.push(serde_json::json!({ "role": "user", "content": nudge }));
}
}
{
let advertised_tools = tools_supported.then_some(&tools);
let (_, wire_messages) = anthropic_wire::anthropic_wire_messages(&messages)?;
let current = full_message_request_pressure_tokens(
prompt_tracker.current(&messages, advertised_tools, cal, estimation),
&wire_messages,
advertised_tools,
cal,
estimation,
);
let message_tokens = estimate_tokens(&messages, estimation);
let has_authoritative_headroom = count_guard_has_headroom(
current,
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
max_ok_input,
);
let reasoning_tail_len =
compress::protected_reasoning_tail_len(&messages, reasoning_replay_scope);
if let Some(trigger) = compression_trigger(
compress::compression_message_count(&messages, reasoning_tail_len),
current,
message_tokens,
CompressionTriggerLimits {
count_threshold: mid_loop_trim_threshold,
token_threshold: mid_loop_trim_tokens,
send_budget,
tool_tokens: tool_tokens_real,
policy: compaction_trigger_policy,
has_authoritative_headroom,
},
) {
let pipeline_budget = if trigger.hard_budget {
calibrate_down(trigger.budget, cal)
} else {
trigger.budget
};
let token_fired = mid_loop_trim_tokens.is_some_and(|t| t > 0 && current > t);
let compression = smart_harness::compress(
CompressRequest {
messages: &messages,
budget: pipeline_budget,
max_messages: if reasoning_tail_len > 0 {
None
} else {
trigger.max_messages
},
replay_protected_tail_len: reasoning_tail_len,
task: active_task,
hard_budget: trigger.hard_budget,
authoritative: token_fired || send_budget_authoritative,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = outcome?;
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused {
anyhow::bail!("{}", refusal_bail_message(outcome.refusal, current, model));
}
if outcome.fired {
let suffix = if trigger.hard_budget && outcome.tokens_after > pipeline_budget {
", still over budget"
} else {
""
};
emit_compression_notice(
color,
outcome.tokens_before,
outcome.tokens_after,
outcome.action,
suffix,
);
if debug {
print_debug(
&format!(
"compression: {} → {} messages (budget ~{} tokens, \
+~{tool_tokens} tool-schema tokens ride along)",
messages.len(),
outcome.messages.len(),
pipeline_budget,
),
color,
);
}
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
anthropic_wire::anthropic_wire_messages(candidate).is_ok_and(
|(_, wire)| {
preflight_full_message_request(
&wire,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
},
)
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
pipeline_budget,
round,
trigger.primary_cause.artifact_reason(),
Some(&trigger),
send_budget_authoritative,
compress_state.floor_trend(),
color,
);
}
}
}
let (model_reply, printed_live, round_est_raw): (
anthropic_wire::AnthropicRound,
bool,
usize,
) = loop {
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
let (system, wire_messages) = anthropic_wire::anthropic_wire_messages(&messages)?;
preflight_full_message_request(
&wire_messages,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)?;
let round_est_raw = estimate_request_tokens(
&wire_messages,
tools_supported.then_some(&tools),
estimation,
);
let use_tools = tools_supported && !tools_anthropic.is_empty();
let body = anthropic_wire::build_messages_body(
model,
max_tokens,
system.as_deref(),
&wire_messages,
use_tools.then_some(tools_anthropic.as_slice()),
streaming_enabled,
);
let dispatch = anthropic_dispatch_round(
&dispatcher,
&body,
&messages,
streaming_enabled,
cancel,
attempts,
)
.await;
match dispatch {
Ok(None) => {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
))
}
Ok(Some((reply, printed))) => {
cw_retries = 0;
if reply.stop_reason.as_deref() == Some("pause_turn") && !pause_turn_retried {
pause_turn_retried = true;
accumulated_usage = merge_round_usage(accumulated_usage, reply.usage);
if debug {
print_debug("pause_turn — re-dispatching the same history once", color);
}
continue;
}
break (reply, printed, round_est_raw);
}
Err(e) => {
if tools_supported && is_tools_unsupported_error(&e) {
tools_supported = false;
if !tools_unsupported_notified {
tools_unsupported_notified = true;
print_newt(
&format!(
"{model} does not support tools — tools disabled for this session"
),
color,
false,
);
}
continue;
}
let overflow = cw_overflow::is_context_overflow(&e.to_string());
let refused_tokens = cw_overflow::parse_context_window_error(&e.to_string())
.map(|(tokens, _)| usize::try_from(tokens).unwrap_or(usize::MAX))
.unwrap_or_else(|| calibrate_up(round_est_raw, cal));
if overflow {
cal = compress_state.calibration.overflow(cal);
tool_tokens_real = calibrate_up(tool_tokens, cal);
}
if cw_retries < 2 {
let recovered_window = recover_cw_400.and_then(|f| f(&e, model, &today));
if let Some(recovered_budget) = recovered_window
.map(|context_window| {
recovered_input_budget(
context_window,
input_ceiling_pct,
Some(max_tokens),
effective_input_ceiling,
)
})
.or_else(|| {
cw_overflow::core_recover_overflow(
&e.to_string(),
send_budget,
None,
)
.map(|cap| cap as usize)
})
.or_else(|| {
overflow.then_some(
send_budget.unwrap_or_else(|| calibrate_up(round_est_raw, cal)),
)
})
.and_then(|budget| {
cw_overflow::recovery_target(
budget,
refused_tokens,
effective_input_ceiling,
)
})
{
if let Some(context_window) = recovered_window {
emit_context_window_400(&mut on_round_usage, context_window);
}
let new_budget = effective_input_ceiling
.map_or(recovered_budget, |c| recovered_budget.min(c));
let new_budget = authoritative_request_budget(
Some(new_budget),
true,
mid_loop_trim_tokens,
)
.unwrap_or(new_budget);
emit_overflow_notice(
color,
&display::OverflowReason::Refused {
request_estimate: Some(
refused_tokens.min(u32::MAX as usize) as u32
),
window: recovered_window,
trim_to: new_budget.min(u32::MAX as usize) as u32,
},
model,
cw_retries + 1,
);
send_budget = Some(new_budget);
effective_input_ceiling = Some(new_budget);
send_budget_authoritative = true;
let compression = context_recovery::compress(
CompressRequest {
messages: &messages,
budget: context_recovery::target(
&messages,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
estimation,
),
max_messages: None,
replay_protected_tail_len:
compress::protected_reasoning_tail_len(
&messages,
reasoning_replay_scope,
)
.max(context_recovery::protected_tool_tail(&messages)),
task: active_task,
hard_budget: true,
authoritative: true,
focus: None,
est: estimation,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
compaction_stage: None,
},
summarizer,
compress_state,
smart_harness,
);
let Some(outcome) = cancellable(cancel, compression).await else {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
let outcome = match outcome {
Ok(outcome) => outcome,
Err(error) => {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e.context(format!(
"context re-projection failed: {error}"
)));
}
};
if let Some(notice) = outcome.notice {
print_harness_notice(¬ice, color);
}
if outcome.action == CompressAction::Refused
|| !outcome.fired
|| outcome.tokens_after >= outcome.tokens_before
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
if outcome.fired {
messages = outcome.messages;
prompt_tracker.invalidate();
apply_post_compaction_continuation(
worktree_session,
&mut messages,
&mut narration_nudges,
outcome.action,
step_ledger,
prompt_context,
round > 0 && action_nudges,
|candidate| {
anthropic_wire::anthropic_wire_messages(candidate)
.is_ok_and(|(_, wire)| {
preflight_full_message_request(
&wire,
tools_supported.then_some(&tools),
authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
cal,
estimation,
model,
)
.is_ok()
})
},
);
record_compaction_artifact(
&mut repeat_calls,
artifact_sink,
artifact_context,
outcome.action,
outcome.tokens_before,
outcome.tokens_after,
calibrate_down(
new_budget.saturating_sub(tool_tokens_real),
cal,
),
round,
"context_window_400",
None,
false,
compress_state.floor_trend(),
color,
);
}
let projected = estimate_request_tokens(
&messages,
tools_supported.then_some(&tools),
estimation,
);
if projected >= round_est_raw
|| calibrate_up(projected, cal) > new_budget
{
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
Some(calibrate_up(projected, cal)),
)?;
cw_retries += 1;
continue;
}
}
if overflow {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
}
return Err(e);
}
}
};
let round_usage = model_reply.usage;
if let Some(u) = round_usage {
compress_state
.calibration
.observe(Some(u.input_tokens), round_est_raw);
cal = compress_state.calibration.ratio(estimate_ratio);
tool_tokens_real = calibrate_up(tool_tokens, cal);
prompt_tracker.record(u.input_tokens, messages.len());
}
accumulated_usage = merge_round_usage(accumulated_usage, round_usage);
let truncation_suspect = false;
if let (Some(u), Some(budget), false) = (round_usage, send_budget, truncation_suspect) {
let raised = capped_accepted_prompt_tokens(u.input_tokens, effective_input_ceiling);
if raised > budget {
send_budget = Some(raised);
if debug {
print_debug(
&format!(
"send budget raised to ~{raised} tokens (backend accepted \
{}-token prompt)",
u.input_tokens
),
color,
);
}
}
}
if model_reply.stop_reason.as_deref() == Some("max_tokens") {
if output_recovery.retry(
!model_reply.tool_uses.is_empty() || model_reply.unfinished_tool_use,
&mut messages,
smart_harness,
)? {
continue;
}
let text = finalize_final_text(
generation_bounds::output_limit_notice(model_reply.text, round_usage),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Failed);
}
if let Some(harness) = smart_harness {
harness.outcome(crate::TurnEndReason::Failed, &text)?;
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
if model_reply.stop_reason.as_deref() == Some("refusal") {
if !model_reply.text.is_empty() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
}
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Completed);
}
let streamed = printed_live && !model_reply.text.is_empty();
if model_reply.text.is_empty() {
let out =
FinalReply::harness("the model declined this request (refusal)".to_string());
let out = finalize_final_text(
out,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
observability::observe_harness_reply(&mut solve_obs);
return Ok((out, streamed, accumulated_usage, hallucination_count));
}
let out = finalize_final_text(
model_reply.text.clone(),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
let streamed = streamed && out == model_reply.text;
return Ok((out, streamed, accumulated_usage, hallucination_count));
}
let oa_content = model_reply.text.clone();
let separate_reasoning = {
let t = model_reply.thinking.trim();
(!t.is_empty()).then_some(t)
};
if debug && separate_reasoning.is_some() {
let n = separate_reasoning.map(str::len).unwrap_or(0);
print_debug(
&format!("reasoning ({n} chars) surfaced to the trace, not the answer"),
color,
);
}
let native_calls = (!model_reply.tool_uses.is_empty()).then_some(&model_reply.tool_uses);
let mut recovered = if native_calls.is_none() {
tool_recovery::recover_tool_calls(&oa_content)
} else {
tool_recovery::Recovery::default()
};
let recovered_identities = tool_recovery::assign_ids(
&mut recovered,
attempt_ledger.and_then(|l| l.lock().ok().and_then(|l| l.latest_primary())),
);
let tool_calls: Option<&Vec<serde_json::Value>> = match native_calls {
Some(t) => Some(t),
_ if !recovered.calls.is_empty() => Some(&recovered.calls),
_ => None,
};
let has_tools = tool_calls.map(|tc| !tc.is_empty()).unwrap_or(false);
let finish_reason = model_reply.stop_reason.as_deref();
if let Some(obs) = solve_obs.as_deref_mut() {
obs.behavior_signals
.push(observability::BehaviorSignal::ChatCompletionFinish {
round,
finish_reason: finish_reason.map(str::to_string),
});
}
let reasoning_text = separate_reasoning;
let reasoning_overflow = finish_reason == Some("max_tokens")
&& oa_content.is_empty()
&& reasoning_text.is_some()
&& !has_tools;
if reasoning_continuation_attempted && !reasoning_overflow {
if has_tools || !oa_content.is_empty() {
if let (Some(obs), Some(index)) =
(solve_obs.as_deref_mut(), reasoning_overflow_signal_index)
{
if let Some(signal) = obs.behavior_signals.get_mut(index) {
signal.mark_continuation_succeeded();
}
}
}
reasoning_overflow_signal_index = None;
}
if reasoning_overflow {
let has_round_budget = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
let can_continue = generation_policy
.allows_reasoning_continuation(reasoning_continuation_attempted, has_round_budget);
let resolving_existing_continuation =
reasoning_continuation_attempted && reasoning_overflow_signal_index.is_some();
if !resolving_existing_continuation {
if let Some(obs) = solve_obs.as_deref_mut() {
let index = obs.behavior_signals.len();
obs.behavior_signals
.push(observability::BehaviorSignal::ReasoningOverflow {
round,
reasoning_overflow_detected: true,
continuation_attempted: can_continue,
continuation_succeeded: false,
finish_reason: "max_tokens".into(),
reasoning_tokens_estimate: estimation.tokens_for_chars(
reasoning_text
.map(|reasoning| reasoning.chars().count())
.unwrap_or(0),
),
});
reasoning_overflow_signal_index = Some(index);
}
}
if can_continue {
print_newt(
"reasoning reached the output limit before an answer — continuing once",
color,
false,
);
messages.push(serde_json::json!({
"role": "assistant",
"content": oa_content,
}));
reasoning_continuation_attempted = true;
continue 'round_loop;
}
let reason = if resolving_existing_continuation {
"the bounded continuation also reached the output limit"
} else if reasoning_continuation_attempted {
"the turn already used its bounded continuation"
} else if !generation_policy.one_bounded_reasoning_continuation {
"the endpoint does not advertise bounded continuation"
} else if reasoning_replay_scope == crate::model_card::ReasoningReplayScope::Never {
"the endpoint does not allow current-turn reasoning replay"
} else {
"the turn has no remaining round budget"
};
print_newt(
&format!("reasoning overflow detected — {reason}"),
color,
false,
);
}
if let Some(obs) = solve_obs.as_deref_mut() {
if let Some(m) = model_reply.model.as_deref().filter(|m| !m.is_empty()) {
obs.served_model = Some(m.to_string());
}
let native = native_calls.is_some();
if let Some(sig) = observability::round_parse_signal(
round,
!oa_content.is_empty(),
native,
recovered.dialect,
recovered_identities.clone(),
) {
obs.parse_signals.push(sig);
}
}
if debug && !recovered.calls.is_empty() {
print_debug(
&format!(
"recovered {} tool call(s) from content (non-native emission)",
recovered.calls.len()
),
color,
);
}
if debug {
let excerpt: String = oa_content.chars().take(80).collect();
let tc_count = tool_calls.map(|tc| tc.len()).unwrap_or(0);
let usage_str = match round_usage {
Some(u) => format!("{} in / {} out", u.input_tokens, u.output_tokens),
None => "no usage".into(),
};
print_debug(
&format!(
"round {round}: tool_calls={tc_count} usage=[{usage_str}] content={excerpt:?}"
),
color,
);
}
if !has_tools {
if let Some(mut nudge) = capability_evidence.correction(
&oa_content,
&tools,
&mut probe_correction_used,
tools_supported
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !is_cancelled(cancel),
) {
if let Some(harness) = smart_harness {
nudge = harness.correct_answer(&oa_content, &nudge)?;
}
messages.push(serde_json::json!({"role":"assistant", "content":oa_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue 'round_loop;
}
if let Some(harness) = smart_harness {
let more = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
let control = harness
.classify(&oa_content, narration_nudge_cap, more, cancel)
.await?;
let control = harness
.verify_answer(
control,
self_verify::Concluding {
messages: &messages,
workspace,
task,
rounds_left: more,
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
smart_verify,
&oa_content,
)
.await?;
let (text, reason) = match control {
smart_harness::Control::Continue(nudge) => {
messages
.push(serde_json::json!({"role":"assistant", "content":oa_content}));
messages.push(serde_json::json!({"role":"user", "content":nudge}));
continue 'round_loop;
}
smart_harness::Control::Answer => (
FinalReply::from(oa_content.clone()),
crate::TurnEndReason::Completed,
),
smart_harness::Control::Finish { text, reason } => (text, reason),
};
if let Some(slot) = &mut end_reason {
**slot = Some(reason);
}
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(reason, &text)?;
return Ok((text, false, accumulated_usage, hallucination_count));
}
if recovered.tool_shaped {
hallucination_count += 1;
if debug {
print_debug(
"format-hallucination: tool call emitted as unrecoverable text",
color,
);
}
}
let content = oa_content.clone();
if !is_cancelled(cancel)
&& readonly_completion_pending(prompt_disposition, &nudge_classifier, &content)
{
let more_rounds = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
if retry_readonly_completion(
&mut messages,
&content,
None,
&mut readonly_completion_retried,
more_rounds,
) {
continue 'round_loop;
}
let out = readonly_completion_handoff(
&content,
&mut end_reason,
more_rounds,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
if content.is_empty() && debug {
print_debug(
"empty content with no tool calls — model produced nothing",
color,
);
}
let nudge_classification =
(!content.is_empty()).then(|| nudge_classifier.classify(&content));
let workflow_classifier_text = workflow_classifier_text(&messages, &content);
let workflow_hint = nudge_classification
.as_ref()
.filter(|classification| classification.is_plan_update())
.and_then(|_| workflow_steerer.plan_update_hint(&workflow_classifier_text));
let classifier_plan_direction = nudge_classification
.as_ref()
.filter(|classification| classification.is_plan_update())
.and_then(|_| nudge_classifier.direction_for(crate::NudgeClass::PlanUpdate));
let plan_nudge_hint =
combine_nudge_hints(classifier_plan_direction, workflow_hint.as_deref());
if should_ground_unverified_run_command_blocker(
&content,
&tools,
run_command_denial_observed,
exec_grounding_turn,
unverified_exec_blocker_nudges,
workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
),
) {
if debug {
print_debug(
"unsupported run_command blocker — requiring a ground-truth probe",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
run_command_ground_truth_nudge()
)
}));
unverified_exec_blocker_nudges += 1;
continue 'round_loop;
}
if !content.is_empty()
&& pending_plan_nudges < PENDING_PLAN_NUDGE_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_turn
{
let needs_plan_update = nudge_classification
.as_ref()
.is_some_and(|c| c.is_plan_update());
if let Some(nudge) = pending_plan_completion_nudge(
step_ledger,
needs_plan_update,
plan_nudge_hint.as_deref(),
) {
if debug {
print_debug(
"active plan has unfinished steps — nudging before final answer",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge)
}));
pending_plan_nudges += 1;
continue 'round_loop;
}
}
if !content.is_empty()
&& stale_file_nudges < STALE_FILE_NUDGE_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_nudges
&& looks_like_unverified_stale_file_blocker(&content)
{
if debug {
print_debug(
"unverified stale-file blocker — nudging to check ground truth",
color,
);
}
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
stale_file_ground_truth_nudge()
),
}));
stale_file_nudges += 1;
continue 'round_loop;
}
if !content.is_empty()
&& narration_nudges < narration_nudge_cap
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& action_turn
&& nudge_classification
.as_ref()
.is_some_and(|c| c.is_pending_action())
{
if debug {
print_debug(
"narrated intent with no tool call — nudging to act and continuing",
color,
);
}
strip_trailing_nudge_exchange(&mut messages);
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
let direction = if narration_nudges == 0 {
nudge_classification
.as_ref()
.and_then(|classification| {
nudge_classifier.direction_for(classification.class)
})
.map(workflow_guidance)
.unwrap_or_else(narration_action_nudge)
} else {
escalated_narration_action_nudge(
narration_nudges + 1,
narration_nudge_cap,
step_ledger,
)
};
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, direction),
}));
narration_nudges += 1;
continue 'round_loop;
}
let mut verification_stop = None;
if verification.result_aware() && action_nudges && !content.is_empty() {
match self_verify::conclude_turn(
self_verify::Concluding {
messages: &messages,
workspace,
task: active_task,
rounds_left: workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
),
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
verification_nudges,
)
.await
{
self_verify::Decision::Nudge(nudge) => {
strip_trailing_nudge_exchange(&mut messages);
messages
.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge),
}));
verification_nudges += 1;
continue 'round_loop;
}
self_verify::Decision::Stop(reason) => verification_stop = Some(reason),
self_verify::Decision::Accept => {}
}
} else if self_verify::enabled()
&& action_nudges
&& self_verify_nudges < SELF_VERIFY_CAP
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !content.is_empty()
{
let cmds = self_verify::commands_from_messages(&messages);
let checks = verification
.applicable_checks(workspace, active_task, &cmds)
.await
.unwrap_or_default();
if let Some(nudge) = self_verify::verify_gate_nudge(&checks, &cmds) {
if debug {
print_debug(
"concluding with unrun verification — self-verify nudge",
color,
);
}
strip_trailing_nudge_exchange(&mut messages);
messages.push(serde_json::json!({ "role": "assistant", "content": content }));
messages.push(serde_json::json!({
"role": "user",
"content": format!("{} {}", compress::LOOP_GUIDANCE_PREFIX, nudge),
}));
self_verify_nudges += 1;
continue 'round_loop;
}
}
if !content.is_empty() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
}
let accepted_reason = if content.is_empty() {
crate::TurnEndReason::Empty
} else if nudge_classification
.as_ref()
.is_some_and(|c| c.is_pending_action())
&& action_turn
{
if !workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
) {
crate::TurnEndReason::NarrationFinalRound
} else {
crate::TurnEndReason::NarrationCapExhausted
}
} else {
crate::TurnEndReason::Completed
};
let accepted_reason = verification_stop.unwrap_or(accepted_reason);
if debug && accepted_reason != crate::TurnEndReason::Completed {
print_debug(
&format!("no-tool reply accepted as final answer ({accepted_reason:?})"),
color,
);
}
if let Some(slot) = &mut end_reason {
**slot = Some(accepted_reason);
}
let streamed = printed_live && !content.is_empty();
if content.is_empty() {
let out = FinalReply::harness("(model returned an empty response — try rephrasing, or check the model with `newt doctor`)".to_string());
let out = finalize_final_text(
out,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
observability::observe_harness_reply(&mut solve_obs);
return Ok((out, streamed, accumulated_usage, hallucination_count));
}
let out = finalize_final_text(
content,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
let streamed = streamed && out == oa_content;
return Ok((out, streamed, accumulated_usage, hallucination_count));
}
messages.push(serde_json::json!({
"role": "assistant",
"content": oa_content,
"tool_calls": tool_calls.unwrap(),
}));
let mut round_modified_workspace = false;
let mut round_progress = false;
let mut round_succeeded = false;
let tcs = tool_calls.unwrap();
let extracted: Vec<(Option<&str>, Option<&str>, &serde_json::Value)> = tcs
.iter()
.map(|tc| {
if tc["function"].is_null() {
(tc["id"].as_str(), tc["name"].as_str(), &tc["input"])
} else {
(
tc["id"].as_str(),
tc["function"]["name"].as_str(),
&tc["function"]["arguments"],
)
}
})
.collect();
let validated = match tools::validate_tool_call_batch(&extracted, true) {
Ok(v) => {
uncorrelatable_batches = 0;
Some(v)
}
Err(tools::BatchRejection::CorrelationImpossible(reason)) => {
let reask = tools::reask_uncorrelatable(
&mut uncorrelatable_batches,
&reason,
smart_harness,
tool_events.as_deref_mut(),
)?;
if let Some(turn) = messages.last_mut() {
tools::withdraw_tool_calls(turn);
}
messages.push(serde_json::json!({"role": "user", "content": reask}));
continue 'round_loop;
}
Err(tools::BatchRejection::ContentInvalid(reason)) => {
uncorrelatable_batches = 0;
if let Some(harness) = smart_harness {
harness.reject_tools(&reason, true)?;
}
for tc in tcs {
let id = tc["id"].as_str().unwrap_or("");
print_synthetic_tool_result(
"(rejected tool-call batch)",
&serde_json::Value::Null,
workspace,
&reason,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(
"(rejected tool-call batch)",
&serde_json::Value::Null,
false,
Some(0),
));
}
smart_harness::push_tool_message(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": format!("tool-call batch rejected before execution: {reason}"),
}),
smart_harness,
true,
"(rejected tool-call batch)",
)?;
}
None
}
};
let batch = smart_harness
.zip(validated.as_ref())
.map(|(harness, calls)| harness.tool_batch(calls, &messages))
.transpose()?;
let mut tool_warnings = Vec::new();
if validated.is_some() {
emit_accepted(
&mut on_round_usage,
round_usage,
truncation_suspect,
round_est_raw,
);
commit_tool_round_narration(
&oa_content,
printed_live,
completed_spill_renderer.as_deref(),
color,
);
}
for (call_index, (tc, vc)) in tcs.iter().zip(validated.iter().flatten()).enumerate() {
let id = vc.call_id.as_str();
let name = vc.name.as_str();
let args = vc.args.clone();
announce_tool_activity(name);
dismiss_completed_spill(&completed_spill_renderer);
if debug && tc["function"].is_null() {
print_debug(
&format!(
"tool call in flat native format inside the executed batch \
(no `function` key) — name={name:?}"
),
color,
);
}
if trace {
print_trace(
&format!(
"raw tool_call element: {}",
serde_json::to_string(tc).unwrap_or_else(|_| "?".into())
),
color,
);
}
let mcp_handles = mcp.handles(name);
if debug {
print_debug(
&format!("dispatching tool name={name:?} mcp_handles={mcp_handles}"),
color,
);
}
if is_hallucination(name, &args) {
hallucination_count += 1;
}
if let Some(content) =
repeat_calls.cached_read(name, &args, ReadScope { workspace, caveats })
{
print_synthetic_tool_result(
name,
&args,
workspace,
&content,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_cache(name, &args));
}
let offloaded =
maybe_offload_tool_result(name, content, tool_offload, spill_store, disclosure);
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": offloaded,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
if let Some(steer) = repeat_calls.repeat_steer(name, &args) {
print_synthetic_tool_result(
name,
&args,
workspace,
&steer,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(name, &args, false, Some(0)));
}
smart_harness::push_tool_resolution(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": steer,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
let invocation = batch
.as_ref()
.map(|batch| batch.start(call_index, disclosure))
.transpose()?;
record_organic_note_use(name, ¬e_sink, &mut note_nudge);
let before_change =
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await;
ledger_note_write(write_ledger, name, &args, workspace);
let tool_t0 = std::time::Instant::now();
let execution = std::sync::OnceLock::new();
let retry_authorized = std::sync::OnceLock::new();
let routed_to = std::sync::OnceLock::new();
let governed_pr = std::sync::OnceLock::new();
let report_capture = std::sync::Mutex::new(observed_report::Capture::default());
let command_directory = std::sync::OnceLock::new();
let attribution_epoch = AttributionEpoch::new(attribution, git_tool, model, name);
let result = if tools::is_context_remaining_call(name) {
let report = budget::render_context_budget(
prompt_tracker.current(&messages, Some(&tools), cal, estimation),
effective_input_ceiling,
num_ctx,
input_ceiling_pct,
low_budget_pct,
);
if let Some(invocation) = &invocation {
invocation.host();
invocation.observe(&report, None, spill_store)?;
}
print_synthetic_tool_result(
name,
&args,
workspace,
&report,
color,
&completed_spill_renderer,
);
report
} else {
let tool_evidence = tool_events
.as_deref()
.map(|e| capability_check::Evidence::from_events(e));
let Some(result) = tools::execute_tool_with_collaborators(
name,
&args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
tools::ToolCollaborators {
command_budget,
shell_dialect,
read_history: Some(&mut read_history),
default_command_cwd,
worktree_session,
invocation: invocation.as_ref(),
build_check_cmd: build_check_cmd.as_deref(),
tool_evidence: tool_evidence.as_ref(),
note_sink: note_sink
.as_deref_mut()
.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
prompt_context: Some(prompt_context),
artifact_context,
artifact_sink,
permission_gate: permission_gate
.as_deref_mut()
.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav,
experience_store,
step_ledger,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
spill_store,
persona_tools,
hidden_tools: Some(&hidden_tools),
live_tool_output: live_tool_output.clone(),
completed_spill_renderer: completed_spill_renderer.clone(),
execution: Some(&execution),
retry_authorized: Some(&retry_authorized),
governed_pr: Some(&governed_pr),
report_capture: Some(&report_capture),
command_directory: Some(&command_directory),
routed_to: Some(&routed_to),
pending_rerun: Some(&mut pending_rerun),
},
tool_offload,
prompt_disposition,
cancel,
)
.await?
else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
result
};
if debug {
dismiss_completed_spill(&completed_spill_renderer);
let excerpt: String = result.chars().take(120).collect();
print_debug(&format!("tool result: {excerpt:?}"), color);
}
let ok = tools::tool_ok(&result, execution.get().copied());
round_succeeded |= ok;
attribution_epoch.finish(&args, ok);
run_command_denial_observed |= run_command_result_is_denial(name, ok, &result);
capability_evidence.record(name, ok, execution.get().copied());
let after_change = if before_change.is_some() {
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await
} else {
None
};
round_modified_workspace |=
workflow_runtime.record_workspace_change(before_change, after_change);
round_progress |= workflow_runtime.record_observation(
name,
&args,
&result,
ok,
execution.get().copied(),
routed_to.get(),
);
repeat_calls.record(
name,
&args,
ok,
&result,
execution.get().copied(),
ReadScope { workspace, caveats },
);
repeat_calls.permit_authorized_retry(name, &args, &retry_authorized);
append_clean_build_warning(
if batch.is_some() {
&mut tool_warnings
} else {
&mut messages
},
&mut clean_build,
&args,
None,
)?;
record_completed_tool_event(
&mut tool_events,
name,
&args,
ok,
execution.get().copied(),
tool_t0,
);
turn_claims.observe_report(
workspace,
&caveats.fs_read,
&report_capture.lock().expect("report capture"),
execution.get().copied(),
governed_pr.get(),
);
if let Some(outcome) = governed_pr.get() {
verification.record_publication_outcome(outcome);
}
if ok {
if let Some(directory) = command_directory.get() {
verification.record_command_directory(directory);
}
}
verification
.observe_routed(
name,
&args,
ok,
execution.get().copied(),
workspace,
routed_to.get(),
)
.await;
record_phantom_reach(
&mut phantom_reaches,
name,
&args,
&result,
ok,
advertise_team,
);
observed_paths.record(&result, &mut observed_resolver);
smart_harness::push_tool_return(
&mut messages,
serde_json::json!({
"role": "tool",
"tool_call_id": id,
"content": smart_harness::tool_result(name, result, tool_offload, spill_store, disclosure, invocation.as_ref())?,
}),
invocation.as_ref(),
)?;
}
for message in tool_warnings {
smart_harness::push_tool_message(
&mut messages,
message,
smart_harness,
true,
"build guidance",
)?;
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
}
if round_succeeded {
output_recovery.completed_tool_round();
}
workflow_runtime.record_round_outcome(round_modified_workspace, round_progress);
if let Some(text) = pending_plan_approval_handoff(
plan_mode_control,
PlanRoundFacts {
disposition: prompt_disposition,
step_ledger,
plan_at_turn_start: &plan_at_turn_start,
progressed: round_modified_workspace || round_progress,
},
smart_harness,
cancel,
&mut end_reason,
)? {
dismiss_completed_spill(&completed_spill_renderer);
let text = finalize_final_text(
FinalReply::harness(text),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((text, false, accumulated_usage, hallucination_count));
}
}
dismiss_completed_spill(&completed_spill_renderer);
let protected_head = protected_prompt_head_len(&messages, prompt_read::ACTIVE_PROMPT_PREFIX);
let replay_protected_tail_len =
compress::protected_reasoning_tail_len(&messages, reasoning_replay_scope);
let trimmed = trim_for_summary(&messages, protected_head, 6.max(replay_protected_tail_len));
let progress = cap_exit_progress(worktree_session, step_ledger, scratchpad_store);
let cap_reason = verification
.cap_exit_reason(
workspace,
if smart_harness.is_some() {
smart_verify
} else {
action_nudges
},
current_tool_round_limit,
solve_obs.as_deref_mut(),
)
.await;
if let Some(hit) = &mut round_cap_hit {
**hit = true;
}
if let Some(harness) = smart_harness {
harness.record_messages(&messages)?;
let text = cap_exit_fallback(
current_tool_round_limit,
accumulated_usage,
repeat_calls.total_failures(),
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(cap_reason, &text)?;
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let cap = CapExit {
max_tool_rounds: current_tool_round_limit,
accumulated: accumulated_usage,
wasted_calls: repeat_calls.total_failures(),
progress,
observed: observed_paths.into_vec(),
request_budget: authoritative_request_budget(
send_budget,
send_budget_authoritative,
mid_loop_trim_tokens,
),
calibration: cal,
estimation,
ollama_options: None,
prompt_measurement: Default::default(),
fell_back: Default::default(),
};
let result = final_summary_anthropic(
&client,
&messages_url,
model,
api_key,
trimmed,
generation_policy,
&cap,
attempts,
)
.await;
let (text, streamed, usage) = cap.recover_rejection(
result,
&mut solve_obs,
compress_state,
current_tool_round_limit,
cw_retries + 1,
)?;
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
Ok((text, streamed, usage, hallucination_count))
}
fn responses_api_selected() -> bool {
std::env::var("NEWT_OPENAI_API")
.ok()
.is_some_and(|v| v.eq_ignore_ascii_case("responses"))
}
fn responses_reasoning_field(
effort: Option<crate::model_card::ReasoningEffort>,
) -> Option<serde_json::Value> {
effort.map(|value| serde_json::json!({ "effort": value }))
}
fn tools_to_responses(tools: &serde_json::Value) -> Vec<serde_json::Value> {
tools
.as_array()
.map(|arr| {
arr.iter()
.map(|t| {
let f = &t["function"];
if f.is_object() {
let mut tool = serde_json::json!({
"type": "function",
"name": f["name"],
"description": f["description"],
"parameters": f["parameters"],
});
if let Some(strict) = f.get("strict").filter(|s| !s.is_null()) {
tool["strict"] = strict.clone();
}
tool
} else {
t.clone()
}
})
.collect()
})
.unwrap_or_default()
}
pub async fn openai_responses_complete(
ctx: ChatCtx<'_>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
openai_responses_complete_with_prompt(ctx, None, None, mcp).await
}
pub async fn openai_responses_complete_with_prompt(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
openai_responses_complete_with_prompt_and_artifacts(
ctx,
turn_prompt_context,
prompt_source,
None,
None,
mcp,
)
.await
}
fn responses_source_messages(
instructions: Option<&str>,
input: &[serde_json::Value],
) -> Vec<serde_json::Value> {
let mut messages = Vec::with_capacity(input.len() + 1);
if let Some(text) = instructions {
messages.push(serde_json::json!({"role":"system", "content":text}));
}
messages.extend_from_slice(input);
messages
}
#[allow(clippy::too_many_arguments)]
async fn dispatch_responses_json<'a>(
client: &reqwest::Client,
url: &str,
api_key: Option<&str>,
validated: &responses_wire_validation::ValidatedResponsesRequest,
retry: &RetryPolicy,
color: bool,
smart_harness: Option<(&smart_harness::SmartHarness, &[serde_json::Value])>,
attempts: Option<attempt_capture::AttemptScope<'a>>,
) -> anyhow::Result<(serde_json::Value, Option<attempt_capture::Attempt<'a>>)> {
let body = validated.body();
let body_bytes = smart_harness
.map(|(h, messages)| h.prepare_with_messages(body, "responses", messages))
.transpose()?;
dispatch_with_decoder(
retry,
attempts,
|| async {
let mut req = match &body_bytes {
Some(bytes) => client
.post(url)
.header(reqwest::header::CONTENT_TYPE, "application/json")
.body(bytes.clone()),
None => client.post(url).json(body),
};
if let Some(key) = api_key {
req = req.bearer_auth(key);
}
Ok(req)
},
"inference endpoint",
|attempt, delay, error| {
print_retry_indicator(attempt, retry.max_retries, delay, error, color);
},
smart_harness.map(|(h, _)| h),
|bytes| Ok(serde_json::from_slice(bytes)?),
)
.await
}
async fn dispatch_with_decoder<'a, Fut>(
retry: &RetryPolicy,
attempts: Option<attempt_capture::AttemptScope<'a>>,
request: impl Fn() -> Fut,
http_error_prefix: &str,
on_retry: impl FnMut(u32, std::time::Duration, &anyhow::Error),
smart_harness: Option<&smart_harness::SmartHarness>,
decode: fn(&[u8]) -> anyhow::Result<serde_json::Value>,
) -> anyhow::Result<(serde_json::Value, Option<attempt_capture::Attempt<'a>>)>
where
Fut: std::future::Future<Output = anyhow::Result<reqwest::RequestBuilder>>,
{
with_backoff_notify_error(
retry,
|| async {
let (resp, attempt) =
attempt_capture::send(attempts, "primary", request().await?, "request failed")
.await?;
let json = smart_harness::response_with_decoder(
resp,
smart_harness,
http_error_prefix,
decode,
)
.await
.inspect_err(|error| attempt_capture::failed(attempt.as_ref(), error))?;
Ok((json, attempt))
},
on_retry,
)
.await
}
fn complete_responses_attempt(
attempt: Option<&attempt_capture::Attempt<'_>>,
json: &serde_json::Value,
decoded: &Result<
crate::responses_wire::DecodedResponse,
crate::responses_wire::ResponseDecodeError,
>,
) {
use crate::attempts::AttemptState;
use crate::responses_wire::ResponseDecodeError as E;
let state = match decoded {
Ok(_)
| Err(E::Refused { .. } | E::Incomplete { .. } | E::MixedRefusalAndToolCalls { .. }) => {
AttemptState::Ok
}
Err(E::Malformed(_)) if json["status"].as_str() == Some("completed") => AttemptState::Ok,
Err(E::Malformed(_) | E::ProviderError(_) | E::Failed(_) | E::NonTerminal(_)) => {
AttemptState::Failed
}
};
attempt_capture::finish(
attempt,
state,
crate::responses_wire::decode_usage(&json["usage"]),
);
}
async fn openai_responses_complete_with_prompt_and_artifacts(
ctx: ChatCtx<'_>,
turn_prompt_context: Option<&crate::TurnPromptContext>,
prompt_source: Option<&dyn PromptSource>,
artifact_source: Option<&dyn artifact_read::ArtifactSource>,
artifact_sink: Option<&dyn artifact_read::PromptArtifactSink>,
mcp: &mut dyn McpTools,
) -> anyhow::Result<(String, bool, Option<crate::TokenUsage>, u32)> {
let ChatCtx {
shell_dialect,
command_budget,
verify_outcomes,
smart_harness,
url,
model,
kind: _,
api_key,
messages: mem_messages,
task,
workspace,
default_command_cwd,
worktree_session,
color,
markdown: _,
tool_offload,
spill_store,
disclosure,
compaction_store,
scratchpad,
scratchpad_store,
code_search,
where_is,
nav,
exposure,
experience_store,
step_ledger,
caveats,
persona_tools,
cognition,
chat_completions_capability: _,
responses_capability,
openai_api: _,
output_allowance,
overflow_retry: _,
attempt_ledger,
reasoning_replay_scope: _,
max_tool_rounds,
workflow_grace_rounds,
narration_nudge_cap,
action_nudges,
prompt_disposition,
prompt_intake,
tool_output_lines,
debug,
trace,
num_ctx,
connect_timeout_secs,
inference_timeout_secs,
mid_loop_trim_threshold: _,
compaction_trigger_policy: _,
mid_loop_trim_tokens,
max_ok_input,
build_check_cmd,
safe_context,
recover_cw_400,
mut note_sink,
mut note_nudge,
recall_source,
memory_source,
summarizer,
compress_state,
mut tool_events,
mut phantom_reaches,
mut end_reason,
mut round_cap_hit,
mut solve_obs,
mut permission_gate,
mut on_round_usage,
estimate_ratio,
estimation,
summary_input_cap_floor_chars,
rewrites_history,
input_ceiling_pct,
low_budget_pct,
exec_floor,
write_ledger,
attribution,
cancel,
run_allowance,
live_tool_output,
git_tool,
crew_runner,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
steering,
completed_spill_renderer,
emits_leading_reasoning: _,
} = ctx;
let mut generation_window = num_ctx;
let generation_policy = generation_policy::GenerationPolicy::resolve_responses(
cognition,
output_allowance,
&responses_capability,
)?;
if let Some(obs) = solve_obs.as_deref_mut() {
obs.responses_capability = Some(responses_capability);
obs.reasoning_effort = generation_policy.reasoning_effort;
}
let _completed_spill_guard = CompletedSpillDismissGuard(&completed_spill_renderer);
if let Some(harness) = smart_harness {
harness.start_turn()?;
}
let spill_store = smart_harness.map(|h| h.spill_store()).or(spill_store);
let tool_offload = tool_offload || smart_harness.is_some();
let smart_verify = action_nudges && prompt_disposition == PromptDisposition::Act;
let action_nudges = action_nudges && smart_harness.is_none();
let max_tool_rounds = prompt_disposition.tool_round_limit(max_tool_rounds);
let mut local_compress_state = CompressState::new();
let compress_state = match compress_state {
Some(s) => s,
None => &mut local_compress_state,
};
let client = reqwest::Client::builder()
.connect_timeout(std::time::Duration::from_secs(connect_timeout_secs))
.read_timeout(std::time::Duration::from_secs(inference_timeout_secs))
.build()?;
let responses_url = format!("{}/v1/responses", url.trim_end_matches('/'));
let retry = tui_retry_policy(url);
let advertise_save_note = note_sink.is_some();
let advertise_recall = recall_source.is_some();
let advertise_memory_fetch = memory_source.is_some();
let advertise_scratchpad = scratchpad_store.is_some() && scratchpad;
let advertise_code_search = code_search.is_some();
let advertise_experiential = experience_store.is_some();
let advertise_scheduled = step_ledger.is_some();
let advertise_git = false;
let advertise_team = crew_runner.is_some();
let advertise_operating_mode = operating_mode_control.is_some();
let advertise_plan_mode = plan_mode_control.is_some();
let advertise_plan_mode_active =
plan_mode_control.is_some_and(|control| control.is_plan_mode());
let mem_messages = observed_report::replay_messages(mem_messages);
let mut msgs_json: Vec<serde_json::Value> = mem_messages
.iter()
.map(|m| serde_json::json!({"role": m.role.as_str(), "content": m.content}))
.collect();
if let Some(harness) = smart_harness {
msgs_json = harness.initial_messages(msgs_json)?;
}
if let (Some(session), Some(context)) = (worktree_session, turn_prompt_context) {
session
.report
.lock()
.expect("report state")
.begin(context.active().root_prompt_id());
}
let ephemeral_prompt = prompt_read::headless_prompt_fallback(turn_prompt_context, task);
let turn_prompt_context = turn_prompt_context.or(ephemeral_prompt.as_ref());
let prompt_context =
prompt_read::PromptReadContext::new(turn_prompt_context, task, prompt_source);
let artifact_context = turn_prompt_context
.map(|turn| artifact_read::ArtifactReadContext::from_turn(turn, artifact_source));
prompt_read::ensure_active_prompt_card(&mut msgs_json, prompt_context, prompt_intake);
let attempt_turn = turn_prompt_context
.map(|turn| turn.active_operator_prompt().id().to_string())
.unwrap_or_default();
let fallback_ledger = std::sync::Mutex::new(crate::attempts::AttemptLedger::default());
let attempt_ledger = attempt_ledger.or_else(|| run_allowance.map(|_| &fallback_ledger));
let attempts = attempt_ledger.map(|ledger| attempt_capture::AttemptScope {
ledger,
turn: &attempt_turn,
model,
backend: url,
cancel,
run_allowance,
});
let exec_grounding_turn = action_nudges && prompt_disposition == PromptDisposition::Act;
let (mut instructions, mut input) = crate::responses_wire::build_responses_input(&msgs_json);
if smart_harness.is_some() {
if let Some(text) = instructions.take() {
input.insert(0, serde_json::json!({"role":"system", "content":text}));
}
}
let tools_chat = merged_tool_definitions(
mcp,
advertise_save_note,
advertise_recall,
advertise_memory_fetch,
advertise_git.then_some(&caveats.fs_read),
advertise_team,
advertise_scratchpad,
advertise_code_search,
advertise_experiential,
advertise_scheduled,
advertise_operating_mode,
advertise_plan_mode,
advertise_plan_mode_active,
command_budget,
shell_dialect,
);
let tools_chat = filter_advertised_tools(tools_chat, persona_tools);
let tools_chat = filter_tools_for_disposition(
smart_harness::advertise(tools_chat, smart_harness),
prompt_disposition,
);
let mut budget_state = resolve_responses_budget(
num_ctx,
safe_context,
max_ok_input,
mid_loop_trim_tokens,
input_ceiling_pct,
None,
generation_policy.output_allowance,
);
observability::observe_output_allowance(&mut solve_obs, budget_state.output_reserve(), false);
let (tools_chat, hidden_tools) = crate::agentic::tools::select_exposed(
tools_chat,
&exposure,
budget_state.exposure_budget(),
&std::collections::BTreeSet::new(),
estimation,
);
let mut tools_chat =
crate::agentic::tools::select_openai_compatible_tools(tools_chat, &hidden_tools);
let mut tools = tools_to_responses(&tools_chat);
let mut tools_for_estimate = serde_json::Value::Array(tools.clone());
let mut cal = compress_state.calibration.ratio(estimate_ratio);
budget_state.set_tool_schema_tokens(calibrate_up(
estimate_value_tokens(&tools_for_estimate, estimation),
cal,
));
let mut cw_retries: u32 = 0;
let mut output_recovery = output_limit_recovery::Recovery::default();
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
preflight_irreducible_request(
&msgs_json,
Some(&tools_for_estimate),
budget_state.actionable_input_budget(),
cal,
estimation,
model,
)?;
let mut accumulated_usage: Option<crate::TokenUsage> = None;
let mut hallucination_count: u32 = 0;
let result_aware = self_verify::enabled() && verify_outcomes;
let mut repeat_calls = RepeatCallGuard::default();
let plan_at_turn_start = step_ledger
.map(|ledger| ledger.snapshot())
.unwrap_or_default();
let mut read_history = tools::ReadHistory::default();
let mut verification = self_verify::VerificationLedger::for_turn(task, result_aware);
let turn_claims =
claim_check::TurnClaims::capture(workspace, &caveats.fs_read, worktree_session)
.with_evidence(observed_report::review::Evidence {
sink: artifact_sink,
context: artifact_context,
spill: spill_store,
});
let mut clean_build = crate::loop_watch::CleanBuildWatch::default();
let mut tools_supported = true;
let mut tools_unsupported_notified = false;
let mut unverified_exec_blocker_nudges: usize = 0;
let mut run_command_denial_observed = false;
let mut pending_rerun: Option<tools::PendingRerun> = None;
let mut capability_evidence = capability_check::Evidence::default();
let mut probe_correction_used = false;
let mut readonly_completion_retried = false;
let nudge_classifier = crate::NudgeClassifier::load_default();
let mut observed_paths = claim_check::ObservedPaths::default();
let mut observed_resolver = claim_check::workspace_resolver(workspace);
let reasoning = responses_reasoning_field(generation_policy.reasoning_effort);
let build_body = |input: &[serde_json::Value], tools: &[serde_json::Value]| {
let source = responses_source_messages(instructions.as_deref(), input);
let (wire_instructions, input) = crate::responses_wire::build_responses_input(&source);
let mut body = serde_json::json!({
"model": model,
"input": input,
"stream": false,
"store": crate::responses_wire::STORE_RESPONSE_SERVER_SIDE,
});
if let Some(ins) = &wire_instructions {
body["instructions"] = serde_json::json!(ins);
}
if let Some(reasoning) = &reasoning {
body["reasoning"] = reasoning.clone();
}
if !tools.is_empty() {
body["tools"] = serde_json::json!(tools);
body["tool_choice"] = serde_json::json!("auto");
}
body
};
let turn_started = std::time::Instant::now();
let mut turn_heartbeat = TurnHeartbeat::default();
let mut workflow_runtime = WorkflowRuntimeState {
no_progress: crate::initiative::installed_no_progress(),
..Default::default()
};
let mut uncorrelatable_batches: u32 = 0;
let mut current_tool_round_limit = max_tool_rounds;
let mut selected_source = selected_target::Projection::default();
let mut observed_facts = semantic_pins::Projection::default();
for round in 0..usize::MAX {
let round = round.saturating_sub(output_recovery.skipped_rounds);
selected_source.refresh(
&mut input,
step_ledger,
workspace,
&caveats.fs_read,
worktree_session,
true,
);
observed_facts.planning = output_budget::guidance(
Some(generation_bounds::output_cap(
generation_policy.output_allowance,
None,
0,
)?),
step_ledger,
&selected_target::root(workspace, worktree_session),
&caveats.fs_read,
estimation,
);
semantic_pins::refresh(
&mut observed_facts,
smart_harness,
&mut input,
prompt_context,
selected_source.current(),
&turn_claims,
workspace,
&caveats.fs_read,
)?;
if hidden_tools.append_promoted(&mut tools_chat) {
tools = tools_to_responses(&tools_chat);
tools_for_estimate = serde_json::Value::Array(tools.clone());
budget_state.set_tool_schema_tokens(calibrate_up(
estimate_value_tokens(&tools_for_estimate, estimation),
cal,
));
}
let turn_elapsed = turn_started.elapsed();
if let Some(line) = turn_heartbeat.notice(turn_elapsed, round, current_tool_round_limit) {
print_newt(&line, color, false);
}
dismiss_completed_spill(&completed_spill_renderer);
if !workflow_runtime.admit_round(
round,
&mut current_tool_round_limit,
workflow_grace_rounds,
) {
break;
}
if is_cancelled(cancel) {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
}
if drain_steering_into(steering, &mut input, color) > 0 {
workflow_runtime.note_operator_steering();
}
if round > 0 {
no_progress_gate!(
workflow_runtime,
action_nudges,
input,
end_reason,
solve_obs,
accumulated_usage,
hallucination_count,
smart_harness,
(
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification
),
|harness| harness.record_responses_messages(instructions.as_deref(), &input)?
);
}
let (json, attempt) = loop {
let mut proactive_rejection: Option<CompactionRejection> = None;
if let Some(budget) = budget_state.actionable_input_budget() {
let before = estimate_responses_request_real_tokens(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
estimation,
cal,
);
if before > budget {
let compression = compact_responses_input(
Some(&observed_facts),
worktree_session,
&mut input,
instructions.as_deref(),
tools_supported.then_some(tools.as_slice()),
Some(budget),
budget_state.compaction_budget(cal, tools_supported),
cal,
estimation,
task,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
summarizer,
compress_state,
color,
smart_harness,
false,
);
let Some(outcome) = cancellable(cancel, compression).await else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
if matches!(outcome, ResponsesCompaction::Compacted) {
repeat_calls.release_read_memos();
}
proactive_rejection = outcome.rejection();
}
}
if let Some(harness) = smart_harness {
harness.record_responses_messages(instructions.as_deref(), &input)?;
}
let mut body = build_body(&input, if tools_supported { &tools } else { &[] });
let output_cap = generation_bounds::apply_responses(
&mut body,
generation_policy.output_allowance,
generation_window,
estimate_responses_request_real_tokens(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
estimation,
cal,
),
);
let policy = responses_wire_validation::ResponsesWirePolicy {
store: crate::responses_wire::STORE_RESPONSE_SERVER_SIDE,
tools_permitted: true,
model,
authoritative_budget: budget_state.actionable_input_budget(),
calibration: cal,
estimation,
spill: spill_store,
compaction: compaction_store,
};
let validated = responses_wire_validation::validate_responses_request(&body, &policy)
.map_err(|e| match proactive_rejection.take() {
Some(reason) => anyhow::Error::new(e).context(reason.to_string()),
None => anyhow::Error::new(e),
})?;
observability::observe_output_allowance(&mut solve_obs, Some(output_cap?), true);
let source = responses_source_messages(instructions.as_deref(), &input);
let dispatch = dispatch_responses_json(
&client,
&responses_url,
api_key,
&validated,
&retry,
color,
smart_harness.map(|h| (h, source.as_slice())),
attempts,
)
.await;
match dispatch {
Ok(j) => {
cw_retries = 0;
break j;
}
Err(e) => {
if tools_supported && is_tools_unsupported_error(&e) {
tools_supported = false;
if !tools_unsupported_notified {
tools_unsupported_notified = true;
print_newt(
&format!(
"{model} does not support tools — tools disabled for this session"
),
color,
false,
);
}
continue;
}
let overflow = cw_overflow::is_context_overflow(&e.to_string());
let round_est_raw = estimate_responses_request_tokens(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
estimation,
);
let refused_tokens = cw_overflow::parse_context_window_error(&e.to_string())
.map(|(tokens, _)| usize::try_from(tokens).unwrap_or(usize::MAX))
.unwrap_or_else(|| calibrate_up(round_est_raw, cal));
if overflow {
cal = compress_state.calibration.overflow(cal);
budget_state.set_tool_schema_tokens(calibrate_up(
estimate_value_tokens(&tools_for_estimate, estimation),
cal,
));
}
if cw_retries < 2 {
let recovered_window = recover_cw_400.and_then(|f| f(&e, model, &today));
if let Some(recovered_budget) = recovered_window
.map(|context_window| {
budget_state.recovered_budget_for_window(context_window)
})
.or_else(|| {
cw_overflow::core_recover_overflow(
&e.to_string(),
budget_state.soft_send_budget(),
None,
)
.map(|cap| cap as usize)
})
.or_else(|| {
overflow.then_some(
budget_state
.soft_send_budget()
.unwrap_or_else(|| calibrate_up(round_est_raw, cal)),
)
})
.and_then(|budget| {
cw_overflow::recovery_target(
budget,
refused_tokens,
budget_state.actionable_input_budget(),
)
})
{
if let Some(context_window) = recovered_window {
generation_window = Some(
generation_window
.map_or(context_window, |old| old.min(context_window)),
);
emit_context_window_400(&mut on_round_usage, context_window);
}
budget_state.recover_from_cw400(recovered_budget);
let target = context_recovery::target(
&input,
budget_state.compaction_budget(cal, tools_supported),
estimation,
);
let compression = compact_responses_input(
Some(&observed_facts),
worktree_session,
&mut input,
instructions.as_deref(),
tools_supported.then_some(tools.as_slice()),
budget_state.actionable_input_budget(),
target,
cal,
estimation,
task,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
summarizer,
compress_state,
color,
smart_harness,
true,
);
let Some(outcome) = cancellable(cancel, compression).await else {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
if matches!(outcome, ResponsesCompaction::Compacted) {
repeat_calls.release_read_memos();
}
match outcome {
ResponsesCompaction::Compacted | ResponsesCompaction::NotFired => {}
other => {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(match other.rejection() {
Some(reason) => e.context(reason.to_string()),
None => e,
});
}
}
let projected = estimate_responses_request_tokens(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
estimation,
);
if projected >= round_est_raw {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
return Err(e);
}
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
Some(calibrate_up(projected, cal)),
)?;
cw_retries += 1;
continue;
}
}
if overflow {
context_recovery::record(
&mut solve_obs,
smart_harness,
round,
cw_retries + 1,
round_est_raw,
None,
)?;
}
return Err(e);
}
}
};
let decoded = crate::responses_wire::decode_response(&json);
complete_responses_attempt(attempt.as_ref(), &json, &decoded);
if generation_bounds::output_limited(&json) {
let usage = crate::responses_wire::decode_usage(&json["usage"]);
accumulated_usage = merge_round_usage(accumulated_usage, usage);
if output_recovery.retry(
json["output"]
.as_array()
.is_some_and(|items| items.iter().any(|item| item["type"] == "function_call")),
&mut input,
smart_harness,
)? {
continue;
}
let text = finalize_final_text(
generation_bounds::output_limit_notice(
generation_bounds::partial_responses_text(&json),
usage,
),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Failed);
}
if let Some(harness) = smart_harness {
harness.outcome(crate::TurnEndReason::Failed, &text)?;
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let refused = matches!(
decoded,
Err(crate::responses_wire::ResponseDecodeError::Refused { .. })
);
let decoded = match decoded {
Ok(d) => d,
Err(crate::responses_wire::ResponseDecodeError::Refused { message, usage })
if smart_harness.is_some() =>
{
crate::responses_wire::DecodedResponse {
text: message,
tool_calls: Vec::new(),
echo: Vec::new(),
model: None,
usage,
}
}
Err(crate::responses_wire::ResponseDecodeError::Refused { message, usage }) => {
accumulated_usage = merge_round_usage(accumulated_usage, usage);
let out = finalize_final_text(
FinalReply::from(message).with_notice("The model refused the request.".into()),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
Err(e) => {
if let Some(harness) = smart_harness {
harness.provider_failure(&e.to_string())?;
}
return Err(anyhow::anyhow!("Responses turn not usable: {e}"));
}
};
compress_state.calibration.observe(
decoded.usage.map(|usage| usage.input_tokens),
estimate_responses_request_tokens(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
estimation,
),
);
cal = compress_state.calibration.ratio(estimate_ratio);
budget_state.set_tool_schema_tokens(calibrate_up(
estimate_value_tokens(&tools_for_estimate, estimation),
cal,
));
accumulated_usage = merge_round_usage(accumulated_usage, decoded.usage);
let (text, calls, echo) = (decoded.text, decoded.tool_calls, decoded.echo);
if debug {
let excerpt: String = text.chars().take(80).collect();
print_debug(
&format!(
"responses round {round}: function_calls={} content={excerpt:?}",
calls.len()
),
color,
);
}
if calls.is_empty() {
if let Some(mut nudge) = capability_evidence.correction(
&text,
&tools_chat,
&mut probe_correction_used,
!refused
&& tools_supported
&& workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
)
&& !is_cancelled(cancel),
) {
if let Some(harness) = smart_harness {
nudge = harness.correct_answer(&text, &nudge)?;
}
input.extend(echo.clone());
input.push(serde_json::json!({"role":"assistant", "content":text}));
input.push(serde_json::json!({"role":"user", "content":nudge}));
continue;
}
if let Some(harness) = smart_harness {
let more = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
let control = harness
.classify(&text, narration_nudge_cap, more, cancel)
.await?;
let control = harness
.verify_answer(
control,
self_verify::Concluding {
messages: &input,
workspace,
task,
rounds_left: more,
round,
ledger: &verification,
solve_obs: solve_obs.as_deref_mut(),
},
smart_verify,
&text,
)
.await?;
let (text, reason) = match control {
smart_harness::Control::Continue(nudge) => {
input.extend(echo.clone());
input.push(serde_json::json!({"role":"assistant", "content":text}));
input.push(serde_json::json!({"role":"user", "content":nudge}));
continue;
}
smart_harness::Control::Answer => {
(FinalReply::from(text), crate::TurnEndReason::Completed)
}
smart_harness::Control::Finish { text, reason } => (text, reason),
};
if let Some(slot) = &mut end_reason {
**slot = Some(reason);
}
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(reason, &text)?;
return Ok((text, false, accumulated_usage, hallucination_count));
}
if !is_cancelled(cancel)
&& readonly_completion_pending(prompt_disposition, &nudge_classifier, &text)
{
let more_rounds = workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
);
if retry_readonly_completion(
&mut input,
&text,
Some(&echo),
&mut readonly_completion_retried,
more_rounds,
) {
continue;
}
let out = readonly_completion_handoff(
&text,
&mut end_reason,
more_rounds,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((out, false, accumulated_usage, hallucination_count));
}
if should_ground_unverified_run_command_blocker(
&text,
&tools_chat,
run_command_denial_observed,
exec_grounding_turn && tools_supported,
unverified_exec_blocker_nudges,
workflow_runtime.has_next_round(
round,
current_tool_round_limit,
workflow_grace_rounds,
),
) {
if debug {
print_debug(
"unsupported run_command blocker — requiring a ground-truth probe",
color,
);
}
input.push(serde_json::json!({ "role": "assistant", "content": text }));
input.push(serde_json::json!({
"role": "user",
"content": format!(
"{} {}",
compress::LOOP_GUIDANCE_PREFIX,
run_command_ground_truth_nudge()
)
}));
unverified_exec_blocker_nudges += 1;
continue;
}
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((text, false, accumulated_usage, hallucination_count));
}
let extracted: Vec<(Option<&str>, Option<&str>, &serde_json::Value)> = calls
.iter()
.map(|c| {
(
c["call_id"].as_str().or_else(|| c["id"].as_str()),
c["name"].as_str(),
&c["arguments"],
)
})
.collect();
let validated = match tools::validate_tool_call_batch(&extracted, true) {
Ok(v) => {
uncorrelatable_batches = 0;
v
}
Err(tools::BatchRejection::CorrelationImpossible(reason)) => {
let reask = tools::reask_uncorrelatable(
&mut uncorrelatable_batches,
&reason,
smart_harness,
tool_events.as_deref_mut(),
)?;
input.push(serde_json::json!({"role": "user", "content": reask}));
continue;
}
Err(tools::BatchRejection::ContentInvalid(reason)) => {
uncorrelatable_batches = 0;
if let Some(harness) = smart_harness {
harness.reject_tools(&reason, true)?;
}
for item in &echo {
input.push(item.clone());
}
for call in &calls {
let call_id = call["call_id"]
.as_str()
.or_else(|| call["id"].as_str())
.unwrap_or("");
print_synthetic_tool_result(
"(rejected tool-call batch)",
&serde_json::Value::Null,
workspace,
&reason,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(
"(rejected tool-call batch)",
&serde_json::Value::Null,
false,
Some(0),
));
}
smart_harness::push_tool_message(
&mut input,
serde_json::json!({
"type": "function_call_output",
"call_id": call_id,
"output": format!("tool-call batch rejected before execution: {reason}"),
}),
smart_harness,
true,
"(rejected tool-call batch)",
)?;
}
continue;
}
};
for item in &echo {
input.push(item.clone());
}
commit_tool_round_narration(&text, false, completed_spill_renderer.as_deref(), color);
let batch = smart_harness
.map(|harness| {
let mut messages = Vec::with_capacity(input.len() + 1);
if let Some(text) = &instructions {
messages.push(serde_json::json!({"role":"system","content":text}));
}
messages.extend_from_slice(&input);
harness.tool_batch(&validated, &messages)
})
.transpose()?;
let mut round_modified_workspace = false;
let mut round_progress = false;
let mut round_succeeded = false;
let mut tool_warnings = Vec::new();
for (call_index, (call, vc)) in calls.iter().zip(validated.iter()).enumerate() {
let call_id = vc.call_id.as_str();
let name = vc.name.as_str();
let args = vc.args.clone();
announce_tool_activity(name);
dismiss_completed_spill(&completed_spill_renderer);
if trace {
print_trace(
&format!(
"raw function_call: {}",
serde_json::to_string(call).unwrap_or_else(|_| "?".into())
),
color,
);
}
if is_hallucination(name, &args) {
hallucination_count += 1;
}
if let Some(content) =
repeat_calls.cached_read(name, &args, ReadScope { workspace, caveats })
{
print_synthetic_tool_result(
name,
&args,
workspace,
&content,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_cache(name, &args));
}
let offloaded =
maybe_offload_tool_result(name, content, tool_offload, spill_store, disclosure);
smart_harness::push_tool_resolution(
&mut input,
serde_json::json!({
"type": "function_call_output",
"call_id": call_id,
"output": offloaded,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
if let Some(steer) = repeat_calls.repeat_steer(name, &args) {
print_synthetic_tool_result(
name,
&args,
workspace,
&steer,
color,
&completed_spill_renderer,
);
if let Some(rec) = tool_events.as_deref_mut() {
rec.push(crate::ToolEvent::from_call(name, &args, false, Some(0)));
}
smart_harness::push_tool_resolution(
&mut input,
serde_json::json!({
"type": "function_call_output",
"call_id": call_id,
"output": steer,
}),
batch.as_ref(),
call_index,
)?;
continue;
}
let invocation = batch
.as_ref()
.map(|batch| batch.start(call_index, disclosure))
.transpose()?;
record_organic_note_use(name, ¬e_sink, &mut note_nudge);
let before_change =
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await;
ledger_note_write(write_ledger, name, &args, workspace);
let tool_t0 = std::time::Instant::now();
let execution = std::sync::OnceLock::new();
let retry_authorized = std::sync::OnceLock::new();
let routed_to = std::sync::OnceLock::new();
let governed_pr = std::sync::OnceLock::new();
let report_capture = std::sync::Mutex::new(observed_report::Capture::default());
let command_directory = std::sync::OnceLock::new();
let attribution_epoch = AttributionEpoch::new(attribution, git_tool, model, name);
let result = if tools::is_context_remaining_call(name) {
let report = responses_context_remaining_report(
instructions.as_deref(),
&input,
tools_supported.then_some(tools.as_slice()),
&budget_state,
cal,
estimation,
low_budget_pct,
);
if let Some(invocation) = &invocation {
invocation.host();
invocation.observe(&report, None, spill_store)?;
}
print_synthetic_tool_result(
name,
&args,
workspace,
&report,
color,
&completed_spill_renderer,
);
report
} else {
let tool_evidence = tool_events
.as_deref()
.map(|e| capability_check::Evidence::from_events(e));
let Some(result) = tools::execute_tool_with_collaborators(
name,
&args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
tools::ToolCollaborators {
command_budget,
shell_dialect,
read_history: Some(&mut read_history),
default_command_cwd,
worktree_session,
invocation: invocation.as_ref(),
build_check_cmd: build_check_cmd.as_deref(),
tool_evidence: tool_evidence.as_ref(),
note_sink: note_sink
.as_deref_mut()
.map(|s| &mut *s as &mut dyn NoteSink),
recall_source,
memory_source,
prompt_context: Some(prompt_context),
artifact_context,
artifact_sink,
permission_gate: permission_gate
.as_deref_mut()
.map(|g| &mut *g as &mut dyn PermissionGate),
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav,
experience_store,
step_ledger,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
spill_store,
persona_tools,
hidden_tools: Some(&hidden_tools),
live_tool_output: live_tool_output.clone(),
completed_spill_renderer: completed_spill_renderer.clone(),
execution: Some(&execution),
retry_authorized: Some(&retry_authorized),
governed_pr: Some(&governed_pr),
report_capture: Some(&report_capture),
command_directory: Some(&command_directory),
routed_to: Some(&routed_to),
pending_rerun: Some(&mut pending_rerun),
},
tool_offload,
prompt_disposition,
cancel,
)
.await?
else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
result
};
if debug {
dismiss_completed_spill(&completed_spill_renderer);
let excerpt: String = result.chars().take(120).collect();
print_debug(&format!("tool result: {excerpt:?}"), color);
}
let ok = tools::tool_ok(&result, execution.get().copied());
round_succeeded |= ok;
let after_change = if before_change.is_some() {
progress_workspace_state(name, &args, workspace, &caveats.fs_read).await
} else {
None
};
round_modified_workspace |=
workflow_runtime.record_workspace_change(before_change, after_change);
round_progress |= workflow_runtime.record_observation(
name,
&args,
&result,
ok,
execution.get().copied(),
routed_to.get(),
);
attribution_epoch.finish(&args, ok);
run_command_denial_observed |= run_command_result_is_denial(name, ok, &result);
capability_evidence.record(name, ok, execution.get().copied());
repeat_calls.record(
name,
&args,
ok,
&result,
execution.get().copied(),
ReadScope { workspace, caveats },
);
repeat_calls.permit_authorized_retry(name, &args, &retry_authorized);
append_clean_build_warning(
if batch.is_some() {
&mut tool_warnings
} else {
&mut input
},
&mut clean_build,
&args,
None,
)?;
record_completed_tool_event(
&mut tool_events,
name,
&args,
ok,
execution.get().copied(),
tool_t0,
);
turn_claims.observe_report(
workspace,
&caveats.fs_read,
&report_capture.lock().expect("report capture"),
execution.get().copied(),
governed_pr.get(),
);
if let Some(outcome) = governed_pr.get() {
verification.record_publication_outcome(outcome);
}
if ok {
if let Some(directory) = command_directory.get() {
verification.record_command_directory(directory);
}
}
verification
.observe_routed(
name,
&args,
ok,
execution.get().copied(),
workspace,
routed_to.get(),
)
.await;
record_phantom_reach(
&mut phantom_reaches,
name,
&args,
&result,
ok,
advertise_team,
);
observed_paths.record(&result, &mut observed_resolver);
smart_harness::push_tool_return(
&mut input,
serde_json::json!({
"type": "function_call_output",
"call_id": call_id,
"output": smart_harness::tool_result(name, result, tool_offload, spill_store, disclosure, invocation.as_ref())?,
}),
invocation.as_ref(),
)?;
}
for message in tool_warnings {
smart_harness::push_tool_message(
&mut input,
message,
smart_harness,
true,
"build guidance",
)?;
}
if let Some(harness) = smart_harness {
harness.record_responses_messages(instructions.as_deref(), &input)?;
}
if round_succeeded {
output_recovery.completed_tool_round();
}
workflow_runtime.record_round_outcome(round_modified_workspace, round_progress);
if let Some(text) = pending_plan_approval_handoff(
plan_mode_control,
PlanRoundFacts {
disposition: prompt_disposition,
step_ledger,
plan_at_turn_start: &plan_at_turn_start,
progressed: round_modified_workspace || round_progress,
},
smart_harness,
cancel,
&mut end_reason,
)? {
dismiss_completed_spill(&completed_spill_renderer);
let text = finalize_final_text(
FinalReply::harness(text),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
return Ok((text, false, accumulated_usage, hallucination_count));
}
}
let cap_accumulated = accumulated_usage;
let progress = cap_exit_progress(worktree_session, step_ledger, scratchpad_store);
let cap_reason = verification
.cap_exit_reason(
workspace,
smart_harness.is_some() && smart_verify,
current_tool_round_limit,
solve_obs.as_deref_mut(),
)
.await;
if let Some(hit) = &mut round_cap_hit {
**hit = true;
}
if let Some(harness) = smart_harness {
harness.record_responses_messages(instructions.as_deref(), &input)?;
let text = cap_exit_fallback(
current_tool_round_limit,
accumulated_usage,
repeat_calls.total_failures(),
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
harness.outcome(cap_reason, &text)?;
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let observed = observed_paths.into_vec();
input.push(serde_json::json!({
"role": "user",
"content": cap_exit_nudge(current_tool_round_limit, progress.as_deref(), &observed),
}));
let mut summary_rejection: Option<CompactionRejection> = None;
if let Some(budget) = budget_state.actionable_input_budget() {
let before = estimate_responses_request_real_tokens(
instructions.as_deref(),
&input,
None,
estimation,
cal,
);
if before > budget {
let compression = compact_responses_input(
Some(&observed_facts),
worktree_session,
&mut input,
instructions.as_deref(),
None,
Some(budget),
budget_state.compaction_budget(cal, false),
cal,
estimation,
task,
summary_input_cap_floor_chars,
rewrites_history,
compaction_store,
summarizer,
compress_state,
color,
smart_harness,
false,
);
let Some(outcome) = cancellable(cancel, compression).await else {
return Ok((
smart_harness::cancelled(smart_harness, &mut end_reason)?,
false,
accumulated_usage,
hallucination_count,
));
};
if matches!(outcome, ResponsesCompaction::Compacted) {
repeat_calls.release_read_memos();
}
summary_rejection = outcome.rejection();
}
}
dismiss_completed_spill(&completed_spill_renderer);
let mut body = build_body(&input, &[]);
let output_cap = generation_bounds::apply_responses(
&mut body,
generation_policy.output_allowance,
generation_window,
estimate_responses_request_real_tokens(
instructions.as_deref(),
&input,
None,
estimation,
cal,
),
)?;
observability::observe_output_allowance(&mut solve_obs, Some(output_cap), true);
let policy = responses_wire_validation::ResponsesWirePolicy {
store: crate::responses_wire::STORE_RESPONSE_SERVER_SIDE,
tools_permitted: false,
model,
authoritative_budget: budget_state.actionable_input_budget(),
calibration: cal,
estimation,
spill: spill_store,
compaction: compaction_store,
};
let validated = match responses_wire_validation::validate_responses_request(&body, &policy) {
Ok(validated) => validated,
Err(e) => {
let error = match summary_rejection.take() {
Some(reason) => anyhow::Error::new(e).context(reason.to_string()),
None => anyhow::Error::new(e),
};
tracing::warn!(
error = %error,
"Responses cap-exit summary validation failed; returning captured progress"
);
let text = finalize_final_text(
cap_exit_fallback(
current_tool_round_limit,
cap_accumulated,
repeat_calls.total_failures(),
progress.as_deref(),
),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
};
let summary_estimate =
estimate_responses_request_tokens(instructions.as_deref(), &input, None, estimation);
let source = responses_source_messages(instructions.as_deref(), &input);
let (json, attempt) = match dispatch_responses_json(
&client,
&responses_url,
api_key,
&validated,
&retry,
color,
smart_harness.map(|h| (h, source.as_slice())),
attempts,
)
.await
{
Ok(dispatched) => dispatched,
Err(error) => {
if crate::retry::classify(&error) == crate::retry::Retryability::ContextExceeded {
context_recovery::terminal_optional(
&mut solve_obs,
compress_state,
cal,
current_tool_round_limit,
cw_retries + 1,
summary_estimate,
)?;
}
tracing::warn!(
error = %error,
"Responses cap-exit summary dispatch failed; returning captured progress"
);
let mut reply = cap_exit_fallback(
current_tool_round_limit,
cap_accumulated,
repeat_calls.total_failures(),
progress.as_deref(),
);
generation_bounds::append_notice(&mut reply, &error);
let text = finalize_final_text(
reply,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
};
let decoded = crate::responses_wire::decode_response(&json);
complete_responses_attempt(attempt.as_ref(), &json, &decoded);
if generation_bounds::output_limited(&json) {
let usage = crate::responses_wire::decode_usage(&json["usage"]);
accumulated_usage = merge_round_usage(accumulated_usage, usage);
let text = finalize_final_text(
generation_bounds::output_limit_notice(
generation_bounds::partial_responses_text(&json),
usage,
),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(crate::TurnEndReason::Failed);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
let decoded = match decoded {
Ok(d) => d,
Err(crate::responses_wire::ResponseDecodeError::Refused { message, usage }) => {
accumulated_usage = merge_round_usage(accumulated_usage, usage);
let text = cap_exit_progress_handoff(
current_tool_round_limit,
cap_accumulated,
&message,
false,
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
Err(error) => {
tracing::warn!(
error = %error,
"Responses cap-exit summary decode failed; returning captured progress"
);
let text = finalize_final_text(
cap_exit_fallback(
current_tool_round_limit,
cap_accumulated,
repeat_calls.total_failures(),
progress.as_deref(),
),
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
return Ok((text, false, accumulated_usage, hallucination_count));
}
};
accumulated_usage = merge_round_usage(accumulated_usage, decoded.usage);
let text = cap_exit_model_reply(
current_tool_round_limit,
cap_accumulated,
&decoded.text,
progress.as_deref(),
);
let text = finalize_final_text(
text,
workspace,
&caveats.fs_read,
&capability_evidence,
disclosure,
&turn_claims,
&verification,
);
if let Some(slot) = &mut end_reason {
**slot = Some(cap_reason);
}
Ok((text, false, accumulated_usage, hallucination_count))
}
#[must_use]
pub fn thinking_mode(report: &mut dyn FnMut(crate::tty::Notice<'static>)) -> crate::ThinkingMode {
match std::env::var("NEWT_THINKING").ok().as_deref() {
Some("off") => return crate::ThinkingMode::Off,
Some("stream") => return crate::ThinkingMode::Stream,
Some("on" | "fold") => return crate::ThinkingMode::Fold,
_ => {}
}
crate::Config::resolve_unpublished(report)
.ok()
.and_then(|c| c.tui)
.map(|t| t.thinking)
.unwrap_or_default()
}
#[must_use]
pub fn thinking_stream_enabled() -> bool {
display::with_migration_notices(thinking_mode) != crate::ThinkingMode::Off
}
fn commit_reasoning_fold(
reasoning: Option<String>,
elapsed: std::time::Duration,
retain: Option<&dyn CompletedSpillRenderer>,
color: bool,
) {
if display::with_migration_notices(thinking_mode) == crate::ThinkingMode::Off {
return;
}
let Some(reasoning) = reasoning.filter(|r| !r.trim().is_empty()) else {
return;
};
let budget = if display::with_migration_notices(thinking_mode) == crate::ThinkingMode::Fold {
display::spill_lines()
} else {
0
};
let mut fold = display::ThinkingFold::default();
let mut shown: Vec<String> = Vec::new();
for line in reasoning.lines().filter(|l| !l.trim().is_empty()) {
if fold.offer(line, budget) {
shown.push(format!(" {line}"));
}
}
let retained = retain.and_then(|r| r.retain_completed(fold.body()));
let hint = retained
.zip(retain)
.map(|(id, renderer)| renderer.recovery_hint(id));
let recovery = hint
.as_deref()
.map_or_else(display::Recovery::default, display::Recovery::Command);
if let Some(closing) = fold.closing_line(elapsed, recovery) {
shown.push(closing);
}
if shown.is_empty() {
return;
}
let block = shown.join("\n");
emit_notice_line(crate::tty::Level::Thinking, "", &block, color);
}
fn emit_notice_line(level: crate::tty::Level, glyph: &str, text: &str, color: bool) {
let notice = crate::tty::Notice::new(level, glyph, text).gap(2);
crate::tty::Terminal::emit_line(crate::tty::Sink::Stdout, notice.writer(color));
}
fn commit_tool_round_narration(
content: &str,
already_printed: bool,
retain: Option<&dyn CompletedSpillRenderer>,
color: bool,
) {
emit_tool_round_narration(
content,
already_printed,
retain,
display::reply_cols(crate::tty::term_cols()),
display::spill_lines(),
|level, glyph, text| emit_notice_line(level, glyph, text, color),
);
}
fn emit_tool_round_narration(
content: &str,
already_printed: bool,
retain: Option<&dyn CompletedSpillRenderer>,
width: usize,
budget: usize,
mut emit: impl FnMut(crate::tty::Level, &str, &str),
) {
if already_printed {
return;
}
let rows = display::tool_round_prose(content);
if rows.is_empty() {
return;
}
let rendered: Vec<_> = rows
.iter()
.flat_map(|row| crate::tty::wrap_line(row, width))
.collect();
let shown = if budget == 0 {
rendered.len()
} else {
rendered.len().min(budget)
};
for (i, line) in rendered.iter().take(shown).enumerate() {
let glyph = if i == 0 { "▹" } else { " " };
emit(crate::tty::Level::Narration, glyph, line);
}
let hidden = rendered.len() - shown;
if hidden == 0 {
return;
}
let body = rows.join("\n");
let retained = retain.and_then(|r| r.retain_completed(&body));
let hint = retained
.zip(retain)
.map(|(id, renderer)| renderer.recovery_hint(id));
let recovery = hint
.as_deref()
.map_or_else(display::Recovery::default, display::Recovery::Command);
emit(
crate::tty::Level::Dim,
"▲",
&display::Fold::lines(hidden, recovery).marker(),
);
}
#[derive(Default)]
struct ReasoningTrickle {
fold: display::ThinkingFold,
partial: String,
}
impl ReasoningTrickle {
fn feed(&mut self, spinner: &crate::tty::Spinner, chunk: &str, budget: usize) {
self.partial.push_str(chunk);
while let Some(nl) = self.partial.find('\n') {
let line: String = self.partial.drain(..=nl).collect();
let trimmed = line.trim_end_matches(['\n', '\r']).to_string();
if trimmed.trim().is_empty() {
continue;
}
if self.fold.offer(&trimmed, budget) {
spinner.detail(&format!("{trimmed}\n"));
}
}
}
fn close(
&mut self,
elapsed: std::time::Duration,
retain: Option<&dyn CompletedSpillRenderer>,
color: bool,
) {
if !self.partial.trim().is_empty() {
let tail = std::mem::take(&mut self.partial);
self.fold.offer(tail.trim_end(), 0);
}
if self.fold.is_empty() {
return;
}
let retained = retain.and_then(|r| r.retain_completed(self.fold.body()));
let hint = retained
.zip(retain)
.map(|(id, renderer)| renderer.recovery_hint(id));
let recovery = hint
.as_deref()
.map_or_else(display::Recovery::default, display::Recovery::Command);
let Some(line) = self.fold.closing_line(elapsed, recovery) else {
return;
};
emit_notice_line(crate::tty::Level::Thinking, "", &line, color);
}
}
fn decode_chunk(carry: &mut Vec<u8>, chunk: &[u8]) -> String {
carry.extend_from_slice(chunk);
let mut out = String::new();
loop {
let err = match std::str::from_utf8(carry) {
Ok(s) => {
out.push_str(s);
carry.clear();
return out;
}
Err(e) => e,
};
let good = err.valid_up_to();
out.push_str(&String::from_utf8_lossy(&carry[..good]));
match err.error_len() {
None => {
carry.drain(..good);
return out;
}
Some(n) => {
carry.drain(..good + n);
out.push(char::REPLACEMENT_CHARACTER);
}
}
}
}
#[cfg(test)]
#[path = "mod_tests/repeat_call_guard_tests.rs"]
mod repeat_call_guard_tests;
#[cfg(test)]
#[path = "mod_tests/cap_exit_unit_tests.rs"]
mod cap_exit_unit_tests;
#[cfg(test)]
pub(crate) fn builtin_catalog_tokens(disposition: PromptDisposition) -> usize {
let tools = filter_tools_for_disposition(
merged_tool_definitions(
&NoMcp,
false,
false,
false,
None,
false,
false,
false,
false,
false,
false,
false,
false,
Default::default(),
crate::agentic::shell_dialect_sentence(false, false),
),
disposition,
);
estimate_value_tokens(&tools, crate::tokens::TokenEstimation::default())
}
#[cfg(test)]
#[path = "mod_tests/tool_round_cap_tests.rs"]
mod tool_round_cap_tests;
#[cfg(test)]
#[path = "mod_tests/artifact_compaction_provenance.rs"]
mod artifact_compaction_provenance_tests;
#[cfg(test)]
#[path = "mod_tests/artifact_provenance.rs"]
mod artifact_provenance_tests;
#[cfg(test)]
#[path = "mod_tests/http_loop.rs"]
mod http_loop_tests;
#[cfg(test)]
#[path = "mod_tests/anthropic_loop.rs"]
mod anthropic_loop_tests;
#[cfg(test)]
pub(crate) use anthropic_loop_tests::EnvGuard as TestEnvGuard;
#[cfg(test)]
#[path = "mod_tests/openai_stream_loop.rs"]
mod openai_stream_loop_tests;
#[cfg(test)]
#[path = "mod_tests/bat_largest_files.rs"]
mod bat_largest_files_tests;
#[cfg(test)]
#[path = "mod_tests/bat_disposition_voice.rs"]
mod bat_disposition_voice_tests;
#[cfg(test)]
#[path = "mod_tests/save_note_loop_tests.rs"]
mod save_note_loop_tests;
#[cfg(test)]
#[path = "mod_tests/compression_loop_tests.rs"]
mod compression_loop_tests;
#[cfg(test)]
#[path = "mod_tests/observation_hook_tests.rs"]
mod observation_hook_tests;
#[cfg(test)]
#[path = "mod_tests/attribution_epoch_tests.rs"]
mod attribution_epoch_tests;
#[cfg(test)]
#[path = "mod_tests/narration_output.rs"]
mod narration_output_tests;