Skip to main content

runifold_agent/
completion.rs

1//! Agent terminal-completion contracts and bounded repair policy.
2
3use serde::{Deserialize, Serialize};
4
5/// Bounded policy applied when a model returns an invalid terminal candidate.
6///
7/// Repairs are explicit and consume the ordinary turn, token, cost, duration,
8/// and deadline budgets. A zero repair limit preserves fail-fast behavior.
9#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
10pub struct CompletionRequirement {
11    max_terminal_repairs: u32,
12    retry_empty_response: bool,
13}
14
15impl CompletionRequirement {
16    /// Creates a fail-fast completion policy.
17    pub const fn new() -> Self {
18        Self {
19            max_terminal_repairs: 0,
20            retry_empty_response: false,
21        }
22    }
23
24    /// Sets the independent maximum number of terminal repair turns.
25    #[must_use]
26    pub const fn max_repairs(mut self, maximum: u32) -> Self {
27        self.max_terminal_repairs = maximum;
28        self
29    }
30
31    /// Allows an empty or non-model-visible terminal response to consume a
32    /// repair turn instead of failing immediately.
33    #[must_use]
34    pub const fn retry_empty_response(mut self, enabled: bool) -> Self {
35        self.retry_empty_response = enabled;
36        self
37    }
38
39    /// Returns the configured terminal repair limit.
40    pub const fn max_terminal_repairs(self) -> u32 {
41        self.max_terminal_repairs
42    }
43
44    /// Returns whether empty terminal candidates are repairable.
45    pub const fn retries_empty_response(self) -> bool {
46        self.retry_empty_response
47    }
48}
49
50/// Stable reason why a terminal model response did not satisfy the Agent.
51#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
52#[serde(rename_all = "snake_case")]
53#[non_exhaustive]
54pub enum TerminalRequirementFailureKind {
55    /// No model-visible terminal content was produced.
56    EmptyResponse,
57    /// A structured response contained no textual JSON body.
58    MissingStructuredText,
59    /// Structured text did not decode as the requested Rust type.
60    InvalidStructuredOutput,
61    /// The provider returned an explicit refusal.
62    Refusal,
63}
64
65/// Safe, bounded diagnostic for a rejected terminal candidate.
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
67pub struct TerminalRequirementFailure {
68    /// Stable failure category.
69    pub kind: TerminalRequirementFailureKind,
70    /// One-based JSON line for a structured decoding failure.
71    pub line: Option<usize>,
72    /// One-based JSON column for a structured decoding failure.
73    pub column: Option<usize>,
74}
75
76impl TerminalRequirementFailure {
77    pub(crate) const fn new(kind: TerminalRequirementFailureKind) -> Self {
78        Self {
79            kind,
80            line: None,
81            column: None,
82        }
83    }
84
85    pub(crate) const fn repairable(&self, policy: CompletionRequirement) -> bool {
86        match self.kind {
87            TerminalRequirementFailureKind::EmptyResponse
88            | TerminalRequirementFailureKind::MissingStructuredText => {
89                policy.retries_empty_response()
90            }
91            TerminalRequirementFailureKind::InvalidStructuredOutput => true,
92            TerminalRequirementFailureKind::Refusal => false,
93        }
94    }
95}