supercode-harness 0.4.8

The optional native Supercode agent and tool harness
Documentation
use thiserror::Error;

/// Result alias used throughout the crate.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can arise while configuring or running an [`crate::Agent`].
///
/// `#[non_exhaustive]` so new variants can be added without a breaking release;
/// match with a `_` arm.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
    /// A native session interchange operation failed.
    #[error(transparent)]
    Interchange(#[from] supercode_interchange::InterchangeError),

    /// No API key was provided and none could be found in the environment.
    #[error("missing API key: set it on the Config or via the {0} environment variable")]
    MissingApiKey(String),

    /// The HTTP transport failed. The underlying error is kept as an opaque
    /// source rather than exposing the `reqwest` type, so a transport-library
    /// bump is not a breaking change for this crate's public API.
    #[error("http transport error: {0}")]
    Http(#[source] Box<dyn std::error::Error + Send + Sync>),

    /// The provider returned a non-success status.
    #[error("provider returned status {status}: {body}")]
    Provider {
        /// HTTP status code.
        status: u16,
        /// Raw response body (truncated upstream if large).
        body: String,
    },

    /// A response body could not be parsed.
    #[error("failed to decode provider response: {0}")]
    Decode(#[from] serde_json::Error),

    /// A persisted session artifact failed its own framing/schema contract.
    #[error("invalid session artifact: {0}")]
    InvalidSession(String),

    /// A versioned SDK/runtime operation failed. Runtime adapters retain this
    /// typed value so outer SDK surfaces preserve its stable error name.
    #[error(transparent)]
    Sdk(#[from] crate::sdk::SdkError),

    /// The model asked for a tool that isn't registered.
    ///
    /// Constructed by the agent loop's `run_tool` dispatch and fed back to the
    /// model as this variant's `Display` rendering, so it is load-bearing on
    /// the real tool-call path, not just a documented-but-unused variant.
    #[error("model requested unknown tool: {0}")]
    UnknownTool(String),

    /// A tool's input arguments were not valid for its schema.
    ///
    /// Constructed both by built-in tools' argument parsing (`parse_args`) and
    /// by the agent loop's `run_tool` when the model's raw argument JSON fails
    /// to parse; either way its `Display` rendering is what the model sees.
    #[error("invalid arguments for tool `{tool}`: {message}")]
    InvalidArguments {
        /// The tool that was called.
        tool: String,
        /// What was wrong with the arguments.
        message: String,
    },

    /// A tool failed while executing.
    #[error("tool `{tool}` failed: {message}")]
    Tool {
        /// The tool that failed.
        tool: String,
        /// Failure detail.
        message: String,
    },

    /// The agent loop exceeded its configured iteration budget.
    #[error("agent exceeded the maximum of {0} reasoning/tool iterations without finishing")]
    MaxIterations(usize),

    /// An I/O operation failed.
    #[error("io error: {0}")]
    Io(#[from] std::io::Error),

    /// PARITY-18 D4 — a live request would exceed the target model's
    /// context window even after [`crate::tokens::context_guard`]'s safety
    /// margin and completion reserve are applied. Raised by
    /// `crate::Agent::run_loop`'s per-send guard, which runs before EVERY
    /// request this agent issues (not only the first) once
    /// [`crate::Agent::set_context_limit`] has armed it — so an
    /// over-context request is refused at any point in a session, not just
    /// at the CLI's one-shot preflight.
    ///
    /// PARITY-18 v3 — `projected_tokens` is [`crate::tokens::context_guard`]'s
    /// margin-adjusted estimate of messages+tools ONLY; it does NOT include
    /// the completion reserve, so the refusal condition is actually
    /// `projected_tokens + reserve_tokens > context_limit`, not
    /// `projected_tokens > context_limit` — printing the bare comparison
    /// (v2's wording) was arithmetically false as written (e.g. "projected
    /// 193,064 > limit 200,000" reads as passing when the refusal is only
    /// true once the reserve is added). `reserve_tokens` is carried on the
    /// error so the `Display` impl states the true inequality.
    #[error(
        "cannot reduce below context limit: projected {projected_tokens} tokens + {reserve_tokens} reserve exceeds model {model} limit {context_limit}"
    )]
    ContextLimitExceeded {
        /// Margin-adjusted projected token count for the request that was
        /// about to be sent (messages + tools only; excludes the completion
        /// reserve — see `reserve_tokens`).
        projected_tokens: u64,
        /// The completion-token reserve
        /// ([`crate::tokens::CONTEXT_RESPONSE_RESERVE_TOKENS`]) added to
        /// `projected_tokens` to derive the true refusal condition:
        /// `projected_tokens + reserve_tokens > context_limit`.
        reserve_tokens: u64,
        /// The target model's context-window size.
        context_limit: u64,
        /// The model slug this limit was resolved for.
        model: String,
    },

    /// P5-3 (§2 module 9 `subagents`, §5.3-style resource bound): a
    /// `spawn_subagent` call was refused because it would exceed
    /// `capabilities.subagents.max_depth` — the fail-closed depth cap that
    /// keeps a parent-spawning-children-spawning-children chain from
    /// growing unbounded. Named so the model (and a test) can tell this
    /// apart from every other tool-error shape.
    #[error(
        "subagent spawn refused: depth {attempted_depth} would exceed \
         capabilities.subagents.max_depth={max_depth}"
    )]
    SubagentDepthExceeded {
        /// The configured cap.
        max_depth: usize,
        /// The depth the new child would have been spawned at.
        attempted_depth: usize,
    },

    /// P5-3 (§2 module 9, §5.3-style resource bound): a `spawn_subagent`
    /// call was refused because `capabilities.subagents.max_concurrent`
    /// subagents are already in flight ANYWHERE in this spawn tree (the
    /// concurrency gauge is shared root-to-leaf) — the fail-closed
    /// fork-bomb guard.
    #[error(
        "subagent spawn refused: {max_concurrent} subagent(s) already running \
         (capabilities.subagents.max_concurrent={max_concurrent})"
    )]
    SubagentConcurrencyExceeded {
        /// The configured cap.
        max_concurrent: usize,
    },

    /// P5-3 (§2.2 C6): a `background: true` spawn was refused because no
    /// `capabilities.subagents.background_prompts` auto-policy
    /// (`"auto_policy"` or `"parent"`) is configured — a detached child
    /// cannot prompt interactively, so this is enforced fail-closed at
    /// spawn time, defensively re-checking what
    /// `crate::configfile::validate_modules`'s C6 resolver rule already
    /// requires at config-resolve time (belt-and-suspenders for a `Config`
    /// hand-built via [`crate::ConfigBuilder`] that bypassed the resolver).
    #[error(
        "subagent spawn refused: background=true requires \
         capabilities.subagents.background_prompts = \"auto_policy\" or \"parent\" (§2.2 C6) \
         — none is configured"
    )]
    SubagentBackgroundPolicyMissing,

    /// P5-3: `spawn_subagent`'s `agent_type` named an agent definition not
    /// present in `capabilities.subagents.agents`.
    #[error("unknown subagent agent_type `{0}` — not defined in capabilities.subagents.agents")]
    SubagentDefinitionNotFound(String),

    /// P5-3: `subagent_status` (or an internal join) named a subagent id
    /// this agent never spawned (or one already reaped).
    #[error("unknown subagent id `{0}`")]
    SubagentNotFound(String),

    /// P5-6 (§2 module 4 `tools.background`, resource bound): a
    /// `background_exec` call was refused because
    /// `capabilities.tools_background.max_concurrent` background jobs are
    /// already running for this agent — fail-closed, mirroring
    /// [`Error::SubagentConcurrencyExceeded`]'s cap treatment (§2 module
    /// 9).
    #[error(
        "background exec refused: {max_concurrent} background job(s) already running \
         (capabilities.tools_background.max_concurrent={max_concurrent})"
    )]
    BackgroundJobConcurrencyExceeded {
        /// The configured cap.
        max_concurrent: usize,
    },

    /// P5-6: `background_status`/`background_kill` named a job id this
    /// agent never spawned (or one already reaped after finishing).
    #[error("unknown background job id `{0}`")]
    BackgroundJobNotFound(String),

    /// A reversible reduction invariant or sidecar pointer check failed.
    #[error(transparent)]
    Reduction(#[from] supercode_reduce::ReductionError),

    /// Catch-all for everything else.
    #[error("{0}")]
    Other(String),
}

impl Error {
    /// Convenience constructor for a tool failure.
    pub fn tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
        Error::Tool {
            tool: tool.into(),
            message: message.into(),
        }
    }
}

// Kept as a manual conversion (not `#[from]`) so the `reqwest` type stays out
// of the public API surface — see `Error::Http`.
impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        Error::Http(Box::new(e))
    }
}