Skip to main content

a3s_code_core/
agent.rs

1//! Agent Loop Implementation
2//!
3//! The agent loop handles the core conversation cycle:
4//! 1. User sends a prompt
5//! 2. LLM generates a response (possibly with tool calls)
6//! 3. If tool calls present, execute them and send results back
7//! 4. Repeat until LLM returns without tool calls
8//!
9//! This implements agentic behavior where the LLM can use tools
10//! to accomplish tasks agentically.
11
12use crate::context::ContextProvider;
13use crate::hitl::ConfirmationProvider;
14use crate::hooks::HookExecutor;
15#[cfg(test)]
16use crate::llm::LlmResponse;
17use crate::llm::{LlmClient, Message, TokenUsage, ToolDefinition};
18use crate::permissions::{PermissionChecker, PermissionPolicy};
19use crate::planning::{AgentGoal, ExecutionPlan, TaskStatus};
20use crate::prompts::{PlanningMode, SystemPromptSlots};
21use crate::queue::{SessionCommand, SessionQueueConfig};
22use crate::session_lane_queue::SessionLaneQueue;
23use crate::subagent::AgentRegistry;
24use crate::tools::{ToolContext, ToolExecutor};
25use anyhow::Result;
26use async_trait::async_trait;
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29use std::sync::Arc;
30
31mod auto_delegation;
32mod completion_runtime;
33mod context_perception;
34mod execution_entry;
35mod execution_mode;
36mod execution_state;
37pub(crate) use execution_state::ExecutionSeed;
38mod hook_runtime;
39mod invocation_context;
40pub(crate) use invocation_context::InvocationContext;
41mod llm_invoker;
42mod model_middleware_obs;
43pub use model_middleware_obs::ModelMiddlewareHealthSnapshot;
44pub(crate) use model_middleware_obs::ModelMiddlewareObs;
45mod llm_turn;
46mod loop_builder;
47mod loop_runtime;
48mod memory_extraction_runtime;
49mod parallel_tool_runtime;
50mod plan_execution;
51mod planning_runtime;
52mod project_context;
53mod prompt_runtime;
54mod queue_forwarder;
55mod telemetry_runtime;
56mod tool_completion_runtime;
57mod tool_execution_runtime;
58mod tool_guard_runtime;
59mod tool_invoker;
60mod tool_result_runtime;
61mod tool_turn;
62mod turn_context;
63
64/// Maximum number of tool execution rounds before stopping
65pub(crate) const MAX_TOOL_ROUNDS: usize = 50;
66pub(crate) const DEFAULT_MAX_PARALLEL_TASKS: usize = 8;
67
68/// Internal agent loop configuration.
69#[derive(Clone)]
70pub(crate) struct AgentConfig {
71    /// Slot-based system prompt customization.
72    ///
73    /// Users can customize specific parts (role, guidelines, response style, extra)
74    /// without overriding the core agentic capabilities. The default agentic core
75    /// (tool usage, autonomous behavior, completion criteria) is always preserved.
76    pub prompt_slots: SystemPromptSlots,
77    pub tools: Vec<ToolDefinition>,
78    /// Immutable model-facing Tool presentation policy for this loop.
79    /// Execution continues through the separately owned `ToolExecutor`.
80    pub tool_presentation_profile: crate::tools::ToolPresentationProfileV1,
81    pub max_tool_rounds: usize,
82    /// Optional security provider for input taint tracking and output sanitization
83    pub security_provider: Option<Arc<dyn crate::security::SecurityProvider>>,
84    /// Optional permission checker for tool execution control
85    pub permission_checker: Option<Arc<dyn PermissionChecker>>,
86    /// Serializable permission policy used to build the checker, when available.
87    pub permission_policy: Option<PermissionPolicy>,
88    /// Optional confirmation manager for HITL (Human-in-the-Loop)
89    pub confirmation_manager: Option<Arc<dyn ConfirmationProvider>>,
90    /// Child-local resolution policy for `Ask` decisions.
91    ///
92    /// This marker remains separate from `confirmation_manager` because both
93    /// `deny_on_ask` and `inherit_parent` intentionally start without a
94    /// child-local provider and must be distinguished when the parent boundary
95    /// is composed into a delegated run.
96    pub confirmation_inheritance: Option<crate::subagent::ConfirmationInheritance>,
97    /// Serializable confirmation policy used to build the manager, when available.
98    pub confirmation_policy: Option<crate::hitl::ConfirmationPolicy>,
99    /// Serializable queue configuration used to build the optional command queue.
100    pub queue_config: Option<SessionQueueConfig>,
101    /// Context providers for augmenting prompts with external context
102    pub context_providers: Vec<Arc<dyn ContextProvider>>,
103    /// Planning mode — Auto (detect from message), Enabled, or Disabled.
104    pub planning_mode: PlanningMode,
105    /// Enable goal tracking
106    pub goal_tracking: bool,
107    /// Optional hook engine for firing lifecycle events (PreToolUse, PostToolUse, etc.)
108    pub hook_engine: Option<Arc<dyn HookExecutor>>,
109    /// Optional structured JSONL trajectory recorder for RL training and service data capture.
110    pub rl_trajectory_recorder: crate::rl_trajectory::RlTrajectoryRecorder,
111    /// Optional skill registry for tool permission enforcement
112    pub skill_registry: Option<Arc<crate::skills::SkillRegistry>>,
113    /// When true, active skill `allowed-tools` restrict ordinary session tool calls.
114    ///
115    /// The default is false: active skills may inject instructions, but ordinary
116    /// tool calls continue to the host permission/HITL approval chain.
117    /// Skill invocations still enable this for their child execution context.
118    pub enforce_active_skill_tool_restrictions: bool,
119    /// Max consecutive malformed-tool-args errors before aborting (default: 2).
120    ///
121    /// When the LLM returns tool arguments with `__parse_error`, the error is
122    /// fed back as a tool result. After this many consecutive parse errors the
123    /// loop bails instead of retrying indefinitely.
124    pub max_parse_retries: u32,
125    /// Per-tool execution timeout in milliseconds (`None` = no timeout).
126    ///
127    /// This applies only after permission/HITL approval has completed. HITL
128    /// confirmation waiting is governed by `ConfirmationPolicy` and must not
129    /// consume this tool execution budget. A timeout produces an error result
130    /// sent back to the LLM rather than crashing the session.
131    pub tool_timeout_ms: Option<u64>,
132    /// Per-model API HTTP timeout in milliseconds (`None` = no timeout).
133    ///
134    /// This is intentionally separate from `tool_timeout_ms`: slow shell/web
135    /// tools and slow model providers have different operational envelopes.
136    pub llm_api_timeout_ms: Option<u64>,
137    /// Maximum number of sibling branches/tools to run concurrently in bounded
138    /// parallel fan-out paths.
139    pub max_parallel_tasks: usize,
140    /// Runtime-driven automatic child-agent delegation.
141    pub auto_delegation: crate::config::AutoDelegationConfig,
142    /// Available child agents for automatic delegation.
143    pub agent_registry: Option<Arc<AgentRegistry>>,
144    /// Circuit-breaker threshold: max consecutive LLM API failures before
145    /// aborting (default: 3).
146    ///
147    /// In non-streaming mode, transient LLM failures are retried up to this
148    /// many times (with short exponential backoff) before the loop bails.
149    /// In streaming mode, any failure is fatal (events cannot be replayed).
150    pub circuit_breaker_threshold: u32,
151    /// Max consecutive identical tool signatures before aborting (default: 3).
152    ///
153    /// A tool signature is the exact combination of tool name + compact JSON
154    /// arguments. This prevents the agent from getting stuck repeating the same
155    /// tool call in a loop, for example repeatedly fetching the same URL.
156    pub duplicate_tool_call_threshold: u32,
157    /// Enable auto-compaction when context usage exceeds threshold.
158    pub auto_compact: bool,
159    /// Context usage percentage threshold to trigger auto-compaction (0.0 - 1.0).
160    /// Default: 0.80 (80%).
161    pub auto_compact_threshold: f32,
162    /// Maximum context window size in tokens (used for auto-compact calculation).
163    /// Default: 200_000.
164    pub max_context_tokens: usize,
165    /// Agent memory for recall and completed-turn extraction.
166    ///
167    /// Session construction resolves a default memory store; this remains
168    /// optional for lower-level/manual `AgentLoop` construction.
169    pub memory: Option<Arc<crate::memory::AgentMemory>>,
170    /// Inject a continuation message when the LLM stops calling tools before the
171    /// task is complete. Enabled by default. Set to `false` to disable.
172    ///
173    /// When enabled, if the LLM produces a response with no tool calls but the
174    /// response text looks like an intermediate step (not a final answer), the
175    /// loop injects [`crate::prompts::CONTINUATION`] as a user message and
176    /// continues for up to `max_continuation_turns` additional turns.
177    pub continuation_enabled: bool,
178    /// Maximum number of continuation injections per execution (default: 3).
179    ///
180    /// Prevents infinite loops when the LLM repeatedly stops without completing.
181    pub max_continuation_turns: u32,
182    /// Maximum execution time in milliseconds (`None` = no timeout).
183    ///
184    /// When set, the execution loop, active LLM attempts, and retry backoff
185    /// share this deadline. If execution exceeds it, the loop bails with an
186    /// error instead of allowing a provider call or retry sleep to overrun the
187    /// run's quota.
188    pub max_execution_time_ms: Option<u64>,
189    /// Host-supplied budget guard consulted before every LLM call (and
190    /// after, for usage accounting). `None` means no enforcement.
191    pub budget_guard: Option<Arc<dyn crate::budget::BudgetGuard>>,
192    /// Host-provided ID generator + clock. Defaults to wall-clock UUIDs.
193    /// Replace via [`SessionOptions::with_host_env`](crate::agent_api::SessionOptions::with_host_env)
194    /// when deterministic replay is needed.
195    pub host_env: Arc<crate::host_env::HostEnv>,
196    /// Host-confirmed waivers. Assistant text cannot populate this list.
197    pub completion_waivers: Vec<crate::harness_loop::CompletionWaiverV1>,
198    /// Distinguishes an ordinary run from a plan-mode implementation admission.
199    pub plan_run: crate::harness_loop::PlanRunAdmission,
200    /// Path-scoped rules injected only when a turn targets a matching path.
201    pub path_rules: Vec<crate::path_instructions::PathRule>,
202    /// Opt-in read-only verifier. Default sessions do not spend a second model call.
203    pub verifier_enabled: bool,
204    /// Host-only completion attestor. Invoked with the live mutation digest
205    /// before the gate decides. Not a tool and not model-grantable (#160).
206    pub completion_attestor: Option<Arc<dyn crate::completion_attestor::CompletionAttestor>>,
207    /// Optional Meta Harness compose recipe for fact-log control.
208    pub harness: Option<crate::meta_harness::HarnessComposeOptions>,
209    /// Host Moore component registry for `host:<id>` mounts in compose.
210    pub host_harness_registry: Option<Arc<dyn crate::meta_harness::HostHarnessRegistry>>,
211    /// Full custom graph assembler (Rust embedders). Takes precedence over
212    /// [`Self::harness`] when set.
213    pub host_harness_assembler: Option<Arc<dyn crate::meta_harness::HostHarnessAssembler>>,
214    /// Host-supplied external observations bound into this run.
215    pub external_observations: Vec<crate::external_observation::ExternalObservationV1>,
216    /// Outcome-conditioned constraints the promoting host has already recorded.
217    pub outcome_ledger: crate::outcome_memory::OutcomeLedger,
218}
219
220impl std::fmt::Debug for AgentConfig {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.debug_struct("AgentConfig")
223            .field("prompt_slots", &self.prompt_slots)
224            .field("tools", &self.tools)
225            .field("tool_presentation_profile", &self.tool_presentation_profile)
226            .field("max_tool_rounds", &self.max_tool_rounds)
227            .field("security_provider", &self.security_provider.is_some())
228            .field("permission_checker", &self.permission_checker.is_some())
229            .field("permission_policy", &self.permission_policy.is_some())
230            .field("confirmation_manager", &self.confirmation_manager.is_some())
231            .field("confirmation_inheritance", &self.confirmation_inheritance)
232            .field("confirmation_policy", &self.confirmation_policy.is_some())
233            .field("queue_config", &self.queue_config.is_some())
234            .field("context_providers", &self.context_providers.len())
235            .field("planning_mode", &self.planning_mode)
236            .field("goal_tracking", &self.goal_tracking)
237            .field("hook_engine", &self.hook_engine.is_some())
238            .field("rl_trajectory", &self.rl_trajectory_recorder.is_enabled())
239            .field(
240                "skill_registry",
241                &self.skill_registry.as_ref().map(|r| r.len()),
242            )
243            .field(
244                "enforce_active_skill_tool_restrictions",
245                &self.enforce_active_skill_tool_restrictions,
246            )
247            .field("max_parse_retries", &self.max_parse_retries)
248            .field("tool_timeout_ms", &self.tool_timeout_ms)
249            .field("llm_api_timeout_ms", &self.llm_api_timeout_ms)
250            .field("max_parallel_tasks", &self.max_parallel_tasks)
251            .field("auto_delegation", &self.auto_delegation)
252            .field(
253                "agent_registry",
254                &self.agent_registry.as_ref().map(|registry| registry.len()),
255            )
256            .field("circuit_breaker_threshold", &self.circuit_breaker_threshold)
257            .field(
258                "duplicate_tool_call_threshold",
259                &self.duplicate_tool_call_threshold,
260            )
261            .field("auto_compact", &self.auto_compact)
262            .field("auto_compact_threshold", &self.auto_compact_threshold)
263            .field("max_context_tokens", &self.max_context_tokens)
264            .field("continuation_enabled", &self.continuation_enabled)
265            .field("max_continuation_turns", &self.max_continuation_turns)
266            .field("memory", &self.memory.is_some())
267            .finish()
268    }
269}
270
271impl Default for AgentConfig {
272    fn default() -> Self {
273        Self {
274            prompt_slots: SystemPromptSlots::default(),
275            tools: Vec::new(), // Tools are provided by ToolExecutor
276            tool_presentation_profile: crate::tools::ToolPresentationProfileV1::default(),
277            max_tool_rounds: MAX_TOOL_ROUNDS,
278            security_provider: None,
279            permission_checker: None,
280            permission_policy: None,
281            confirmation_manager: None,
282            confirmation_inheritance: None,
283            confirmation_policy: None,
284            queue_config: None,
285            context_providers: Vec::new(),
286            planning_mode: PlanningMode::default(),
287            goal_tracking: false,
288            hook_engine: None,
289            rl_trajectory_recorder: crate::rl_trajectory::RlTrajectoryRecorder::disabled(),
290            skill_registry: Some(Arc::new(crate::skills::SkillRegistry::with_builtins())),
291            enforce_active_skill_tool_restrictions: false,
292            max_parse_retries: 2,
293            tool_timeout_ms: None,
294            llm_api_timeout_ms: None,
295            max_parallel_tasks: DEFAULT_MAX_PARALLEL_TASKS,
296            auto_delegation: crate::config::AutoDelegationConfig::default(),
297            agent_registry: None,
298            circuit_breaker_threshold: 3,
299            duplicate_tool_call_threshold: 3,
300            auto_compact: false,
301            auto_compact_threshold: 0.80,
302            max_context_tokens: 200_000,
303            memory: None,
304            continuation_enabled: true,
305            max_continuation_turns: 3,
306            max_execution_time_ms: None,
307            budget_guard: None,
308            host_env: Arc::new(crate::host_env::HostEnv::system()),
309            completion_waivers: Vec::new(),
310            plan_run: crate::harness_loop::PlanRunAdmission::ordinary(),
311            path_rules: Vec::new(),
312            verifier_enabled: false,
313            completion_attestor: None,
314            harness: None,
315            host_harness_registry: None,
316            host_harness_assembler: None,
317            external_observations: Vec::new(),
318            outcome_ledger: crate::outcome_memory::OutcomeLedger::default(),
319        }
320    }
321}
322
323/// Events emitted during agent execution
324///
325/// Subscribe via [`crate::AgentSession::stream`].
326/// New variants may be added in minor releases — always include a wildcard arm
327/// (`_ => {}`) when matching.
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(tag = "type")]
330#[non_exhaustive]
331pub enum AgentEvent {
332    /// Agent started processing
333    #[serde(rename = "agent_start")]
334    Start { prompt: String },
335
336    /// Runtime agent style/mode selected for the current execution.
337    #[serde(rename = "agent_mode_changed")]
338    AgentModeChanged {
339        /// Stable UI/runtime mode label, e.g. "general", "planning", "explore".
340        mode: String,
341        /// Canonical built-in agent name associated with this mode.
342        agent: String,
343        /// Human-readable explanation of the selected style.
344        description: String,
345    },
346
347    /// LLM turn started. The same turn number is emitted again when an
348    /// interrupted response stream is retried; consumers should roll back
349    /// provisional output from that turn before applying replacement deltas.
350    #[serde(rename = "turn_start")]
351    TurnStart { turn: usize },
352
353    /// Text delta from streaming
354    #[serde(rename = "text_delta")]
355    TextDelta { text: String },
356
357    /// Reasoning/thinking delta from streaming (for models like kimi, deepseek)
358    #[serde(rename = "reasoning_delta")]
359    ReasoningDelta { text: String },
360
361    /// The model started preparing a streamed tool call.
362    #[serde(rename = "tool_start")]
363    ToolStart { id: String, name: String },
364
365    /// Tool input delta from streaming (partial JSON arguments)
366    #[serde(rename = "tool_input_delta")]
367    ToolInputDelta {
368        #[serde(default, skip_serializing_if = "Option::is_none")]
369        id: Option<String>,
370        delta: String,
371    },
372
373    /// Digest-only evidence for a validated Tool request after pre-tool hooks
374    /// have applied their final argument projection and before governance can
375    /// deny, confirm, or execute it.
376    #[serde(rename = "tool_request_bound")]
377    ToolRequestBound {
378        tool_id: String,
379        tool_name: String,
380        snapshot: crate::harness_evidence::ToolRequestSnapshotV1,
381    },
382
383    /// A fully prepared tool call passed safety/confirmation and began execution.
384    #[serde(rename = "tool_execution_start")]
385    ToolExecutionStart {
386        id: String,
387        name: String,
388        args: serde_json::Value,
389    },
390
391    /// Tool execution completed
392    #[serde(rename = "tool_end")]
393    ToolEnd {
394        id: String,
395        name: String,
396        #[serde(default, skip_serializing_if = "Option::is_none")]
397        args: Option<serde_json::Value>,
398        output: String,
399        exit_code: i32,
400        #[serde(skip_serializing_if = "Option::is_none")]
401        metadata: Option<serde_json::Value>,
402        /// Structured discriminant set by tools that mapped their failure
403        /// into a typed [`ToolErrorKind`](crate::tools::ToolErrorKind)
404        /// (e.g. `edit` / `patch` on a `WorkspaceError::VersionConflict`).
405        /// `None` on success or untyped failure.
406        #[serde(skip_serializing_if = "Option::is_none")]
407        error_kind: Option<crate::tools::ToolErrorKind>,
408    },
409
410    /// Intermediate tool output (streaming delta)
411    #[serde(rename = "tool_output_delta")]
412    ToolOutputDelta {
413        id: String,
414        name: String,
415        delta: String,
416    },
417
418    /// LLM turn completed
419    #[serde(rename = "turn_end")]
420    TurnEnd { turn: usize, usage: TokenUsage },
421
422    /// Agent completed
423    #[serde(rename = "agent_end")]
424    End {
425        text: String,
426        usage: TokenUsage,
427        verification_summary: Box<crate::verification::VerificationSummary>,
428        #[serde(skip_serializing_if = "Option::is_none")]
429        meta: Option<crate::llm::LlmResponseMeta>,
430    },
431
432    /// Error occurred
433    #[serde(rename = "error")]
434    Error { message: String },
435
436    /// Tool execution requires confirmation (HITL)
437    #[serde(rename = "confirmation_required")]
438    ConfirmationRequired {
439        tool_id: String,
440        tool_name: String,
441        args: serde_json::Value,
442        timeout_ms: u64,
443    },
444
445    /// Confirmation received from user (HITL)
446    #[serde(rename = "confirmation_received")]
447    ConfirmationReceived {
448        tool_id: String,
449        approved: bool,
450        reason: Option<String>,
451    },
452
453    /// Confirmation timed out (HITL)
454    #[serde(rename = "confirmation_timeout")]
455    ConfirmationTimeout {
456        tool_id: String,
457        action_taken: String, // "rejected" or "auto_approved"
458    },
459
460    /// Structured question. Distinct from permission `Ask` and from steer.
461    #[serde(rename = "user_question")]
462    UserQuestion {
463        question_id: String,
464        question: String,
465        options: Vec<String>,
466        #[serde(default)]
467        allow_free_text: bool,
468    },
469
470    /// External task pending (needs SDK processing)
471    #[serde(rename = "external_task_pending")]
472    ExternalTaskPending {
473        task_id: String,
474        session_id: String,
475        lane: crate::queue::SessionLane,
476        command_type: String,
477        payload: serde_json::Value,
478        timeout_ms: u64,
479    },
480
481    /// External task completed
482    #[serde(rename = "external_task_completed")]
483    ExternalTaskCompleted {
484        task_id: String,
485        session_id: String,
486        success: bool,
487    },
488
489    /// Tool execution denied by permission policy
490    #[serde(rename = "permission_denied")]
491    PermissionDenied {
492        tool_id: String,
493        tool_name: String,
494        args: serde_json::Value,
495        reason: String,
496    },
497
498    /// Context resolution started
499    #[serde(rename = "context_resolving")]
500    ContextResolving { providers: Vec<String> },
501
502    /// Context resolution completed
503    #[serde(rename = "context_resolved")]
504    ContextResolved {
505        total_items: usize,
506        total_tokens: usize,
507    },
508
509    /// The model-visible capability surface observed before a provider call.
510    /// Repeated calls reuse the digest until tools, policy, workspace services,
511    /// or retrieval readiness/generation changes.
512    #[serde(rename = "run_capability_bound")]
513    RunCapabilityBound {
514        call_sequence: u64,
515        snapshot: crate::harness_evidence::RunCapabilitySnapshotV1,
516    },
517
518    /// The frozen presentation-profile identity, candidate definition cost,
519    /// and exact presented definition cost for one model call.
520    #[serde(rename = "model_presentation_bound")]
521    ModelPresentationBound {
522        snapshot: crate::harness_evidence::ModelPresentationSnapshotV1,
523    },
524
525    /// Bounded content-addressed evidence for the actual provider-neutral
526    /// arguments submitted to one model call.
527    #[serde(rename = "model_input_bound")]
528    ModelInputBound {
529        snapshot: crate::harness_evidence::ModelInputSnapshotV1,
530    },
531
532    /// Per-call correlation between repeated Tool-result context, Code's
533    /// prompt estimate, and the normalized usage report returned by the
534    /// provider-neutral model client.
535    #[serde(rename = "model_usage_bound")]
536    ModelUsageBound {
537        snapshot: crate::harness_evidence::ModelUsageSnapshotV1,
538    },
539
540    /// One run is using the exact cognitive-package generation retained by
541    /// the surrounding session snapshot.
542    #[serde(rename = "cognitive_context_bound")]
543    CognitiveContextBound {
544        binding: crate::cognitive_context::CognitivePackageBindingV1,
545    },
546
547    // ========================================================================
548    // a3s-lane integration events
549    // ========================================================================
550    /// Command moved to dead letter queue after exhausting retries
551    #[serde(rename = "command_dead_lettered")]
552    CommandDeadLettered {
553        command_id: String,
554        command_type: String,
555        lane: String,
556        error: String,
557        attempts: u32,
558    },
559
560    /// Command retry attempt
561    #[serde(rename = "command_retry")]
562    CommandRetry {
563        command_id: String,
564        command_type: String,
565        lane: String,
566        attempt: u32,
567        delay_ms: u64,
568    },
569
570    /// Queue alert (depth warning, latency alert, etc.)
571    #[serde(rename = "queue_alert")]
572    QueueAlert {
573        level: String,
574        alert_type: String,
575        message: String,
576    },
577
578    // ========================================================================
579    // Task tracking events
580    // ========================================================================
581    /// Task list updated
582    #[serde(rename = "task_updated")]
583    TaskUpdated {
584        session_id: String,
585        tasks: Vec<crate::planning::Task>,
586    },
587
588    // ========================================================================
589    // Memory System events (Phase 3)
590    // ========================================================================
591    /// Memory stored
592    #[serde(rename = "memory_stored")]
593    MemoryStored {
594        memory_id: String,
595        memory_type: String,
596        importance: f32,
597        tags: Vec<String>,
598    },
599
600    /// Memory recalled
601    #[serde(rename = "memory_recalled")]
602    MemoryRecalled {
603        memory_id: String,
604        content: String,
605        relevance: f32,
606    },
607
608    /// Memories searched
609    #[serde(rename = "memories_searched")]
610    MemoriesSearched {
611        query: Option<String>,
612        tags: Vec<String>,
613        result_count: usize,
614    },
615
616    /// Memory cleared
617    #[serde(rename = "memory_cleared")]
618    MemoryCleared {
619        tier: String, // "long_term", "short_term", "working"
620        count: u64,
621    },
622
623    // ========================================================================
624    // Subagent events
625    // ========================================================================
626    /// Subagent task started
627    #[serde(rename = "subagent_start")]
628    SubagentStart {
629        /// Unique task identifier
630        task_id: String,
631        /// Child session ID
632        session_id: String,
633        /// Parent session ID
634        parent_session_id: String,
635        /// Agent type (e.g., "explore", "general")
636        agent: String,
637        /// Short description of the task
638        description: String,
639        /// Wall-clock start timestamp in milliseconds since Unix epoch.
640        #[serde(default)]
641        started_ms: u64,
642    },
643
644    /// Subagent task progress update
645    #[serde(rename = "subagent_progress")]
646    SubagentProgress {
647        /// Task identifier
648        task_id: String,
649        /// Child session ID
650        session_id: String,
651        /// Progress status message
652        status: String,
653        /// Additional metadata
654        metadata: serde_json::Value,
655    },
656
657    /// Subagent task completed
658    #[serde(rename = "subagent_end")]
659    SubagentEnd {
660        /// Task identifier
661        task_id: String,
662        /// Child session ID
663        session_id: String,
664        /// Agent type
665        agent: String,
666        /// Task output/result
667        output: String,
668        /// Whether the task succeeded
669        success: bool,
670        /// Wall-clock finish timestamp in milliseconds since Unix epoch.
671        #[serde(default)]
672        finished_ms: u64,
673    },
674
675    // ========================================================================
676    // Planning and Goal Tracking Events (Phase 1)
677    // ========================================================================
678    /// Planning phase started
679    #[serde(rename = "planning_start")]
680    PlanningStart { prompt: String },
681
682    /// Planning phase completed
683    #[serde(rename = "planning_end")]
684    PlanningEnd {
685        plan: ExecutionPlan,
686        estimated_steps: usize,
687    },
688
689    /// Step execution started
690    #[serde(rename = "step_start")]
691    StepStart {
692        step_id: String,
693        description: String,
694        step_number: usize,
695        total_steps: usize,
696    },
697
698    /// Step execution completed
699    #[serde(rename = "step_end")]
700    StepEnd {
701        step_id: String,
702        status: TaskStatus,
703        step_number: usize,
704        total_steps: usize,
705    },
706
707    /// Goal extracted from prompt
708    #[serde(rename = "goal_extracted")]
709    GoalExtracted { goal: AgentGoal },
710
711    /// Goal progress update
712    #[serde(rename = "goal_progress")]
713    GoalProgress {
714        goal: String,
715        progress: f32,
716        completed_steps: usize,
717        total_steps: usize,
718    },
719
720    /// Goal achieved
721    #[serde(rename = "goal_achieved")]
722    GoalAchieved {
723        goal: String,
724        total_steps: usize,
725        duration_ms: i64,
726    },
727
728    // ========================================================================
729    // Context Compaction events
730    // ========================================================================
731    /// Context automatically compacted due to high usage
732    #[serde(rename = "context_compacted")]
733    ContextCompacted {
734        session_id: String,
735        before_messages: usize,
736        after_messages: usize,
737        percent_before: f32,
738        /// Cumulative continuation summary when message history was reduced.
739        /// Prune-only compactions omit this field.
740        #[serde(default, skip_serializing_if = "Option::is_none")]
741        summary: Option<String>,
742    },
743
744    // ========================================================================
745    // In-flight run control events
746    // ========================================================================
747    /// A host steer/interrupt request was consumed at a loop safe point.
748    #[serde(rename = "run_control_applied")]
749    RunControlApplied {
750        request_id: String,
751        operation: crate::run_control::RunControlOperation,
752        turn_id: Option<String>,
753        turn_revision: u64,
754        #[serde(default, skip_serializing_if = "Option::is_none")]
755        input: Option<String>,
756        #[serde(default, skip_serializing_if = "Option::is_none")]
757        reason: Option<String>,
758    },
759
760    // ========================================================================
761    // Persistence events
762    // ========================================================================
763    /// Session persistence failed — SDK clients should handle this
764    #[serde(rename = "persistence_failed")]
765    PersistenceFailed {
766        session_id: String,
767        operation: String,
768        error: String,
769    },
770
771    // ========================================================================
772    // Cluster / platform events
773    //
774    // These variants are emitted by the host platform via
775    // `HookExecutor` and are not produced by the agent loop itself. They
776    // give in-session code a uniform way to observe platform-level
777    // decisions (budget exhaustion, scheduled passivation, peer
778    // invocations) without coupling to the host's transport.
779    // ========================================================================
780    /// A budget threshold was crossed for this session/tenant.
781    ///
782    /// Emitted by a host `BudgetGuard` impl when LLM/tool spend hits a
783    /// soft or hard threshold. The session is **not** automatically
784    /// halted — `kind` lets in-session policy decide (e.g. fast-compact
785    /// at "soft", refuse next LLM call at "hard").
786    #[serde(rename = "budget_threshold_hit")]
787    BudgetThresholdHit {
788        /// Logical resource: "llm_tokens", "tool_calls", "wall_time",
789        /// "usd_cost", or host-defined.
790        resource: String,
791        /// "soft" or "hard"; host-defined semantics beyond that.
792        kind: String,
793        /// Current consumed amount in the same unit as `limit`.
794        consumed: f64,
795        /// Threshold that was crossed.
796        limit: f64,
797        /// Optional explanation for logs / UI.
798        #[serde(default, skip_serializing_if = "Option::is_none")]
799        message: Option<String>,
800    },
801
802    /// The host is asking the session to release in-memory state.
803    ///
804    /// Emitted before the host calls `session.close()` or moves the
805    /// session to another node. Session code that holds large caches
806    /// can react (flush to memory store, drop derived state). The
807    /// framework does not act on this event itself.
808    #[serde(rename = "passivation_requested")]
809    PassivationRequested {
810        /// "idle_reaper", "node_drain", "migration", "manual", or
811        /// host-defined.
812        reason: String,
813        /// Optional deadline (Unix epoch ms) before forced close.
814        #[serde(default, skip_serializing_if = "Option::is_none")]
815        deadline_ms: Option<u64>,
816    },
817
818    /// Another session in the cluster has invoked this one.
819    ///
820    /// Lets in-session hooks distinguish "human-driven send" from
821    /// "peer-driven send" without inspecting prompts. The host routes
822    /// the actual prompt through the normal `send` / `stream` path;
823    /// this event is metadata only.
824    #[serde(rename = "peer_invocation")]
825    PeerInvocation {
826        /// Session id of the invoking peer (cluster-stable).
827        from_session_id: String,
828        /// Optional tenant of the invoking peer.
829        #[serde(default, skip_serializing_if = "Option::is_none")]
830        from_tenant_id: Option<String>,
831        /// Distributed-trace correlation id linking the two sessions.
832        #[serde(default, skip_serializing_if = "Option::is_none")]
833        correlation_id: Option<String>,
834    },
835}
836
837/// Result of agent execution
838#[derive(Debug, Clone)]
839pub struct AgentResult {
840    pub text: String,
841    pub messages: Vec<Message>,
842    pub usage: TokenUsage,
843    pub tool_calls_count: usize,
844    pub verification_reports: Vec<crate::verification::VerificationReport>,
845    /// How the run was allowed to finish. Incomplete mutations do not produce this value.
846    pub completion: crate::harness_loop::CompletionTerminal,
847    /// `ordinary` or `plan_implementation`. A plan-exit claim without a digest stays ordinary.
848    pub run_admission: String,
849}
850
851/// An execution error paired with the usage and tool-call accounting that was
852/// completed before the failure.
853///
854/// Agent execution still returns an error: this type only prevents already
855/// incurred work from disappearing when a provider call, tool round, or
856/// capability boundary fails. Rust callers can downcast the returned
857/// `anyhow::Error` to this type when they need the partial accounting.
858#[derive(Debug)]
859pub struct AgentExecutionFailure {
860    source: anyhow::Error,
861    usage: TokenUsage,
862    tool_calls_count: usize,
863}
864
865impl AgentExecutionFailure {
866    pub(crate) fn new(source: anyhow::Error, usage: TokenUsage, tool_calls_count: usize) -> Self {
867        Self {
868            source,
869            usage,
870            tool_calls_count,
871        }
872    }
873
874    /// Usage reported by provider calls that completed before the failure.
875    pub fn usage(&self) -> &TokenUsage {
876        &self.usage
877    }
878
879    /// Tool calls admitted before the failure, including failed tool calls.
880    pub fn tool_calls_count(&self) -> usize {
881        self.tool_calls_count
882    }
883
884    pub(crate) fn add_tool_calls(&mut self, count: usize) {
885        self.tool_calls_count = self.tool_calls_count.saturating_add(count);
886    }
887}
888
889impl std::fmt::Display for AgentExecutionFailure {
890    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
891        self.source.fmt(f)
892    }
893}
894
895impl std::error::Error for AgentExecutionFailure {
896    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
897        Some(self.source.as_ref())
898    }
899}
900
901impl AgentResult {
902    pub fn verification_summary(&self) -> crate::verification::VerificationSummary {
903        crate::verification::VerificationSummary::from_reports(&self.verification_reports)
904    }
905
906    pub fn verification_summary_text(&self) -> String {
907        crate::verification::format_verification_summary(&self.verification_summary())
908    }
909
910    pub fn has_pending_verification(&self) -> bool {
911        matches!(
912            self.verification_summary().status,
913            crate::verification::VerificationStatus::NeedsReview
914        )
915    }
916}
917
918// ============================================================================
919// ToolCommand — bridges ToolExecutor to SessionCommand for queue submission
920// ============================================================================
921
922/// Adapter that implements `SessionCommand` for tool execution via the queue.
923///
924/// Wraps a `ToolExecutor` call so it can be submitted to `SessionLaneQueue`.
925pub struct ToolCommand {
926    tool_executor: Arc<ToolExecutor>,
927    tool_name: String,
928    tool_args: Value,
929    tool_context: ToolContext,
930    tool_timeout_ms: Option<u64>,
931}
932
933impl ToolCommand {
934    /// Create a new ToolCommand
935    pub fn new(
936        tool_executor: Arc<ToolExecutor>,
937        tool_name: String,
938        tool_args: Value,
939        tool_context: ToolContext,
940        tool_timeout_ms: Option<u64>,
941    ) -> Self {
942        Self {
943            tool_executor,
944            tool_name,
945            tool_args,
946            tool_context,
947            tool_timeout_ms,
948        }
949    }
950}
951
952#[async_trait]
953impl SessionCommand for ToolCommand {
954    async fn execute(&self) -> Result<Value> {
955        if self.tool_context.is_cancelled() {
956            anyhow::bail!("Tool '{}' cancelled before queue execution", self.tool_name);
957        }
958
959        let result = tool_execution_runtime::execute_tool_with_deadline(
960            self.tool_executor.as_ref(),
961            &self.tool_name,
962            &self.tool_args,
963            &self.tool_context,
964            self.tool_timeout_ms,
965        )
966        .await?;
967        let images = result
968            .images
969            .iter()
970            .map(|image| {
971                serde_json::json!({
972                    "data": image.base64_data(),
973                    "media_type": image.media_type,
974                })
975            })
976            .collect::<Vec<_>>();
977        Ok(serde_json::json!({
978            "output": result.output,
979            "exit_code": result.exit_code,
980            "metadata": result.metadata,
981            "images": images,
982            "error_kind": result.error_kind,
983        }))
984    }
985
986    fn command_type(&self) -> &str {
987        &self.tool_name
988    }
989
990    fn payload(&self) -> Value {
991        self.tool_args.clone()
992    }
993}
994
995// ============================================================================
996// AgentLoop
997// ============================================================================
998
999/// Internal agent loop executor.
1000#[derive(Clone)]
1001pub(crate) struct AgentLoop {
1002    llm_client: Arc<dyn LlmClient>,
1003    model_generation_admission: crate::llm::ModelGenerationAdmission,
1004    /// Whether the surrounding session explicitly owns this admission gate.
1005    /// Compatibility/standalone loops keep the historical per-facade gate so
1006    /// detached maintenance work (for example background memory extraction)
1007    /// cannot stall the foreground turn.
1008    shared_model_generation_admission: bool,
1009    /// Secret-free middleware stage counters shared with the session when
1010    /// installed via [`AgentLoop::with_model_middleware_obs`].
1011    middleware_obs: Arc<ModelMiddlewareObs>,
1012    tool_executor: Arc<ToolExecutor>,
1013    tool_context: ToolContext,
1014    config: AgentConfig,
1015    /// Optional lane queue for priority-based tool execution
1016    command_queue: Option<Arc<SessionLaneQueue>>,
1017    /// Optional sink for per-tool-round checkpoints. Populated by
1018    /// `build_agent_loop` when the session has a configured
1019    /// `SessionStore`. The agent loop uses
1020    /// [`AgentLoop::set_checkpoint_run`] to bind a run id before
1021    /// `execute_with_session`, then persists a checkpoint after each
1022    /// completed tool round.
1023    pub(crate) checkpoint_sink: Option<Arc<dyn crate::loop_checkpoint::LoopCheckpointSink>>,
1024    /// Run id under which checkpoints are stored. Reset per execution
1025    /// via [`AgentLoop::set_checkpoint_run`].
1026    pub(crate) checkpoint_run_id: Option<String>,
1027    /// Complete immutable capability identity captured from the admitted Run.
1028    /// Compatibility loops without a scoped Run leave this empty.
1029    pub(crate) checkpoint_capability_binding: Option<crate::capability::RunCapabilityBindingV1>,
1030    /// The invocation shared by every model helper in the currently scoped
1031    /// run. Base session loops keep this empty and bind it on execution.
1032    bound_invocation: Option<InvocationContext>,
1033    /// Weak capability parent used to create and close one real Turn scope per
1034    /// provider/tool iteration. It never extends the admitted Run lease.
1035    capability_runtime: Option<crate::capability::AgentCapabilityRuntime>,
1036}
1037
1038#[cfg(test)]
1039pub(crate) mod tests;
1040
1041#[cfg(test)]
1042mod extra_agent_tests;
1043
1044#[cfg(test)]
1045mod model_retry_soak;
1046
1047#[cfg(test)]
1048mod conversation_compaction_soak;