pub mod agent_node;
pub mod chrome;
pub mod hosted;
pub mod methods;
pub mod schema;
pub mod speech;
pub mod stats;
pub mod workflows;

use roder_api::artifacts::{
    ArtifactGrepPage, ArtifactReadPage, ArtifactTailPage, ContextArtifactDescriptor,
    ContextArtifactKind,
};

use roder_api::automations::{
    AutomationConcurrencyPolicy, AutomationDefinition, AutomationId, AutomationProject,
    AutomationRunId, AutomationRunState, AutomationRunSummary, AutomationSchedule, CatchUpPolicy,
};
use roder_api::capabilities::CapabilityStatus;
use roder_api::code_index::{
    CodeChunk, CodeIndexGenerationId, CodeIndexSearchResponse, CodeIndexStats, CodeIndexStatus,
    ContentProof,
};
use roder_api::context::ContextBlock;
use roder_api::discovery::{
    DiscoveryCatalog, DiscoveryCatalogGroup, DiscoveryCatalogItem, DiscoveryPromotionRecord,
};
use roder_api::events::{ThreadId, TurnId};
use roder_api::extension::{ExtensionId, ExtensionManifest};
pub use roder_api::forks::WorkspaceFork;
pub use roder_api::goals::{ThreadGoal, ThreadGoalStatus};
use roder_api::inference::{
    HostedWebSearchMode, InferenceCapabilities, ModelDescriptor, ModelSelection, ProviderAuthType,
    TokenUsage,
};
use roder_api::inference_routing::{
    InferenceRoutingCostDelta, InferenceRoutingDecision, InferenceRoutingOptionDescriptor,
    InferenceRoutingOutcome, ModelSelectionMode,
};
use roder_api::knowledge::{
    KnowledgeDocId, KnowledgeDocSummary, KnowledgeDocument, KnowledgeKind, KnowledgeLinkType,
    KnowledgeRevisionInfo, KnowledgeSearchResult as KnowledgeSearchMatch, KnowledgeStatus,
};
use roder_api::marketplace::{
    DedupedMarketplacePlugin, DefaultMarketplaceSelection, InstalledPluginRecord,
    MarketplaceDescriptor, MarketplaceKind, MarketplacePluginEntry, MarketplaceSource,
};
use roder_api::media::{MediaArtifact, MediaArtifactId, MediaAttachment, MediaPreview};
use roder_api::memory::{
    MemoryId, MemoryProviderSelection, MemoryRecord, MemoryScope, MemorySearchResult,
};
use roder_api::packages::{PackageRecord, PackageResource, PackageResourceFilters, PackageScope};
use roder_api::plan_review::{
    HunkId, HunkRecord, PagedHunkDiff, PlanComment, PlanCommentAnchor, PlanReview, PlanReviewId,
    PlanRewrite,
};
use roder_api::policy_mode::PolicyMode;
use roder_api::processes::{ProcessDescriptor, ProcessId, ProcessOutput, ProcessStopResult};
use roder_api::retrieval::{RetrievalMeasuredOutcome, RetrievalMode, RetrievalRoutePlan};
use roder_api::skills::{Skill, SkillDescriptor, SkillExposure, SkillSelector};
use roder_api::subagents::SubagentPermissionMode;
use roder_api::tasks::{TaskHandle, TaskOutputStream};
use roder_api::teams::{
    AgentTeamDisplayMode, TeamId, TeamMailboxMessage, TeamMemberDescriptor, TeamMemberId,
    TeamMemberStatus, TeamTaskDescriptor,
};
use roder_api::thread::ThreadUsageMetadata;
use roder_api::tools::ToolSpec;
use roder_api::trace::{SubagentTraceDelta, SubagentTraceId, SubagentTraceSummary};
use roder_api::transcript::InputImage;
use roder_api::version_control::VcsChangeArea;
use roder_api::workflow::{
    WorkflowImportDecision, WorkflowImportItem, WorkflowImportScan, WorkflowImportState,
};
use roder_api::workspace_changes::WorkspaceChangeObservation;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::collections::HashMap;
use time::OffsetDateTime;

pub use chrome::*;
pub use speech::*;
pub use workflows::*;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcRequest {
    #[serde(default = "default_jsonrpc_version")]
    pub jsonrpc: String,
    pub id: Option<serde_json::Value>,
    pub method: String,
    pub params: Option<serde_json::Value>,
}

fn default_jsonrpc_version() -> String {
    "2.0".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcResponse {
    pub jsonrpc: String,
    pub id: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub result: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error: Option<JsonRpcError>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcNotification {
    #[serde(default = "default_jsonrpc_version")]
    pub jsonrpc: String,
    pub method: String,
    pub params: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
    pub code: i32,
    pub message: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub data: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResult {
    pub provider: String,
    pub model: String,
    pub cwd: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStatus {
    #[serde(rename = "type")]
    pub kind: String,
    pub active_turn_id: Option<TurnId>,
    pub active_flags: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Thread {
    pub id: ThreadId,
    pub preview: String,
    pub model_provider: String,
    pub model: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selection_mode: Option<ModelSelectionMode>,
    pub created_at: i64,
    pub updated_at: i64,
    pub status: ThreadStatus,
    pub cwd: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message_count: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turns: Option<Vec<Turn>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub usage: Option<ThreadUsageMetadata>,
    /// Per-thread tool filter applied on top of the runtime allowlist. Empty = no filtering.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub tool_allowlist: Vec<String>,
    /// Host-supplied instructions added to the developer slot of every turn's inference request.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub developer_instructions: Option<String>,
    /**
     * Host-executed tool specs advertised to the model on every turn of this thread. Calls pause
     * on `thread/toolExecutionRequested` until the client answers with `tools/resolve`.
     */
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub external_tools: Vec<ToolSpec>,
    /**
     * Remote-runner binding fixed at thread/start; absent = native coding tools execute locally.
     * Lets hosts verify that a reused thread targets the intended runner workspace, since the
     * binding never changes after creation. The destination config carries no secrets.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runner: Option<ThreadRunnerParams>,
    /// Parent thread for conversation forks; absent for normal threads.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_thread_id: Option<ThreadId>,
    /// Compact workspace-fork provenance for forked threads.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_fork: Option<WorkspaceFork>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Turn {
    pub id: TurnId,
    pub items: Vec<Item>,
    pub items_view: String,
    pub status: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<serde_json::Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub started_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub completed_at: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub usage: Option<TokenUsage>,
    /**
     * Normalized stop reason of the turn's terminal inference step ("stop",
     * "length", "toolUse", "contentFilter", "refusal", or a provider-native
     * value passed through). Present on `turn/completed` for completed turns.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub finish_reason: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ThreadItemStatus {
    InProgress,
    Completed,
    Failed,
}

impl From<roder_api::thread::ThreadItemStatus> for ThreadItemStatus {
    fn from(value: roder_api::thread::ThreadItemStatus) -> Self {
        match value {
            roder_api::thread::ThreadItemStatus::InProgress => Self::InProgress,
            roder_api::thread::ThreadItemStatus::Completed => Self::Completed,
            roder_api::thread::ThreadItemStatus::Failed => Self::Failed,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Item {
    UserMessage {
        id: String,
        text: String,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        images: Vec<InputImage>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    AgentMessage {
        id: String,
        text: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        phase: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    Reasoning {
        id: String,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        summary: Vec<String>,
        #[serde(default, skip_serializing_if = "Vec::is_empty")]
        content: Vec<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    ToolExecution {
        id: String,
        #[serde(rename = "toolCallId")]
        tool_call_id: String,
        #[serde(rename = "toolName")]
        tool_name: String,
        status: ThreadItemStatus,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        input: Option<serde_json::Value>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        output: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        error: Option<String>,
    },
    RoutingDecision {
        id: String,
        decision: InferenceRoutingDecisionEvent,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    Compaction {
        id: String,
        summary: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    Error {
        id: String,
        message: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
    Raw {
        id: String,
        payload: serde_json::Value,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        status: Option<ThreadItemStatus>,
    },
}

impl From<roder_api::thread::ThreadItem> for Item {
    fn from(value: roder_api::thread::ThreadItem) -> Self {
        match value {
            roder_api::thread::ThreadItem::UserMessage {
                id,
                text,
                images,
                status,
            } => Self::UserMessage {
                id,
                text,
                images,
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::AgentMessage {
                id,
                text,
                phase,
                status,
            } => Self::AgentMessage {
                id,
                text,
                phase,
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::Reasoning {
                id,
                summary,
                content,
                status,
            } => Self::Reasoning {
                id,
                summary,
                content,
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::ToolExecution {
                id,
                tool_call_id,
                tool_name,
                status,
                input,
                output,
                error,
            } => Self::ToolExecution {
                id,
                tool_call_id,
                tool_name,
                status: status.into(),
                input,
                output,
                error,
            },
            roder_api::thread::ThreadItem::RoutingDecision {
                id,
                decision,
                status,
            } => Self::RoutingDecision {
                id,
                decision: decision.into(),
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::Compaction {
                id,
                summary,
                status,
            } => Self::Compaction {
                id,
                summary,
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::Error {
                id,
                message,
                status,
            } => Self::Error {
                id,
                message,
                status: status.map(Into::into),
            },
            roder_api::thread::ThreadItem::Raw {
                id,
                payload,
                status,
            } => Self::Raw {
                id,
                payload,
                status: status.map(Into::into),
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadItemDelta {
    AgentMessageText {
        delta: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        phase: Option<String>,
    },
    ReasoningText {
        delta: String,
        #[serde(rename = "contentIndex")]
        content_index: usize,
    },
    ReasoningSummaryPartAdded {
        #[serde(rename = "summaryIndex")]
        summary_index: usize,
    },
    ReasoningSummaryText {
        delta: String,
        #[serde(rename = "summaryIndex")]
        summary_index: usize,
    },
}

impl From<roder_api::thread::ThreadItemDelta> for ThreadItemDelta {
    fn from(value: roder_api::thread::ThreadItemDelta) -> Self {
        match value {
            roder_api::thread::ThreadItemDelta::AgentMessageText { delta, phase } => {
                Self::AgentMessageText { delta, phase }
            }
            roder_api::thread::ThreadItemDelta::ReasoningText {
                delta,
                content_index,
            } => Self::ReasoningText {
                delta,
                content_index,
            },
            roder_api::thread::ThreadItemDelta::ReasoningSummaryPartAdded { summary_index } => {
                Self::ReasoningSummaryPartAdded { summary_index }
            }
            roder_api::thread::ThreadItemDelta::ReasoningSummaryText {
                delta,
                summary_index,
            } => Self::ReasoningSummaryText {
                delta,
                summary_index,
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ThreadItemEventKind {
    ItemStarted {
        item: Item,
    },
    ItemDelta {
        #[serde(rename = "itemId")]
        item_id: String,
        delta: ThreadItemDelta,
    },
    ItemCompleted {
        item: Item,
    },
}

impl From<roder_api::thread::ThreadItemEventKind> for ThreadItemEventKind {
    fn from(value: roder_api::thread::ThreadItemEventKind) -> Self {
        match value {
            roder_api::thread::ThreadItemEventKind::ItemStarted { item } => {
                Self::ItemStarted { item: item.into() }
            }
            roder_api::thread::ThreadItemEventKind::ItemDelta { item_id, delta } => {
                Self::ItemDelta {
                    item_id,
                    delta: delta.into(),
                }
            }
            roder_api::thread::ThreadItemEventKind::ItemCompleted { item } => {
                Self::ItemCompleted { item: item.into() }
            }
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ThreadItemEvent {
    pub seq: u64,
    pub event_id: String,
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    #[serde(with = "time::serde::rfc3339")]
    pub timestamp: OffsetDateTime,
    pub event: ThreadItemEventKind,
}

impl From<roder_api::thread::ThreadItemEvent> for ThreadItemEvent {
    fn from(value: roder_api::thread::ThreadItemEvent) -> Self {
        Self {
            seq: value.seq,
            event_id: value.event_id,
            thread_id: value.thread_id,
            turn_id: value.turn_id,
            timestamp: value.timestamp,
            event: value.event.into(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStartParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selection: Option<ModelSelectChoice>,
    pub model: Option<String>,
    pub model_provider: Option<String>,
    pub reasoning: Option<String>,
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
    /**
     * Per-thread tool filter applied on top of the runtime allowlist. Absent = no
     * filtering; an explicit empty array is rejected with invalid params.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_allowlist: Option<Vec<String>>,
    /// Host-supplied instructions added to the developer slot of every turn's inference request.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub developer_instructions: Option<String>,
    /**
     * Host-executed tool specs advertised to the model on every turn of this thread. Calls pause
     * on `thread/toolExecutionRequested` until the client answers with `tools/resolve`.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub external_tools: Option<Vec<ToolSpec>>,
    /**
     * Binds the thread's native coding tools to a remote-runner workspace.
     * Absent = local execution, even when a runtime-level runner destination
     * is selected via `runners/select`.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub runner: Option<ThreadRunnerParams>,
    #[serde(default)]
    pub ephemeral: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ThreadRunnerParams {
    /// Installed remote-runner provider id (e.g. "e2b").
    pub provider_id: String,
    /**
     * Provider-specific destination config. Persisted with the thread, so it
     * must not carry secrets; providers read those from their environment.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub config: Option<serde_json::Value>,
    /// Absolute path on the runner used as the thread's coding-tool workspace root.
    pub workspace: String,
    /**
     * Extra absolute runner paths file reads may resolve under, beyond
     * `workspace`. Writes and the working directory stay confined to
     * `workspace`.
     */
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub read_roots: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStartResult {
    pub thread: Thread,
    pub model: String,
    pub model_provider: String,
    pub reasoning: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selection_mode: Option<ModelSelectionMode>,
    pub cwd: String,
    pub workspace_id: String,
    pub root_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceRoot {
    pub id: String,
    pub path: String,
    pub name: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Workspace {
    pub id: String,
    pub name: String,
    pub roots: Vec<WorkspaceRoot>,
    pub default_root_id: String,
    pub updated_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceRootInput {
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceListParams {}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceListResult {
    pub workspaces: Vec<Workspace>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceCreateParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    pub roots: Vec<WorkspaceRootInput>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_root_path: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceCreateResult {
    pub workspace: Workspace,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceUpdateParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub roots: Option<Vec<WorkspaceRootInput>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_root_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceUpdateResult {
    pub workspace: Workspace,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceForgetParams {
    pub workspace_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceForgetResult {
    pub workspace_id: String,
    pub forgotten: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadListParams {
    pub limit: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadListResult {
    pub data: Vec<Thread>,
    pub next_cursor: Option<String>,
    pub backwards_cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadReadParams {
    pub thread_id: ThreadId,
    #[serde(default)]
    pub include_turns: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadReadResult {
    pub thread: Option<Thread>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadArchiveParams {
    pub thread_id: ThreadId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadArchiveResult {
    pub thread_id: ThreadId,
    pub archived: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadForkParams {
    /// Parent thread to fork.
    pub thread_id: ThreadId,
    /// Fork name; the provider sanitizes it into its naming scheme.
    pub name: String,
    /// Fork at a specific parent turn; absent = latest turn.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub from_turn_id: Option<TurnId>,
    /// Fork provider id; absent = `git-worktree`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    /// Provider-specific options (never secrets).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_config: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadForkResult {
    /// The new child thread (its `cwd` is the fork workspace).
    pub thread: Thread,
    pub fork: WorkspaceFork,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadForkStatusParams {
    pub thread_id: ThreadId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadForkStatusResult {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub forked_from_turn_id: Option<TurnId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fork: Option<WorkspaceFork>,
    /// True when the fork is active but its workspace is missing.
    pub workspace_missing: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadRemoveForkParams {
    pub thread_id: ThreadId,
    /// Exact fork workspace path; required as destructive confirmation.
    pub confirm_path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadRemoveForkResult {
    pub thread_id: ThreadId,
    pub fork: WorkspaceFork,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksProvidersListResult {
    pub providers: Vec<roder_api::forks::ForkProviderDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksListParams {
    /// Source workspace to list forks of (absolute path).
    pub source_workspace: String,
    /// Provider id; absent = `git-worktree`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksListResult {
    pub forks: Vec<WorkspaceFork>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksCreateParams {
    pub source_workspace: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_config: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksCreateResult {
    pub fork: WorkspaceFork,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksRemoveParams {
    pub fork_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
    /// Exact fork workspace path; required as destructive confirmation.
    pub confirm_workspace: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ForksRemoveResult {
    pub fork_id: String,
    pub removed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalGetParams {
    pub thread_id: ThreadId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalGetResult {
    pub goal: Option<ThreadGoal>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalSetParams {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub objective: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub status: Option<ThreadGoalStatus>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_goal_token_budget_patch"
    )]
    pub token_budget: Option<Option<i64>>,
}

fn deserialize_goal_token_budget_patch<'de, D>(
    deserializer: D,
) -> Result<Option<Option<i64>>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = serde_json::Value::deserialize(deserializer)?;
    match value {
        serde_json::Value::Null => Ok(Some(None)),
        value => i64::deserialize(value)
            .map(Some)
            .map(Some)
            .map_err(serde::de::Error::custom),
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalSetResult {
    pub goal: Option<ThreadGoal>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalClearParams {
    pub thread_id: ThreadId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalClearResult {
    pub cleared: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnInputItem {
    #[serde(rename = "type")]
    pub kind: String,
    pub text: Option<String>,
    pub path: Option<String>,
    #[serde(default, alias = "image_url", skip_serializing_if = "Option::is_none")]
    pub image_url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnStartParams {
    pub thread_id: ThreadId,
    #[serde(default)]
    pub input: Vec<TurnInputItem>,
    pub prompt: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_provider: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reasoning: Option<String>,
    /**
     * Per-turn developer-authority context layered after the thread's
     * developerInstructions for this turn only. Never persisted to thread
     * metadata; absent means no per-turn context.
     */
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub developer_context: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_mode: Option<PolicyMode>,
    #[serde(default)]
    pub task_ledger_required: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnStartResult {
    pub turn_id: TurnId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnSteerParams {
    pub thread_id: ThreadId,
    pub expected_turn_id: TurnId,
    #[serde(default)]
    pub input: Vec<TurnInputItem>,
    pub prompt: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnSteerResult {
    pub turn_id: TurnId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnInterruptParams {
    pub thread_id: ThreadId,
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnInterruptResult {
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelListResult {
    pub models: Vec<Model>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Model {
    pub id: String,
    pub name: String,
    pub model_provider: String,
    pub default_reasoning_effort: Option<String>,
    pub reasoning_efforts: Vec<String>,
    pub is_default: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryListParams {
    pub scope: Option<MemoryScope>,
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryListResult {
    pub memories: Vec<MemoryRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryReadParams {
    pub memory_id: MemoryId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryReadResult {
    pub memory: Option<MemoryRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemorySaveParams {
    pub scope: MemoryScope,
    pub text: String,
    #[serde(default)]
    pub metadata: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemorySaveResult {
    pub memory_id: MemoryId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryUpdateParams {
    pub memory_id: MemoryId,
    pub text: String,
    #[serde(default)]
    pub metadata: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryDeleteParams {
    pub memory_id: MemoryId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryDeleteResult {
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryQueryParams {
    pub scope: Option<MemoryScope>,
    pub text: String,
    pub limit: Option<usize>,
    #[serde(default)]
    pub include_global: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryQueryResult {
    pub results: Vec<MemorySearchResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryProviderListResult {
    pub providers: Vec<roder_api::embeddings::EmbeddingProviderDescriptor>,
    pub selected: MemoryProviderSelection,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryProviderSetParams {
    pub provider_id: String,
    pub model: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryRecallPreviewParams {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub scope: Option<MemoryScope>,
    pub text: String,
    pub limit: Option<usize>,
    #[serde(default)]
    pub include_global: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryRecallPreviewResult {
    pub citations: Vec<roder_api::memory::MemoryCitation>,
    pub results: Vec<MemorySearchResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeListParams {
    pub scope: Option<MemoryScope>,
    #[serde(default)]
    pub kind: Option<KnowledgeKind>,
    #[serde(default)]
    pub tag: Option<String>,
    #[serde(default)]
    pub status: Option<KnowledgeStatus>,
    #[serde(default)]
    pub include_archived: bool,
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeListResult {
    pub documents: Vec<KnowledgeDocSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeReadParams {
    pub doc_id: KnowledgeDocId,
    #[serde(default)]
    pub revision: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeReadResult {
    pub document: Option<KnowledgeDocument>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeSaveParams {
    pub scope: MemoryScope,
    pub kind: KnowledgeKind,
    pub title: String,
    #[serde(default)]
    pub tags: Vec<String>,
    pub body: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeSaveResult {
    pub document: KnowledgeDocument,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeUpdateParams {
    pub doc_id: KnowledgeDocId,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub body: Option<String>,
    #[serde(default)]
    pub status: Option<KnowledgeStatus>,
    #[serde(default)]
    pub tags: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeDeleteParams {
    pub doc_id: KnowledgeDocId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeDeleteResult {
    pub archived: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeSearchParams {
    pub scope: Option<MemoryScope>,
    pub text: String,
    #[serde(default)]
    pub kind: Option<KnowledgeKind>,
    pub limit: Option<usize>,
    #[serde(default)]
    pub include_global: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeSearchResults {
    pub results: Vec<KnowledgeSearchMatch>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeLinkSetParams {
    pub from: KnowledgeDocId,
    pub to: KnowledgeDocId,
    #[serde(rename = "type")]
    pub link_type: KnowledgeLinkType,
    #[serde(default)]
    pub remove: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeRevisionsParams {
    pub doc_id: KnowledgeDocId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KnowledgeRevisionsResult {
    pub revisions: Vec<KnowledgeRevisionInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStartedNotification {
    pub thread: Thread,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStatusChangedNotification {
    pub thread_id: ThreadId,
    pub status: ThreadStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalUpdatedNotification {
    pub thread_id: ThreadId,
    pub goal: ThreadGoal,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadGoalClearedNotification {
    pub thread_id: ThreadId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnStartedNotification {
    pub thread_id: ThreadId,
    pub turn: Turn,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnCompletedNotification {
    pub thread_id: ThreadId,
    pub turn: Turn,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnDeadlineExceededNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub deadline: time::OffsetDateTime,
    pub partial_result: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TurnPartialResultNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub summary: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalRequestedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub approval_id: String,
    pub tool_id: String,
    pub tool_name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApprovalResolvedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub approval_id: String,
    pub tool_id: String,
    pub tool_name: String,
    pub approved: bool,
}

/// Model-issued call to a host-executed external tool, embedded in `thread/toolExecutionRequested`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExternalToolCall {
    pub id: String,
    pub name: String,
    pub arguments: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolExecutionRequestedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub call: ExternalToolCall,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolExecutionResolvedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub tool_id: String,
    pub tool_name: String,
    /// "resolved", "timedOut", or "cancelled".
    pub outcome: roder_api::events::ExternalToolCallOutcome,
    pub is_error: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserInputRequestedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub questions: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserInputResolvedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub answers: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationRequiredNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub reason: String,
    pub changed_files: Vec<String>,
    pub tool_evidence: Vec<String>,
    pub tests_run: Vec<String>,
    pub open_gaps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationCompletedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub passed: bool,
    pub changed_files: Vec<String>,
    pub tool_evidence: Vec<String>,
    pub tests_run: Vec<String>,
    pub open_gaps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationSkippedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationRunNotification {
    pub run: AutomationRunSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationRunFailedNotification {
    pub run: AutomationRunSummary,
    pub error: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationRunSkippedNotification {
    pub run: AutomationRunSummary,
    pub reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanExitRequestedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub target_mode: roder_api::policy_mode::PolicyMode,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plan_summary: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub next_steps: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanExitResolvedNotification {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub approved: bool,
    pub target_mode: roder_api::policy_mode::PolicyMode,
    pub resolved_mode: roder_api::policy_mode::PolicyMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsReadFileParams {
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsReadFileResponse {
    pub data_base64: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsReadDirectoryParams {
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsReadDirectoryEntry {
    pub file_name: String,
    pub is_directory: bool,
    pub is_file: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FsReadDirectoryResponse {
    pub entries: Vec<FsReadDirectoryEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesStatusParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkspaceFilesIndexState {
    Missing,
    Building,
    Ready,
    /// Reserved: the index is built but known to be out of date. The server
    /// does not emit this yet (watcher-backed staleness is future work); clients
    /// should treat it like `Ready` and may offer a manual rebuild.
    Stale,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesRootStatus {
    pub root_id: String,
    pub root_name: String,
    pub state: WorkspaceFilesIndexState,
    pub stale: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub file_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub directory_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub build_time_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub indexed_at_ms: Option<i64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesStatus {
    pub workspace_id: String,
    pub state: WorkspaceFilesIndexState,
    pub stale: bool,
    pub roots: Vec<WorkspaceFilesRootStatus>,
    pub file_count: u64,
    pub directory_count: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesStatusResult {
    pub status: WorkspaceFilesStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesRebuildParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesRebuildResult {
    pub status: WorkspaceFilesStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesChildrenParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkspaceFileKind {
    Directory,
    File,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFileEntry {
    pub root_id: String,
    pub root_name: String,
    pub path: String,
    pub name: String,
    pub kind: WorkspaceFileKind,
    pub has_children: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub size: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub modified_ms: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesChildrenResult {
    pub status: WorkspaceFilesStatus,
    pub entries: Vec<WorkspaceFileEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesQueryParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    pub query: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFileQueryMatch {
    pub entry: WorkspaceFileEntry,
    pub score: i64,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub match_positions: Vec<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesQueryResult {
    pub status: WorkspaceFilesStatus,
    pub matches: Vec<WorkspaceFileQueryMatch>,
    pub indexed_file_count: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesReadParams {
    pub workspace_id: String,
    pub root_id: String,
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub offset: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum WorkspaceFilesReadEncoding {
    Utf8,
    Binary,
    Unsupported,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesReadResult {
    pub entry: WorkspaceFileEntry,
    pub encoding: WorkspaceFilesReadEncoding,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub text: Option<String>,
    pub offset: usize,
    pub limit: usize,
    pub total_bytes: u64,
    pub has_more: bool,
    pub truncated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFilesStatusNotification {
    pub status: WorkspaceFilesStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignWorkspaceParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
}

pub type DesignGetVariablesParams = DesignWorkspaceParams;
pub type DesignSnapshotLayoutParams = DesignWorkspaceParams;
pub type DesignGetGuidelinesParams = DesignWorkspaceParams;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSetSelectionParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub selected_node_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSetVariablesParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    pub variables: BTreeMap<String, serde_json::Value>,
    #[serde(default)]
    pub replace: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSpawnAgentsParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub scope_node_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    #[serde(default)]
    pub allow_patch: bool,
    #[serde(default)]
    pub allow_export: bool,
    #[serde(default)]
    pub require_review: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignGetEditorStateParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default)]
    pub include_schema: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoderDesignDocument {
    pub version: String,
    pub document_id: String,
    pub title: String,
    pub created_at: String,
    pub updated_at: String,
    #[serde(default)]
    pub nodes: BTreeMap<String, RoderDesignNode>,
    #[serde(default)]
    pub root_ids: Vec<String>,
    #[serde(default)]
    pub variables: BTreeMap<String, serde_json::Value>,
    #[serde(default)]
    pub assets: BTreeMap<String, serde_json::Value>,
    pub metadata: RoderDesignMetadata,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoderDesignMetadata {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_root: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub selected_node_ids: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RoderDesignNode {
    pub id: String,
    #[serde(rename = "type")]
    pub node_type: String,
    pub name: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub child_ids: Vec<String>,
    #[serde(default)]
    pub x: f64,
    #[serde(default)]
    pub y: f64,
    #[serde(default)]
    pub width: f64,
    #[serde(default)]
    pub height: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rotation: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub opacity: Option<f64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub visible: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub locked: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fill: Option<serde_json::Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stroke: Option<serde_json::Value>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty", flatten)]
    pub extra: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignDocumentResult {
    pub path: String,
    pub document: RoderDesignDocument,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignNodeAlias {
    pub alias: String,
    pub node_id: String,
    pub name: String,
    #[serde(rename = "type")]
    pub node_type: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignEditorStateResult {
    pub path: String,
    pub document: RoderDesignDocument,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub selected_node_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub node_aliases: Vec<DesignNodeAlias>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub schema: Option<serde_json::Value>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub rules: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignBatchGetParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub node_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub patterns: Vec<DesignNodeSearchPattern>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub read_depth: Option<u32>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub search_depth: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignNodeSearchPattern {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    #[serde(rename = "type")]
    pub node_type: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignBatchGetResult {
    pub path: String,
    pub nodes: Vec<RoderDesignNode>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub node_aliases: Vec<DesignNodeAlias>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignPatchParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    pub operations: Vec<DesignPatchOperation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "op")]
pub enum DesignPatchOperation {
    InsertNode {
        #[serde(default, alias = "parentId", skip_serializing_if = "Option::is_none")]
        parent_id: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        index: Option<usize>,
        node: RoderDesignNode,
    },
    UpdateNode {
        #[serde(alias = "nodeId")]
        node_id: String,
        patch: serde_json::Value,
    },
    DeleteNode {
        #[serde(alias = "nodeId")]
        node_id: String,
        #[serde(default)]
        recursive: bool,
    },
    ReorderNode {
        #[serde(alias = "nodeId")]
        node_id: String,
        index: usize,
    },
    SetVariables {
        variables: BTreeMap<String, serde_json::Value>,
        #[serde(default)]
        replace: bool,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignPatchResult {
    pub path: String,
    pub document: RoderDesignDocument,
    pub applied: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignVariablesResult {
    pub path: String,
    pub variables: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignLayoutNode {
    pub id: String,
    #[serde(rename = "type")]
    pub node_type: String,
    pub name: String,
    pub x: f64,
    pub y: f64,
    pub width: f64,
    pub height: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub child_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub problems: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSnapshotLayoutResult {
    pub path: String,
    pub nodes: Vec<DesignLayoutNode>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignGuidelineCategory {
    pub name: String,
    pub description: String,
    pub guidelines: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignGuidelinesResult {
    pub categories: Vec<DesignGuidelineCategory>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSpawnedAgentScope {
    pub alias: String,
    pub scope_node_id: String,
    pub scope_name: String,
    #[serde(rename = "type")]
    pub node_type: String,
    pub child_count: usize,
    pub prompt: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignSpawnAgentsResult {
    pub path: String,
    pub planned: Vec<DesignSpawnedAgentScope>,
    pub allow_patch: bool,
    pub allow_export: bool,
    pub require_review: bool,
    pub instructions: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignExportNodesParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    pub node_ids: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output_dir: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub format: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignGetScreenshotParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub node_id: Option<String>,
    #[serde(default)]
    pub format: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignExportedNode {
    pub node_id: String,
    pub path: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignExportNodesResult {
    pub exported: Vec<DesignExportedNode>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DesignScreenshotResult {
    pub path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub node_id: Option<String>,
    pub mime_type: String,
    pub data_url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandExecParams {
    pub command: Vec<String>,
    pub process_id: Option<String>,
    #[serde(default)]
    pub tty: bool,
    #[serde(default)]
    pub stream_stdin: bool,
    #[serde(default)]
    pub stream_stdout_stderr: bool,
    pub output_bytes_cap: Option<usize>,
    #[serde(default)]
    pub disable_output_cap: bool,
    #[serde(default)]
    pub disable_timeout: bool,
    pub timeout_ms: Option<u64>,
    pub cwd: Option<String>,
    pub env: Option<HashMap<String, Option<String>>>,
    #[serde(default)]
    pub size: Option<serde_json::Value>,
    #[serde(default)]
    pub sandbox_policy: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandExecResponse {
    pub exit_code: i32,
    pub stdout: String,
    pub stderr: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stdout_artifact: Option<ContextArtifactDescriptor>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stderr_artifact: Option<ContextArtifactDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandExecOutputDeltaNotification {
    pub process_id: String,
    pub stream: String,
    pub delta_base64: String,
    pub cap_reached: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamDescriptor {
    pub id: TeamId,
    pub lead_thread_id: ThreadId,
    pub display_mode: AgentTeamDisplayMode,
    pub members: Vec<TeamMemberDescriptor>,
    pub tasks: Vec<TeamTaskDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamStartMemberParams {
    pub name: String,
    pub model_provider: Option<String>,
    pub model: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamStartParams {
    pub lead_thread_id: Option<ThreadId>,
    pub display_mode: Option<AgentTeamDisplayMode>,
    #[serde(default)]
    pub members: Vec<TeamStartMemberParams>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamStartResult {
    pub team: TeamDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamListParams {
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamListResult {
    pub data: Vec<TeamDescriptor>,
    pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamReadParams {
    pub team_id: TeamId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamReadResult {
    pub team: Option<TeamDescriptor>,
    pub messages: Vec<TeamMailboxMessage>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberStartParams {
    pub team_id: TeamId,
    pub name: String,
    pub model_provider: Option<String>,
    pub model: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberStartResult {
    pub member: TeamMemberDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberMessageParams {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
    pub text: String,
    pub expected_turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberMessageResult {
    pub turn_id: TurnId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberInterruptParams {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberInterruptResult {
    pub interrupted: bool,
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberFocusParams {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberFocusResult {
    pub focused_member_id: TeamMemberId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamCleanupParams {
    pub team_id: TeamId,
    #[serde(default)]
    pub force: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamCleanupResult {
    pub cleaned: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamStartedNotification {
    pub team: TeamDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberStartedNotification {
    pub team_id: TeamId,
    pub member: TeamMemberDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberStatusChangedNotification {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
    pub status: TeamMemberStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberMessageDeltaNotification {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
    pub turn_id: TurnId,
    pub delta: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamMemberCompletedNotification {
    pub team_id: TeamId,
    pub member_id: TeamMemberId,
    pub turn_id: Option<TurnId>,
    pub status: TeamMemberStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamDisplayModeChangedNotification {
    pub team_id: TeamId,
    pub display_mode: AgentTeamDisplayMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamTaskChangedNotification {
    pub team_id: TeamId,
    pub task: TeamTaskDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TeamCleanupCompletedNotification {
    pub team_id: TeamId,
    pub forced: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunnerStatus {
    pub destination_id: String,
    pub provider_id: String,
    pub state: String,
    pub session_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RunnerProviderDescriptor {
    pub provider_id: String,
    pub capabilities: roder_api::remote_runner::RunnerCapabilities,
    /// Setup guidance when the provider is installed but missing credentials
    /// (documented env-var names only; never secret values).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub setup_hint: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersListResult {
    pub active: Option<RunnerStatus>,
    pub providers: Vec<RunnerProviderDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersSelectParams {
    pub destination_id: String,
    pub provider_id: Option<String>,
    #[serde(default)]
    pub config: serde_json::Value,
    #[serde(default)]
    pub manifest: roder_api::remote_runner::RunnerManifest,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersSelectResult {
    pub active: Option<RunnerStatus>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersSessionResult {
    pub active: Option<RunnerStatus>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersSnapshotResult {
    pub snapshot: Option<roder_api::remote_runner::RunnerSnapshotRef>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersDeleteResult {
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnersPortsResult {
    pub ports: Vec<roder_api::remote_runner::RunnerPortResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WebSearchSettings {
    pub mode: HostedWebSearchMode,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SearchIndexSettings {
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShellSettings {
    pub shell: String,
    pub options: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum SearchIndexStatusState {
    Disabled,
    Missing,
    Building,
    Ready,
    Stale,
    Failed,
    Cleared,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexStatus {
    pub state: SearchIndexStatusState,
    pub enabled: bool,
    pub workspace: String,
    pub store_dir: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub index_version: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub document_count: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub index_bytes: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub build_time_ms: Option<u64>,
    pub stale: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexStatusParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexWarmupParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexRebuildParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexClearParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexStatusResult {
    pub status: SearchIndexStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexWarmupResult {
    pub status: SearchIndexStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexRebuildResult {
    pub status: SearchIndexStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexClearResult {
    pub status: SearchIndexStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SearchIndexStatusNotification {
    pub status: SearchIndexStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexStatusView {
    pub status: CodeIndexStatus,
    pub workspace: String,
    pub store_path: String,
    pub generation_id: Option<CodeIndexGenerationId>,
    pub root_hash: Option<String>,
    pub stale: bool,
    pub stats: CodeIndexStats,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexStatusParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexRebuildParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexSearchParams {
    pub query: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
    #[serde(default)]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexReadChunkParams {
    pub chunk_hash: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
    #[serde(default)]
    pub offset: Option<usize>,
    #[serde(default)]
    pub limit: Option<usize>,
    #[serde(default)]
    pub include_source: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexProofsListParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexStatusResult {
    pub status: CodeIndexStatusView,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexRebuildResult {
    pub status: CodeIndexStatusView,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexSearchResultEnvelope {
    pub status: CodeIndexStatusView,
    pub response: CodeIndexSearchResponse,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexChunkReadPage {
    pub chunk: CodeChunk,
    pub text: String,
    pub offset: usize,
    pub limit: usize,
    pub total_bytes: usize,
    pub has_more: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexReadChunkResult {
    pub status: CodeIndexStatusView,
    pub page: CodeIndexChunkReadPage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexProofsListResult {
    pub status: CodeIndexStatusView,
    pub proofs: Vec<ContentProof>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeIndexStatusNotification {
    pub status: CodeIndexStatusView,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtensionsListResult {
    pub extensions: Vec<ExtensionManifest>,
    #[serde(default)]
    pub capability_statuses: std::collections::BTreeMap<ExtensionId, Vec<CapabilityStatus>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderDescriptor {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub auth_type: ProviderAuthType,
    pub auth_label: Option<String>,
    pub authenticated: bool,
    pub auth_detail: Option<String>,
    pub recommended: bool,
    pub sort_order: i32,
    pub capabilities: InferenceCapabilities,
    pub models: Vec<ModelDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidersListResult {
    pub active_provider: String,
    pub active_model: String,
    pub active_reasoning: String,
    #[serde(
        default,
        rename = "selectionMode",
        skip_serializing_if = "Option::is_none"
    )]
    pub selection_mode: Option<ModelSelectionMode>,
    #[serde(default, rename = "routingOptions")]
    pub routing_options: Vec<InferenceRoutingOptionDescriptor>,
    pub providers: Vec<ProviderDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfigureParams {
    pub provider: String,
    pub api_key: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfigureResult {
    pub provider: String,
    pub authenticated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderClearParams {
    pub provider: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderClearResult {
    pub provider: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubagentTracesListParams {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubagentTracesListResult {
    pub traces: Vec<SubagentTraceSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubagentTraceReadParams {
    pub thread_id: ThreadId,
    pub trace_id: SubagentTraceId,
    #[serde(default)]
    pub offset: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubagentTraceReadResult {
    pub trace_id: SubagentTraceId,
    pub events: Vec<SubagentTraceDelta>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_offset: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewReadParams {
    pub thread_id: ThreadId,
    pub review_id: PlanReviewId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewReadResult {
    pub review: Option<PlanReview>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewCommentParams {
    pub thread_id: ThreadId,
    pub review_id: PlanReviewId,
    pub anchor: PlanCommentAnchor,
    pub body: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewCommentResult {
    pub comment: PlanComment,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewRewriteParams {
    pub thread_id: ThreadId,
    pub review_id: PlanReviewId,
    pub replacement_markdown: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewRewriteResult {
    pub rewrite: PlanRewrite,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewApproveParams {
    pub thread_id: ThreadId,
    pub review_id: PlanReviewId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewApproveResult {
    pub approved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewRejectParams {
    pub thread_id: ThreadId,
    pub review_id: PlanReviewId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanReviewRejectResult {
    pub rejected: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkListParams {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub review_id: Option<PlanReviewId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkListResult {
    pub hunks: Vec<HunkRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkReadParams {
    pub thread_id: ThreadId,
    pub hunk_id: HunkId,
    #[serde(default)]
    pub offset: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkReadResult {
    pub page: Option<PagedHunkDiff>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceChangesListParams {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceChangesListResult {
    pub changes: Vec<WorkspaceChangeObservation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkRollbackParams {
    pub thread_id: ThreadId,
    pub hunk_id: HunkId,
    #[serde(default)]
    pub confirmed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HunkRollbackResult {
    pub rolled_back: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsWorkspaceParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsChangesListParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsChangesReadParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub path: String,
    #[serde(default)]
    pub offset: usize,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub area: Option<VcsChangeArea>,
    #[serde(default)]
    pub ignore_whitespace: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsChangesTotals {
    pub files: u32,
    pub additions: u32,
    pub deletions: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsChangesListResult {
    pub status: roder_api::version_control::VcsStatus,
    pub files: Vec<roder_api::version_control::VcsChangedFile>,
    pub totals: VcsChangesTotals,
    pub truncated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsSelectionParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub paths: Vec<String>,
    pub granularity: roder_api::version_control::VcsSelectionGranularity,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsSnapshotCreateParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub message: String,
    #[serde(default)]
    pub paths: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsRestoreParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub paths: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsLineSwitchParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub line_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VcsSyncParams {
    pub workspace_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider_id: Option<String>,
    pub operation: roder_api::version_control::VcsSyncOperation,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaListParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<roder_api::media::MediaKind>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaListResult {
    pub artifacts: Vec<MediaArtifact>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactListParams {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<ContextArtifactKind>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactListResult {
    pub artifacts: Vec<ContextArtifactDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactReadParams {
    pub thread_id: ThreadId,
    pub artifact_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub start_line: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactReadResult {
    pub page: ArtifactReadPage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactGrepParams {
    pub thread_id: ThreadId,
    pub artifact_id: String,
    pub query: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub offset: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactGrepResult {
    pub page: ArtifactGrepPage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactTailParams {
    pub thread_id: ThreadId,
    pub artifact_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lines: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactTailResult {
    pub page: ArtifactTailPage,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactDeleteParams {
    pub thread_id: ThreadId,
    pub artifact_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ArtifactDeleteResult {
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryGroupsParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub refresh: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryGroupsResult {
    pub catalog_id: String,
    pub title: String,
    pub hidden_item_count: u64,
    pub groups: Vec<DiscoveryCatalogGroup>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverySearchParams {
    pub query: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub refresh: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoverySearchResult {
    pub query: String,
    pub items: Vec<DiscoveryCatalogItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryReadParams {
    pub item_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub refresh: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub start_line: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub promote: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryReadPage {
    pub text: String,
    pub start_line: usize,
    pub end_line: usize,
    pub total_lines: usize,
    pub truncated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryReadResult {
    pub item: DiscoveryCatalogItem,
    pub page: DiscoveryReadPage,
    pub promoted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryRefreshResult {
    pub catalog: DiscoveryCatalog,
    pub catalog_root: String,
    pub promotion_state_dir: String,
    pub written_files: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromoteParams {
    pub item_id: String,
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromoteResult {
    pub record: DiscoveryPromotionRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromotedListParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromotedListResult {
    pub records: Vec<DiscoveryPromotionRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromotedClearParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub item_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryPromotedClearResult {
    pub cleared: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalTurnParams {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalDebugSummary {
    pub text: String,
    #[serde(default)]
    pub notes: Vec<String>,
    pub truncated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalRecommendationsResult {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub plans: Vec<RetrievalRoutePlan>,
    pub summary: RetrievalDebugSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalMetricsResult {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub outcomes: Vec<RetrievalMeasuredOutcome>,
    pub accepted_count: u64,
    pub ignored_count: u64,
    pub failed_count: u64,
    pub outcome_counts: BTreeMap<String, u64>,
    pub mode_counts: BTreeMap<RetrievalMode, u64>,
    pub summary: RetrievalDebugSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingMetricsParams {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingStatusParams {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingDecisionEvent {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    #[serde(default)]
    pub round_index: u32,
    pub default_selection: ModelSelection,
    pub selected_selection: ModelSelection,
    pub decision: InferenceRoutingDecision,
    #[serde(with = "time::serde::rfc3339")]
    pub timestamp: OffsetDateTime,
}

impl From<roder_api::events::InferenceRoutingDecisionEvent> for InferenceRoutingDecisionEvent {
    fn from(event: roder_api::events::InferenceRoutingDecisionEvent) -> Self {
        Self {
            thread_id: event.thread_id,
            turn_id: event.turn_id,
            round_index: event.round_index,
            default_selection: event.default_selection,
            selected_selection: event.selected_selection,
            decision: event.decision,
            timestamp: event.timestamp,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingStatusResult {
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
    pub active: bool,
    pub decision_count: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub router_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub latest_outcome: Option<InferenceRoutingOutcome>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub default_selection: Option<ModelSelection>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub selected_selection: Option<ModelSelection>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub latest_decision: Option<InferenceRoutingDecisionEvent>,
    pub summary: RetrievalDebugSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingCostSummary {
    pub selected_estimated_cost_usd: f64,
    pub baseline_estimated_cost_usd: f64,
    pub estimated_savings_usd: f64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub classifier_overhead_usd: Option<f64>,
    pub incomplete_estimate_count: u64,
    pub priced_decision_count: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingRegretSummary {
    pub retry_count: u64,
    pub failure_count: u64,
    pub turn_failed: bool,
    pub escalation_count: u64,
    pub fallback_count: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceRoutingMetricsResult {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub decisions: Vec<InferenceRoutingDecisionEvent>,
    pub decision_count: u64,
    pub outcome_counts: BTreeMap<String, u64>,
    pub cost: InferenceRoutingCostSummary,
    pub regret: InferenceRoutingRegretSummary,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub cost_deltas: Vec<InferenceRoutingCostDelta>,
    pub summary: RetrievalDebugSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalPromotedCapabilityState {
    pub item_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub route_id: Option<String>,
    pub state: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cache_status: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
    pub thread_id: ThreadId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub turn_id: Option<TurnId>,
    #[serde(with = "time::serde::rfc3339")]
    pub timestamp: OffsetDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievalPromotedResult {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub states: Vec<RetrievalPromotedCapabilityState>,
    pub summary: RetrievalDebugSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaReadParams {
    pub artifact_id: MediaArtifactId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub max_bytes: Option<u64>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaReadResult {
    pub artifact: MediaArtifact,
    pub bytes_base64: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaThumbnailParams {
    pub artifact_id: MediaArtifactId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaThumbnailResult {
    pub preview: MediaPreview,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaDeleteParams {
    pub artifact_id: MediaArtifactId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaDeleteResult {
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaAttachToTurnParams {
    pub artifact_id: MediaArtifactId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaAttachToTurnResult {
    pub attachment: MediaAttachment,
    pub image: Option<InputImage>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaImageProvidersListParams {}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaImageProvidersListResult {
    pub default_provider: String,
    pub providers: Vec<roder_api::media::MediaProviderDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaImageGenerateParams {
    /// Canonical provider-neutral image generation request.
    #[serde(flatten)]
    pub request: roder_api::media::MediaGenerationRequest,
    /// Optional thread to associate emitted media events with.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MediaImageGenerateResult {
    pub response: roder_api::media::MediaGenerationResponse,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowScanParams {
    pub workspace: Option<String>,
    #[serde(default)]
    pub include_user: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowScanResult {
    pub scan: WorkflowImportScan,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowPreviewParams {
    pub workspace: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub item_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowPreviewResult {
    pub items: Vec<WorkflowImportItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowEnableParams {
    pub workspace: Option<String>,
    pub item_id: String,
    #[serde(default)]
    pub approve_side_effects: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowEnableResult {
    pub item: WorkflowImportItem,
    pub decision: WorkflowImportDecision,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowIgnoreParams {
    pub workspace: Option<String>,
    pub item_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowIgnoreResult {
    pub item_id: String,
    pub decision: WorkflowImportDecision,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowRefreshParams {
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowRefreshResult {
    pub scan: WorkflowImportScan,
    pub stale: Vec<WorkflowImportItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowRemoveParams {
    pub workspace: Option<String>,
    pub item_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowRemoveResult {
    pub item_id: String,
    pub state: WorkflowImportState,
    pub decision: WorkflowImportDecision,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReportsListParams {
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReliabilitySummary {
    #[serde(default)]
    pub error_class_counts: BTreeMap<String, u64>,
    pub retry_attempts: u64,
    pub retry_recoveries: u64,
    pub failure_limit_stops: u64,
    pub unknown_errors: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReportSummary {
    pub id: String,
    pub suite_id: String,
    pub fixture_count: usize,
    pub passed: usize,
    pub failed: usize,
    #[serde(default)]
    pub reliability: EvalReliabilitySummary,
    #[serde(with = "time::serde::rfc3339")]
    pub generated_at: time::OffsetDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReportsListResult {
    pub reports: Vec<EvalReportSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReportReadParams {
    pub report_id: String,
    pub max_bytes: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EvalReportReadResult {
    pub summary: EvalReportSummary,
    pub markdown: String,
    pub truncated: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesListResult {
    pub marketplaces: Vec<MarketplaceDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesInstallDefaultParams {
    pub selection: DefaultMarketplaceSelection,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesInstallDefaultResult {
    pub marketplaces: Vec<MarketplaceDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesAddParams {
    pub id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub kind: Option<MarketplaceKind>,
    pub display_name: String,
    pub source: MarketplaceSource,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesAddResult {
    pub marketplace: MarketplaceDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesRemoveParams {
    pub marketplace_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesRemoveResult {
    pub removed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesRefreshParams {
    pub marketplace_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesRefreshResult {
    pub marketplace: MarketplaceDescriptor,
    pub plugins: Vec<MarketplacePluginEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesSearchParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub query: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacesSearchResult {
    pub plugins: Vec<DedupedMarketplacePlugin>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacePluginParams {
    pub marketplace_id: String,
    pub plugin_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MarketplacePluginResult {
    pub plugin: Option<MarketplacePluginEntry>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginPreviewInstallParams {
    pub marketplace_id: String,
    pub plugin_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginPreviewInstallResult {
    pub preview: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginInstallParams {
    pub marketplace_id: String,
    pub plugin_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginInstallResult {
    pub plugin: InstalledPluginRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginInstallAllVariantsParams {
    pub marketplace_id: String,
    pub plugin_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginInstallAllVariantsResult {
    pub plugins: Vec<InstalledPluginRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginListInstalledResult {
    pub plugins: Vec<InstalledPluginRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginDisableParams {
    pub variant_key: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginDisableResult {
    pub plugin: Option<InstalledPluginRecord>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginUninstallParams {
    pub variant_key: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginUninstallResult {
    pub removed: bool,
}

/// One resource of an installed package, with its registry-facing id
/// (`<package-id>:<kind>/<name>`).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageResourceDescriptor {
    #[serde(flatten)]
    pub resource: PackageResource,
    pub id: String,
}

impl From<PackageResource> for PackageResourceDescriptor {
    fn from(resource: PackageResource) -> Self {
        let id = resource.id();
        Self { resource, id }
    }
}

/// Installed package record plus its enumerated resources.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageDescriptor {
    #[serde(flatten)]
    pub record: PackageRecord,
    pub shadowed_by_project: bool,
    pub resources: Vec<PackageResourceDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesListResult {
    pub packages: Vec<PackageDescriptor>,
    pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesInstallParams {
    pub spec: String,
    pub scope: PackageScope,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub allow_scripts: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesInstallResult {
    pub package: PackageDescriptor,
    pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesRemoveParams {
    pub spec_or_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scope: Option<PackageScope>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesRemoveResult {
    pub removed: PackageRecord,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesUpdateParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PackageUpdateStatus {
    Updated,
    SkippedPinned,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageUpdateOutcome {
    pub package_id: String,
    pub identity: String,
    pub scope: PackageScope,
    pub status: PackageUpdateStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resolved: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesUpdateResult {
    pub outcomes: Vec<PackageUpdateOutcome>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PackageSyncStatus {
    Materialized,
    AlreadyPresent,
    Failed,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageSyncOutcome {
    pub package_id: String,
    pub identity: String,
    pub status: PackageSyncStatus,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resolved: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesSyncResult {
    pub outcomes: Vec<PackageSyncOutcome>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesSetEnabledParams {
    /// Package id or resource id (`<package-id>:<kind>/<name>`).
    pub id: String,
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesSetEnabledResult {
    pub package: PackageDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesApproveExtensionsParams {
    pub package_id: String,
    pub approved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesApproveExtensionsResult {
    pub package: PackageDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesSetFiltersParams {
    pub package_id: String,
    pub filters: PackageResourceFilters,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PackagesSetFiltersResult {
    pub package: PackageDescriptor,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderSelectParams {
    pub provider: String,
    pub model: Option<String>,
    pub reasoning: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProviderSelectResult {
    pub provider: String,
    pub model: String,
    pub reasoning: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_profile: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_switch_summary: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ModelSelectChoice {
    Manual {
        provider: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        model: Option<String>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        reasoning: Option<String>,
    },
    Auto {
        option_id: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSelectParams {
    pub selection: ModelSelectChoice,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_id: Option<ThreadId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelSelectResult {
    pub selection_mode: ModelSelectionMode,
    pub provider: String,
    pub model: String,
    pub reasoning: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_profile: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_switch_summary: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsGetResult {
    pub web_search: WebSearchSettings,
    pub search_index: SearchIndexSettings,
    pub shell: ShellSettings,
    pub default_provider: String,
    pub default_model: String,
    pub default_reasoning: String,
    pub default_mode: PolicyMode,
    pub file_backed_dynamic_context: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetWebSearchParams {
    pub mode: HostedWebSearchMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetWebSearchResult {
    pub web_search: WebSearchSettings,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetSearchIndexParams {
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetSearchIndexResult {
    pub search_index: SearchIndexSettings,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetShellParams {
    pub shell: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetShellResult {
    pub shell: ShellSettings,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetDefaultModeParams {
    pub mode: PolicyMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetDefaultModeResult {
    pub default_mode: PolicyMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetFileBackedDynamicContextParams {
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettingsSetFileBackedDynamicContextResult {
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderAuthResult {
    pub signed_in: bool,
    pub account_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadStateResult {
    pub mode: PolicyMode,
    pub pending_plan_exit: Option<PendingPlanExitDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PendingPlanExitDescriptor {
    pub thread_id: ThreadId,
    pub turn_id: TurnId,
    pub request_id: String,
    pub target_mode: PolicyMode,
    pub plan_summary: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub next_steps: Vec<String>,
    pub requested_at: OffsetDateTime,
    pub expires_at: Option<OffsetDateTime>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadSetModeParams {
    pub mode: PolicyMode,
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadSetModeResult {
    pub mode: PolicyMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadExitPlanParams {
    pub request_id: String,
    pub approved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadExitPlanResult {
    pub resolved: bool,
    pub mode: PolicyMode,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadResolveApprovalParams {
    pub approval_id: String,
    pub approved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadResolveApprovalResult {
    pub resolved: bool,
}

/// Completes a pending host-executed tool call published via `thread/toolExecutionRequested`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsResolveParams {
    pub request_id: String,
    pub output: String,
    #[serde(default)]
    pub is_error: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsResolveResult {
    pub resolved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadResolveUserInputParams {
    pub request_id: String,
    pub answers: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThreadResolveUserInputResult {
    pub resolved: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandDescriptor {
    pub name: String,
    pub description: Option<String>,
    pub argument_hint: Option<String>,
    pub source: String,
    pub model: Option<String>,
    pub agent: Option<String>,
    pub has_shell_includes: bool,
    pub has_url_includes: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandsListResult {
    pub commands: Vec<CommandDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandsExpandParams {
    pub name: String,
    #[serde(default)]
    pub arguments: String,
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandsExpandResult {
    pub command: CommandDescriptor,
    pub message: String,
    pub context_blocks: Vec<ContextBlock>,
    pub allowed_tools: Vec<String>,
    pub model: Option<String>,
    pub agent: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandsRunParams {
    pub thread_id: ThreadId,
    pub name: String,
    #[serde(default)]
    pub arguments: String,
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommandsRunResult {
    pub turn_id: TurnId,
    pub expanded: CommandsExpandResult,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsListParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsListResult {
    pub skills: Vec<SkillDescriptor>,
    #[serde(default)]
    pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsReadParams {
    pub selector: SkillSelector,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsReadResult {
    pub skill: Option<Skill>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsSetEnabledParams {
    pub selector: SkillSelector,
    pub enabled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsSetExposureParams {
    pub selector: SkillSelector,
    pub exposure: SkillExposure,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SkillsUpdateResult {
    pub skills: Vec<SkillDescriptor>,
    #[serde(default)]
    pub diagnostics: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsListParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project_cwd: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub include_disabled: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsListResult {
    pub automations: Vec<AutomationDefinition>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsCreateParams {
    pub name: String,
    pub project: AutomationProject,
    pub schedule: AutomationSchedule,
    pub prompt: String,
    #[serde(default = "default_true_bool")]
    pub enabled: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_provider: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_mode: Option<roder_api::policy_mode::PolicyMode>,
    pub catch_up: CatchUpPolicy,
    pub concurrency: AutomationConcurrencyPolicy,
}

fn default_true_bool() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsCreateResult {
    pub automation: AutomationDefinition,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsUpdatePatch {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub project: Option<AutomationProject>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub schedule: Option<AutomationSchedule>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enabled: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_provider: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub policy_mode: Option<roder_api::policy_mode::PolicyMode>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub catch_up: Option<CatchUpPolicy>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub concurrency: Option<AutomationConcurrencyPolicy>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsUpdateParams {
    pub automation_id: AutomationId,
    pub patch: AutomationsUpdatePatch,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsUpdateResult {
    pub automation: AutomationDefinition,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsDeleteParams {
    pub automation_id: AutomationId,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsDeleteResult {
    pub automation_id: AutomationId,
    pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsRunNowParams {
    pub automation_id: AutomationId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt_override: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsRunNowResult {
    pub run: AutomationRunSummary,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsRunsParams {
    pub automation_id: AutomationId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<AutomationRunState>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsRunsResult {
    pub runs: Vec<AutomationRunSummary>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_cursor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsCancelRunParams {
    pub run_id: AutomationRunId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsCancelRunResult {
    pub run_id: AutomationRunId,
    pub cancelled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AutomationsStatusResult {
    pub scheduler_enabled: bool,
    pub read_api_enabled: bool,
    pub server_id: String,
    pub server_role: String,
    pub store_path: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_tick_at: Option<OffsetDateTime>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_tick_at: Option<OffsetDateTime>,
    #[serde(default)]
    pub active_runs: usize,
    #[serde(default)]
    pub due_count: usize,
    #[serde(default)]
    pub leased_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsListResult {
    pub tools: Vec<ToolSpec>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallParams {
    pub thread_id: ThreadId,
    pub tool_name: String,
    pub arguments: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolCallResult {
    pub text: String,
    pub data: serde_json::Value,
    pub is_error: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentsListResult {
    pub agents: Vec<AgentDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksSubmitParams {
    pub executor_id: String,
    #[serde(default)]
    pub input: serde_json::Value,
    pub thread_id: Option<ThreadId>,
    pub turn_id: Option<TurnId>,
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksSubmitResult {
    pub task: TaskHandle,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksListResult {
    pub tasks: Vec<TaskHandle>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksGetParams {
    pub task_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskLogDescriptor {
    pub stream: TaskOutputStream,
    pub chunk: String,
    pub timestamp: OffsetDateTime,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksGetResult {
    pub task: TaskHandle,
    pub logs: Vec<TaskLogDescriptor>,
    pub dropped_bytes: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksCancelParams {
    pub task_id: String,
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksCancelResult {
    pub cancelled: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TasksSubscribeResult {
    pub subscribed: bool,
    pub event_kinds: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightPrepareParams {
    pub task: String,
    #[serde(default)]
    pub mode: Option<String>,
    #[serde(default)]
    pub start_url: Option<String>,
    #[serde(default)]
    pub task_id: Option<String>,
    #[serde(default)]
    pub browser: Option<String>,
    #[serde(default)]
    pub headless: Option<bool>,
    #[serde(default)]
    pub output_dir: Option<String>,
    #[serde(default)]
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightWorkspaceParams {
    pub workspace: String,
    #[serde(default)]
    pub workspace_root: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightSubmitParams {
    pub task: String,
    #[serde(default)]
    pub mode: Option<String>,
    #[serde(default)]
    pub start_url: Option<String>,
    #[serde(default)]
    pub task_id: Option<String>,
    #[serde(default)]
    pub browser: Option<String>,
    #[serde(default)]
    pub headless: Option<bool>,
    #[serde(default)]
    pub output_dir: Option<String>,
    #[serde(default)]
    pub timeout_seconds: Option<u64>,
    #[serde(default)]
    pub thread_id: Option<ThreadId>,
    #[serde(default)]
    pub turn_id: Option<TurnId>,
    #[serde(default)]
    pub workspace: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightSetupParams {
    #[serde(default)]
    pub python: Option<String>,
    #[serde(default)]
    pub browser: Option<String>,
    #[serde(default)]
    pub dry_run: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightRerunParams {
    pub workspace: String,
    #[serde(default)]
    pub workspace_root: Option<String>,
    #[serde(default)]
    pub python: Option<String>,
    #[serde(default)]
    pub thread_id: Option<ThreadId>,
    #[serde(default)]
    pub turn_id: Option<TurnId>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightExportParams {
    pub workspace: String,
    #[serde(default)]
    pub workspace_root: Option<String>,
    pub output_dir: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightVisualJudgeParams {
    pub workspace: String,
    #[serde(default)]
    pub workspace_root: Option<String>,
    #[serde(default)]
    pub run_id: Option<u32>,
    #[serde(default)]
    pub enabled: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightPrepareResult {
    pub task_id: String,
    pub workspace: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightArtifactsResult {
    pub workspace: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightLatestRunResult {
    pub latest_run: Option<u32>,
    pub run: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightReportResult {
    pub task_definition: Option<serde_json::Value>,
    pub report: Option<serde_json::Value>,
    pub rendered_text: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightVerifyResult {
    pub verification: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightSubmitResult {
    pub task: TaskHandle,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightSetupStepResult {
    pub label: String,
    pub command: Vec<String>,
    pub status: String,
    pub stdout_tail: String,
    pub stderr_tail: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightSetupResult {
    pub roder_home: String,
    pub runtime_dir: String,
    pub python: String,
    pub browser: String,
    pub dry_run: bool,
    pub installed: bool,
    pub steps: Vec<WebwrightSetupStepResult>,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightRerunResult {
    pub task: TaskHandle,
    pub run_id: u32,
    pub run_dir: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightExportResult {
    pub export_dir: String,
    pub files: Vec<String>,
    pub excluded: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebwrightVisualJudgeResult {
    pub visual_judge: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesListParams {
    #[serde(default)]
    pub include_completed: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesListResult {
    pub processes: Vec<ProcessDescriptor>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesGetParams {
    pub process_id: ProcessId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub output_bytes: Option<usize>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesGetResult {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub process: Option<ProcessDescriptor>,
    #[serde(default)]
    pub output: Vec<ProcessOutput>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesStopParams {
    pub process_id: ProcessId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesStopResult {
    pub result: ProcessStopResult,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesStopAllParams {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesStopAllResult {
    pub results: Vec<ProcessStopResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessesSubscribeResult {
    pub subscribed: bool,
    pub event_kinds: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDescriptor {
    pub agent_type: String,
    pub description: String,
    pub tools: Vec<String>,
    pub model: Option<String>,
    pub permission_mode: SubagentPermissionMode,
    pub max_turns: Option<u32>,
    pub max_result_chars: Option<usize>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use time::OffsetDateTime;

    #[test]
    fn protocol_turn_start_params_accept_input_shape() {
        let params: TurnStartParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "input": [
                { "type": "text", "text": "hello" },
                { "type": "image", "imageUrl": "data:image/png;base64,YWJj" }
            ]
        }))
        .unwrap();

        assert_eq!(params.thread_id, "thread-1");
        assert_eq!(params.input[0].kind, "text");
        assert_eq!(params.input[0].text.as_deref(), Some("hello"));
        assert_eq!(params.input[1].kind, "image");
        assert_eq!(
            params.input[1].image_url.as_deref(),
            Some("data:image/png;base64,YWJj")
        );
        assert!(!params.task_ledger_required);
    }

    #[test]
    fn protocol_turn_start_params_accept_task_ledger_required() {
        let params: TurnStartParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "taskLedgerRequired": true
        }))
        .unwrap();

        assert!(params.task_ledger_required);
    }

    #[test]
    fn protocol_turn_start_params_round_trip_developer_context() {
        let params: TurnStartParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "developerContext": "Connected accounts: example-service."
        }))
        .unwrap();
        assert_eq!(
            params.developer_context.as_deref(),
            Some("Connected accounts: example-service.")
        );

        let encoded = serde_json::to_value(&params).unwrap();
        assert_eq!(
            encoded["developerContext"],
            "Connected accounts: example-service."
        );

        // Absent developerContext stays absent on the wire.
        let without: TurnStartParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1"
        }))
        .unwrap();
        assert!(without.developer_context.is_none());
        assert!(
            serde_json::to_value(&without)
                .unwrap()
                .get("developerContext")
                .is_none()
        );
    }

    #[test]
    fn protocol_thread_start_params_round_trip_runner_selection() {
        let params: ThreadStartParams = serde_json::from_value(serde_json::json!({
            "workspaceId": "ws-1",
            "model": null,
            "modelProvider": null,
            "reasoning": null,
            "runner": {
                "providerId": "acme",
                "config": { "space_id": "space-1", "mode": "readwrite" },
                "workspace": "/workspace"
            }
        }))
        .unwrap();

        let runner = params.runner.clone().expect("runner params");
        assert_eq!(runner.provider_id, "acme");
        assert_eq!(
            runner.config,
            Some(serde_json::json!({ "space_id": "space-1", "mode": "readwrite" }))
        );
        assert_eq!(runner.workspace, "/workspace");

        let encoded = serde_json::to_value(&params).unwrap();
        assert_eq!(encoded["runner"]["providerId"], "acme");
        assert_eq!(encoded["runner"]["workspace"], "/workspace");

        // Absent runner stays absent on the wire.
        let without: ThreadStartParams = serde_json::from_value(serde_json::json!({
            "workspaceId": "ws-1",
            "model": null,
            "modelProvider": null,
            "reasoning": null
        }))
        .unwrap();
        assert!(without.runner.is_none());
        assert!(
            serde_json::to_value(&without)
                .unwrap()
                .get("runner")
                .is_none()
        );
    }

    #[test]
    fn protocol_turn_start_params_accept_selected_controls() {
        let params: TurnStartParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "modelProvider": "mock",
            "model": "gpt-5.5",
            "reasoning": "high",
            "policyMode": "plan"
        }))
        .unwrap();

        assert_eq!(params.model_provider.as_deref(), Some("mock"));
        assert_eq!(params.model.as_deref(), Some("gpt-5.5"));
        assert_eq!(params.reasoning.as_deref(), Some("high"));
        assert_eq!(params.policy_mode, Some(PolicyMode::Plan));
    }

    #[test]
    fn protocol_notifications_serialize_with_json_rpc_method_and_camel_case_params() {
        let notification = JsonRpcNotification {
            jsonrpc: "2.0".to_string(),
            method: "item/agentMessage/delta".to_string(),
            params: serde_json::to_value(ThreadItemEvent {
                seq: 1,
                event_id: "event-1".to_string(),
                thread_id: "thread-1".to_string(),
                turn_id: "turn-1".to_string(),
                timestamp: OffsetDateTime::UNIX_EPOCH,
                event: ThreadItemEventKind::ItemDelta {
                    item_id: "turn-1-assistant".to_string(),
                    delta: ThreadItemDelta::AgentMessageText {
                        delta: "hello".to_string(),
                        phase: Some("final_answer".to_string()),
                    },
                },
            })
            .unwrap(),
        };

        let value = serde_json::to_value(notification).unwrap();
        assert_eq!(value["jsonrpc"], "2.0");
        assert_eq!(value["method"], "item/agentMessage/delta");
        assert_eq!(value["params"]["seq"], 1);
        assert_eq!(value["params"]["eventId"], "event-1");
        assert_eq!(value["params"]["threadId"], "thread-1");
        assert_eq!(value["params"]["turnId"], "turn-1");
        assert_eq!(value["params"]["event"]["type"], "itemDelta");
        assert_eq!(value["params"]["event"]["itemId"], "turn-1-assistant");
        assert_eq!(
            value["params"]["event"]["delta"]["type"],
            "agentMessageText"
        );
        assert_eq!(value["params"]["event"]["delta"]["delta"], "hello");
        assert_eq!(value["params"]["event"]["delta"]["phase"], "final_answer");
    }

    #[test]
    fn thread_item_reasoning_serializes_as_typed_public_item() {
        let value = serde_json::to_value(Item::Reasoning {
            id: "turn-1-agent-reasoning".to_string(),
            summary: vec!["Inspecting project".to_string()],
            content: vec!["I need to inspect files.".to_string()],
            status: Some(ThreadItemStatus::Completed),
        })
        .unwrap();

        assert_eq!(
            value,
            serde_json::json!({
                "type": "reasoning",
                "id": "turn-1-agent-reasoning",
                "summary": ["Inspecting project"],
                "content": ["I need to inspect files."],
                "status": "completed"
            })
        );
    }

    #[test]
    fn thread_item_routing_decision_serializes_as_typed_public_item() {
        let selected = roder_api::inference::ModelSelection {
            provider: "anthropic".to_string(),
            model: "claude-sonnet-5".to_string(),
        };
        let value = serde_json::to_value(Item::RoutingDecision {
            id: "turn-1-routing-decision-0".to_string(),
            decision: InferenceRoutingDecisionEvent {
                thread_id: "thread-1".to_string(),
                turn_id: "turn-1".to_string(),
                round_index: 0,
                default_selection: roder_api::inference::ModelSelection {
                    provider: "openai".to_string(),
                    model: "gpt-5.5".to_string(),
                },
                selected_selection: selected.clone(),
                decision: roder_api::inference_routing::InferenceRoutingDecision::selected(
                    "local",
                    selected,
                    "Large diff and failing tests",
                ),
                timestamp: OffsetDateTime::UNIX_EPOCH,
            },
            status: Some(ThreadItemStatus::Completed),
        })
        .unwrap();

        assert_eq!(value["type"], "routingDecision");
        assert_eq!(value["id"], "turn-1-routing-decision-0");
        assert_eq!(
            value["decision"]["selectedSelection"]["model"],
            "claude-sonnet-5"
        );
        assert_eq!(
            value["decision"]["decision"]["reason"],
            "Large diff and failing tests"
        );
        assert_eq!(value["status"], "completed");
    }

    #[test]
    fn reasoning_text_delta_notification_targets_reasoning_content_index() {
        let notification = JsonRpcNotification {
            jsonrpc: "2.0".to_string(),
            method: "item/reasoning/textDelta".to_string(),
            params: serde_json::to_value(ThreadItemEvent {
                seq: 1,
                event_id: "event-1".to_string(),
                thread_id: "thread-1".to_string(),
                turn_id: "turn-1".to_string(),
                timestamp: OffsetDateTime::UNIX_EPOCH,
                event: ThreadItemEventKind::ItemDelta {
                    item_id: "turn-1-agent-reasoning".to_string(),
                    delta: ThreadItemDelta::ReasoningText {
                        delta: "thinking".to_string(),
                        content_index: 0,
                    },
                },
            })
            .unwrap(),
        };

        let value = serde_json::to_value(notification).unwrap();
        assert_eq!(value["method"], "item/reasoning/textDelta");
        assert_eq!(value["params"]["threadId"], "thread-1");
        assert_eq!(value["params"]["turnId"], "turn-1");
        assert_eq!(value["params"]["event"]["itemId"], "turn-1-agent-reasoning");
        assert_eq!(value["params"]["event"]["delta"]["delta"], "thinking");
        assert_eq!(value["params"]["event"]["delta"]["contentIndex"], 0);
    }

    #[test]
    fn thread_status_serializes_required_activity_fields() {
        let idle = serde_json::to_value(ThreadStatus {
            kind: "idle".to_string(),
            active_turn_id: None,
            active_flags: Vec::new(),
        })
        .unwrap();
        assert_eq!(
            idle,
            serde_json::json!({
                "type": "idle",
                "activeTurnId": null,
                "activeFlags": []
            })
        );

        let running = serde_json::to_value(ThreadStatus {
            kind: "running".to_string(),
            active_turn_id: Some("turn-1".to_string()),
            active_flags: vec!["approvalRequired".to_string()],
        })
        .unwrap();
        assert_eq!(
            running,
            serde_json::json!({
                "type": "running",
                "activeTurnId": "turn-1",
                "activeFlags": ["approvalRequired"]
            })
        );
    }

    #[test]
    fn verification_notifications_use_camel_case_fields() {
        let value = serde_json::to_value(VerificationRequiredNotification {
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
            reason: "code_changes_without_verification".to_string(),
            changed_files: vec!["src/lib.rs".to_string()],
            tool_evidence: vec!["write_file: wrote src/lib.rs".to_string()],
            tests_run: vec!["cargo test".to_string()],
            open_gaps: Vec::new(),
        })
        .unwrap();

        assert_eq!(value["threadId"], "thread-1");
        assert_eq!(value["turnId"], "turn-1");
        assert_eq!(value["changedFiles"][0], "src/lib.rs");
        assert_eq!(value["toolEvidence"][0], "write_file: wrote src/lib.rs");
        assert_eq!(value["testsRun"][0], "cargo test");
        assert!(value.get("changed_files").is_none());
    }

    #[test]
    fn search_index_status_protocol_uses_camel_case_fields() {
        let value = serde_json::to_value(SearchIndexStatusNotification {
            status: SearchIndexStatus {
                state: SearchIndexStatusState::Ready,
                enabled: true,
                workspace: "/tmp/workspace".to_string(),
                store_dir: "/tmp/home/.roder/indexes/abc".to_string(),
                index_version: Some("fastregex-v1".to_string()),
                document_count: Some(7),
                index_bytes: Some(128),
                build_time_ms: Some(4),
                stale: false,
                message: None,
            },
        })
        .unwrap();

        assert_eq!(value["status"]["state"], "ready");
        assert_eq!(value["status"]["storeDir"], "/tmp/home/.roder/indexes/abc");
        assert_eq!(value["status"]["indexVersion"], "fastregex-v1");
        assert_eq!(value["status"]["documentCount"], 7);
        assert_eq!(value["status"]["buildTimeMs"], 4);
        assert!(value["status"].get("store_dir").is_none());
        assert!(value["status"].get("document_count").is_none());
    }

    #[test]
    fn code_index_status_protocol_uses_camel_case_fields() {
        let value = serde_json::to_value(CodeIndexStatusNotification {
            status: CodeIndexStatusView {
                status: CodeIndexStatus::Ready,
                workspace: "/tmp/workspace".to_string(),
                store_path: "/tmp/home/.roder/code-index/abc/code-index.sqlite3".to_string(),
                generation_id: Some("gen-1".to_string()),
                root_hash: Some("root-hash".to_string()),
                stale: false,
                stats: roder_api::code_index::CodeIndexStats {
                    file_count: 2,
                    chunk_count: 3,
                    embedded_chunk_count: 3,
                    cached_embedding_count: 1,
                    index_bytes: 256,
                },
                message: None,
            },
        })
        .unwrap();

        assert_eq!(value["status"]["status"], "ready");
        assert_eq!(
            value["status"]["storePath"],
            "/tmp/home/.roder/code-index/abc/code-index.sqlite3"
        );
        assert_eq!(value["status"]["generationId"], "gen-1");
        assert_eq!(value["status"]["stats"]["chunkCount"], 3);
        assert_eq!(value["status"]["stats"]["cachedEmbeddingCount"], 1);
        assert!(value["status"].get("store_path").is_none());
        assert!(value["status"]["stats"].get("chunk_count").is_none());
    }

    #[test]
    fn workspace_files_protocol_structs_use_workspace_scoped_locators() {
        let children: WorkspaceFilesChildrenParams = serde_json::from_value(serde_json::json!({
            "workspaceId": "ws-1",
            "rootId": "root-1",
            "path": "roadmap"
        }))
        .unwrap();
        assert_eq!(children.workspace_id, "ws-1");
        assert_eq!(children.root_id.as_deref(), Some("root-1"));
        assert_eq!(children.path.as_deref(), Some("roadmap"));

        let status = WorkspaceFilesStatus {
            workspace_id: "ws-1".to_string(),
            state: WorkspaceFilesIndexState::Ready,
            stale: false,
            roots: vec![WorkspaceFilesRootStatus {
                root_id: "root-1".to_string(),
                root_name: "repo".to_string(),
                state: WorkspaceFilesIndexState::Ready,
                stale: false,
                file_count: Some(1),
                directory_count: Some(1),
                build_time_ms: Some(4),
                indexed_at_ms: Some(42),
                message: None,
            }],
            file_count: 1,
            directory_count: 1,
            message: None,
        };
        let entry = WorkspaceFileEntry {
            root_id: "root-1".to_string(),
            root_name: "repo".to_string(),
            path: "roadmap/status.md".to_string(),
            name: "status.md".to_string(),
            kind: WorkspaceFileKind::File,
            has_children: false,
            size: Some(12),
            modified_ms: Some(99),
        };
        let result = WorkspaceFilesQueryResult {
            status,
            matches: vec![WorkspaceFileQueryMatch {
                entry,
                score: 120,
                match_positions: vec![0, 8],
            }],
            indexed_file_count: 1,
        };
        let value = serde_json::to_value(result).unwrap();

        assert_eq!(value["status"]["workspaceId"], "ws-1");
        assert_eq!(value["status"]["roots"][0]["rootId"], "root-1");
        assert_eq!(value["matches"][0]["entry"]["rootName"], "repo");
        assert_eq!(value["matches"][0]["entry"]["kind"], "file");
        assert_eq!(
            value["matches"][0]["matchPositions"],
            serde_json::json!([0, 8])
        );
        assert!(value["matches"][0]["entry"].get("root_id").is_none());
        assert!(value.get("indexed_file_count").is_none());
    }

    #[test]
    fn team_start_params_round_trip_camel_case_display_mode() {
        let params: TeamStartParams = serde_json::from_value(serde_json::json!({
            "leadThreadId": "lead-thread",
            "displayMode": "tmux",
            "members": [{
                "name": "reviewer",
                "modelProvider": "mock",
                "model": "mock"
            }]
        }))
        .unwrap();

        assert_eq!(params.lead_thread_id.as_deref(), Some("lead-thread"));
        assert_eq!(params.display_mode, Some(AgentTeamDisplayMode::Tmux));
        assert_eq!(params.members[0].model_provider.as_deref(), Some("mock"));

        let value = serde_json::to_value(params).unwrap();
        assert_eq!(value["leadThreadId"], "lead-thread");
        assert_eq!(value["displayMode"], "tmux");
        assert_eq!(value["members"][0]["modelProvider"], "mock");
    }

    #[test]
    fn subagent_trace_protocol_structs_use_camel_case_fields() {
        let list_params: SubagentTracesListParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "turnId": "turn-1"
        }))
        .unwrap();
        assert_eq!(list_params.thread_id, "thread-1");
        assert_eq!(list_params.turn_id, "turn-1");

        let read_params: SubagentTraceReadParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "traceId": "trace-1",
            "offset": 10,
            "limit": 20
        }))
        .unwrap();
        assert_eq!(read_params.thread_id, "thread-1");
        assert_eq!(read_params.trace_id, "trace-1");
        assert_eq!(read_params.offset, 10);
        assert_eq!(read_params.limit, Some(20));

        let result = SubagentTraceReadResult {
            trace_id: "trace-1".to_string(),
            events: Vec::new(),
            next_offset: Some(30),
        };
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["traceId"], "trace-1");
        assert_eq!(value["nextOffset"], 30);
    }

    #[test]
    fn artifacts_protocol_structs_use_camel_case_fields() {
        let params: ArtifactReadParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "artifactId": "artifact-1",
            "startLine": 2,
            "limit": 10
        }))
        .unwrap();

        assert_eq!(params.thread_id, "thread-1");
        assert_eq!(params.artifact_id, "artifact-1");
        assert_eq!(params.start_line, Some(2));

        let command = serde_json::to_value(CommandExecResponse {
            exit_code: 0,
            stdout: "short".to_string(),
            stderr: String::new(),
            stdout_artifact: None,
            stderr_artifact: None,
        })
        .unwrap();

        assert_eq!(command["exitCode"], 0);
        assert!(command.get("stdoutArtifact").is_none());
    }

    #[test]
    fn discovery_protocol_structs_use_camel_case_fields() {
        let params: DiscoveryReadParams = serde_json::from_value(serde_json::json!({
            "itemId": "tool:builtin/grep",
            "startLine": 2,
            "limit": 10,
            "threadId": "thread-1",
            "turnId": "turn-1"
        }))
        .unwrap();

        assert_eq!(params.item_id, "tool:builtin/grep");
        assert_eq!(params.start_line, Some(2));
        assert_eq!(params.thread_id.as_deref(), Some("thread-1"));

        let clear = serde_json::to_value(DiscoveryPromotedClearResult { cleared: 2 }).unwrap();
        assert_eq!(clear["cleared"], 2);
    }

    #[test]
    fn retrieval_protocol_structs_use_camel_case_fields() {
        let params: RetrievalTurnParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "turnId": "turn-1",
            "limit": 5
        }))
        .unwrap();
        assert_eq!(params.thread_id, "thread-1");
        assert_eq!(params.turn_id, "turn-1");
        assert_eq!(params.limit, Some(5));

        let result = RetrievalRecommendationsResult {
            thread_id: "thread-1".to_string(),
            turn_id: "turn-1".to_string(),
            plans: Vec::new(),
            summary: RetrievalDebugSummary {
                text: "no route recommendations recorded".to_string(),
                notes: vec!["router did not emit retrieval/routePlanned".to_string()],
                truncated: false,
            },
        };
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["threadId"], "thread-1");
        assert_eq!(value["turnId"], "turn-1");
        assert_eq!(value["summary"]["truncated"], false);

        let state = RetrievalPromotedCapabilityState {
            item_id: "tool:builtin/grep".to_string(),
            route_id: Some("route-1".to_string()),
            state: "skipped".to_string(),
            cache_status: None,
            reason: Some("already warm".to_string()),
            thread_id: "thread-1".to_string(),
            turn_id: Some("turn-1".to_string()),
            timestamp: OffsetDateTime::UNIX_EPOCH,
        };
        let value = serde_json::to_value(state).unwrap();
        assert_eq!(value["itemId"], "tool:builtin/grep");
        assert_eq!(value["routeId"], "route-1");
        assert!(value.get("cacheStatus").is_none());
    }

    #[test]
    fn plan_review_and_hunk_protocol_structs_use_camel_case_fields() {
        let comment_params: PlanReviewCommentParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "reviewId": "review-1",
            "anchor": { "step": { "stepId": "step-1" } },
            "body": "Use the smaller patch."
        }))
        .unwrap();
        assert_eq!(comment_params.thread_id, "thread-1");
        assert_eq!(comment_params.review_id, "review-1");

        let hunk_params: HunkReadParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-1",
            "hunkId": "hunk-1",
            "offset": 4,
            "limit": 20
        }))
        .unwrap();
        assert_eq!(hunk_params.hunk_id, "hunk-1");
        assert_eq!(hunk_params.limit, Some(20));

        let result = HunkRollbackResult {
            rolled_back: false,
            error: Some("checkpoint data is unavailable".to_string()),
        };
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["rolledBack"], false);
        assert_eq!(value["error"], "checkpoint data is unavailable");
    }

    #[test]
    fn workflow_import_protocol_structs_use_camel_case_fields() {
        let scan_params: WorkflowScanParams = serde_json::from_value(serde_json::json!({
            "workspace": "/tmp/repo",
            "includeUser": true
        }))
        .unwrap();
        assert_eq!(scan_params.workspace.as_deref(), Some("/tmp/repo"));
        assert!(scan_params.include_user);

        let enable_params: WorkflowEnableParams = serde_json::from_value(serde_json::json!({
            "workspace": "/tmp/repo",
            "itemId": "workflow-1",
            "approveSideEffects": true
        }))
        .unwrap();
        assert_eq!(enable_params.item_id, "workflow-1");
        assert!(enable_params.approve_side_effects);

        let remove = WorkflowRemoveResult {
            item_id: "workflow-1".to_string(),
            state: WorkflowImportState::Removed,
            decision: WorkflowImportDecision {
                item_id: "workflow-1".to_string(),
                decision: roder_api::workflow::WorkflowImportDecisionKind::Remove,
                source_hash: "hash".to_string(),
                approved_side_effects: false,
                decided_at: OffsetDateTime::UNIX_EPOCH,
            },
        };
        let value = serde_json::to_value(remove).unwrap();
        assert_eq!(value["itemId"], "workflow-1");
        assert_eq!(value["state"], "removed");
        assert_eq!(value["decision"]["sourceHash"], "hash");
    }

    #[test]
    fn marketplace_protocol_structs_use_camel_case_fields() {
        let params: MarketplacesInstallDefaultParams = serde_json::from_value(serde_json::json!({
            "selection": "all"
        }))
        .unwrap();
        assert_eq!(params.selection, DefaultMarketplaceSelection::All);

        let add: MarketplacesAddParams = serde_json::from_value(serde_json::json!({
            "id": "local-cursor",
            "kind": "cursor",
            "displayName": "Local Cursor",
            "source": {
                "kind": "localPath",
                "path": "/tmp/cursor"
            }
        }))
        .unwrap();
        assert_eq!(add.id, "local-cursor");
        assert_eq!(add.kind, Some(MarketplaceKind::Cursor));
        assert_eq!(
            add.source,
            MarketplaceSource::LocalPath {
                path: "/tmp/cursor".to_string()
            }
        );

        let remove = MarketplacesRemoveParams {
            marketplace_id: "local-cursor".to_string(),
        };
        let value = serde_json::to_value(remove).unwrap();
        assert_eq!(value["marketplaceId"], "local-cursor");

        let disable = PluginDisableParams {
            variant_key: "codex-plugins:superpowers".to_string(),
        };
        let value = serde_json::to_value(disable).unwrap();
        assert_eq!(value["variantKey"], "codex-plugins:superpowers");

        let all = PluginInstallAllVariantsParams {
            marketplace_id: "codex-plugins".to_string(),
            plugin_id: "superpowers".to_string(),
        };
        let value = serde_json::to_value(all).unwrap();
        assert_eq!(value["marketplaceId"], "codex-plugins");

        let uninstall = PluginUninstallParams {
            variant_key: "codex-plugins:superpowers".to_string(),
        };
        let value = serde_json::to_value(uninstall).unwrap();
        assert_eq!(value["variantKey"], "codex-plugins:superpowers");
    }

    #[test]
    fn skills_protocol_structs_use_camel_case_fields() {
        let params: SkillsSetExposureParams = serde_json::from_value(serde_json::json!({
            "selector": { "name": { "name": "commit" } },
            "exposure": "direct_only"
        }))
        .unwrap();
        assert_eq!(
            params.selector,
            SkillSelector::Name {
                name: "commit".to_string()
            }
        );
        assert_eq!(params.exposure, SkillExposure::DirectOnly);

        let enabled = SkillsSetEnabledParams {
            selector: SkillSelector::Path {
                path: "roder-builtin://commit/SKILL.md".to_string(),
            },
            enabled: false,
        };
        let value = serde_json::to_value(enabled).unwrap();
        assert_eq!(
            value["selector"]["path"]["path"],
            "roder-builtin://commit/SKILL.md"
        );
        assert_eq!(value["enabled"], false);
    }

    #[test]
    fn automations_protocol_structs_use_camel_case_fields() {
        let create: AutomationsCreateParams = serde_json::from_value(serde_json::json!({
            "name": "Nightly cleanup",
            "project": { "cwd": "/repo", "displayName": "repo" },
            "schedule": { "interval": { "seconds": 300 } },
            "prompt": "summarize status",
            "modelProvider": "codex",
            "model": "gpt-5.5",
            "policyMode": "plan",
            "catchUp": { "runLatestOnly": null },
            "concurrency": "forbid"
        }))
        .unwrap();

        assert_eq!(create.name, "Nightly cleanup");
        assert_eq!(create.project.display_name.as_deref(), Some("repo"));
        assert_eq!(create.model_provider.as_deref(), Some("codex"));
        assert_eq!(
            create.policy_mode,
            Some(roder_api::policy_mode::PolicyMode::Plan)
        );
        assert_eq!(create.catch_up, CatchUpPolicy::RunLatestOnly);

        let update = AutomationsUpdateParams {
            automation_id: "automation-1".to_string(),
            patch: AutomationsUpdatePatch {
                model_provider: Some("codex".to_string()),
                catch_up: Some(CatchUpPolicy::SkipExpired { grace_seconds: 60 }),
                ..AutomationsUpdatePatch::default()
            },
        };
        let value = serde_json::to_value(update).unwrap();
        assert_eq!(value["automationId"], "automation-1");
        assert_eq!(value["patch"]["modelProvider"], "codex");
        assert_eq!(value["patch"]["catchUp"]["skipExpired"]["graceSeconds"], 60);
        assert!(value.get("automation_id").is_none());
    }

    #[test]
    fn processes_protocol_structs_cover_list_detail_stop_and_subscribe() {
        let descriptor = ProcessDescriptor {
            process_id: "process-1".to_string(),
            origin: roder_api::processes::ProcessOrigin::CommandExec,
            state: roder_api::processes::ProcessState::Running,
            command: vec!["sleep".to_string(), "10".to_string()],
            command_summary: "sleep 10".to_string(),
            cwd: Some("/repo".to_string()),
            pid: Some(1234),
            task_id: Some("task-1".to_string()),
            thread_id: Some("thread-1".to_string()),
            turn_id: Some("turn-1".to_string()),
            runner_destination_id: None,
            runner_session_id: None,
            stoppable: true,
            started_at: OffsetDateTime::UNIX_EPOCH,
            updated_at: OffsetDateTime::UNIX_EPOCH,
            stdout_tail: Some("ready\n".to_string()),
            stderr_tail: None,
        };

        let list_params: ProcessesListParams = serde_json::from_value(serde_json::json!({
            "includeCompleted": true
        }))
        .unwrap();
        assert!(list_params.include_completed);

        let list = serde_json::to_value(ProcessesListResult {
            processes: vec![descriptor.clone()],
        })
        .unwrap();
        assert_eq!(list["processes"][0]["processId"], "process-1");
        assert_eq!(list["processes"][0]["pid"], 1234);
        assert!(list["processes"][0].get("process_id").is_none());

        let get: ProcessesGetParams = serde_json::from_value(serde_json::json!({
            "processId": "process-1",
            "outputBytes": 1024
        }))
        .unwrap();
        assert_eq!(get.process_id, "process-1");
        assert_eq!(get.output_bytes, Some(1024));

        let stop: ProcessesStopParams = serde_json::from_value(serde_json::json!({
            "processId": "process-1",
            "reason": "user requested stop"
        }))
        .unwrap();
        assert_eq!(stop.process_id, "process-1");
        assert_eq!(stop.reason.as_deref(), Some("user requested stop"));

        let stop_all: ProcessesStopAllResult = serde_json::from_value(serde_json::json!({
            "results": [{
                "processId": "process-1",
                "stopped": true,
                "process": list["processes"][0].clone()
            }]
        }))
        .unwrap();
        assert_eq!(stop_all.results[0].process_id, "process-1");
        assert!(stop_all.results[0].stopped);

        let subscribe = ProcessesSubscribeResult {
            subscribed: true,
            event_kinds: vec!["process.started".to_string(), "process.output".to_string()],
        };
        let value = serde_json::to_value(subscribe).unwrap();
        assert_eq!(value["subscribed"], true);
        assert_eq!(value["eventKinds"][0], "process.started");
        assert!(value.get("event_kinds").is_none());
    }

    #[test]
    fn eval_report_protocol_structs_use_camel_case_fields() {
        let list: EvalReportsListParams = serde_json::from_value(serde_json::json!({
            "limit": 5
        }))
        .unwrap();
        assert_eq!(list.limit, Some(5));

        let read: EvalReportReadParams = serde_json::from_value(serde_json::json!({
            "reportId": "eval-run",
            "maxBytes": 1024
        }))
        .unwrap();
        assert_eq!(read.report_id, "eval-run");
        assert_eq!(read.max_bytes, Some(1024));

        let result = EvalReportReadResult {
            summary: EvalReportSummary {
                id: "eval-run".to_string(),
                suite_id: "tool-calls".to_string(),
                fixture_count: 2,
                passed: 1,
                failed: 1,
                reliability: EvalReliabilitySummary {
                    retry_attempts: 2,
                    retry_recoveries: 1,
                    ..EvalReliabilitySummary::default()
                },
                generated_at: OffsetDateTime::UNIX_EPOCH,
            },
            markdown: "# Report".to_string(),
            truncated: false,
        };
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["summary"]["suiteId"], "tool-calls");
        assert_eq!(value["summary"]["fixtureCount"], 2);
        assert_eq!(value["summary"]["reliability"]["retryAttempts"], 2);
        assert_eq!(value["summary"]["reliability"]["retryRecoveries"], 1);
        assert_eq!(value["markdown"], "# Report");
    }

    #[test]
    fn deadline_notifications_use_camel_case_fields() {
        let value = serde_json::to_value(TurnDeadlineExceededNotification {
            thread_id: "thread-a".to_string(),
            turn_id: "turn-a".to_string(),
            deadline: OffsetDateTime::UNIX_EPOCH,
            partial_result: "partial evidence".to_string(),
        })
        .unwrap();
        assert_eq!(value["threadId"], "thread-a");
        assert_eq!(value["turnId"], "turn-a");
        assert_eq!(value["partialResult"], "partial evidence");

        let value = serde_json::to_value(TurnPartialResultNotification {
            thread_id: "thread-a".to_string(),
            turn_id: "turn-a".to_string(),
            summary: "partial evidence".to_string(),
        })
        .unwrap();
        assert_eq!(value["summary"], "partial evidence");
    }

    #[test]
    fn thread_goal_set_params_preserve_null_budget_clear() {
        let params: ThreadGoalSetParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-a",
            "tokenBudget": null
        }))
        .unwrap();
        assert_eq!(params.thread_id, "thread-a");
        assert_eq!(params.token_budget, Some(None));

        let params: ThreadGoalSetParams = serde_json::from_value(serde_json::json!({
            "threadId": "thread-a"
        }))
        .unwrap();
        assert_eq!(params.token_budget, None);
    }

    #[test]
    fn media_protocol_structs_use_camel_case_fields() {
        let read: MediaReadParams = serde_json::from_value(serde_json::json!({
            "artifactId": "media-1",
            "maxBytes": 1024
        }))
        .unwrap();
        assert_eq!(read.artifact_id, "media-1");
        assert_eq!(read.max_bytes, Some(1024));

        let attach = MediaAttachToTurnResult {
            attachment: MediaAttachment {
                artifact_id: "media-1".to_string(),
                mime_type: "image/png".to_string(),
                data_url: "data:image/png;base64,YWJj".to_string(),
            },
            image: Some(InputImage {
                image_url: "data:image/png;base64,YWJj".to_string(),
            }),
        };
        let value = serde_json::to_value(attach).unwrap();
        assert_eq!(value["attachment"]["artifactId"], "media-1");
        assert_eq!(value["attachment"]["mimeType"], "image/png");
        assert_eq!(value["image"]["image_url"], "data:image/png;base64,YWJj");
    }

    #[test]
    fn media_image_generation_params_flatten_the_canonical_request() {
        let params: MediaImageGenerateParams = serde_json::from_value(serde_json::json!({
            "prompt": "a tiny test image",
            "provider": "openai",
            "model": "gpt-image-2",
            "count": 2,
            "size": "1024x1024",
            "threadId": "thread-1"
        }))
        .unwrap();
        assert_eq!(params.request.prompt, "a tiny test image");
        assert_eq!(params.request.provider.as_deref(), Some("openai"));
        assert_eq!(params.request.count, Some(2));
        assert_eq!(params.thread_id.as_deref(), Some("thread-1"));

        let result = MediaImageProvidersListResult {
            default_provider: "fake".to_string(),
            providers: vec![roder_api::media::MediaProviderDescriptor {
                id: "fake".to_string(),
                display_name: "Fake Media (offline)".to_string(),
                supports_images: true,
                configured: true,
                ..roder_api::media::MediaProviderDescriptor::default()
            }],
        };
        let value = serde_json::to_value(result).unwrap();
        assert_eq!(value["defaultProvider"], "fake");
        assert_eq!(value["providers"][0]["supportsImages"], true);
    }

    #[test]
    fn transcript_json_rpc_boundary_values_round_trip() {
        let request = JsonRpcRequest {
            jsonrpc: "2.0".to_string(),
            id: Some(serde_json::json!(7)),
            method: "processes/list".to_string(),
            params: Some(serde_json::json!({"includeCompleted": true})),
        };
        let response = JsonRpcResponse {
            jsonrpc: "2.0".to_string(),
            id: Some(serde_json::json!(7)),
            result: Some(serde_json::json!({"processes": []})),
            error: None,
        };
        let notification = JsonRpcNotification {
            jsonrpc: "2.0".to_string(),
            method: "processes/changed".to_string(),
            params: serde_json::json!({"processId": "proc-a"}),
        };

        assert_eq!(
            serde_json::to_value(&request).unwrap()["method"],
            "processes/list"
        );
        assert_eq!(
            serde_json::to_value(&response).unwrap()["result"]["processes"],
            serde_json::json!([])
        );
        assert_eq!(
            serde_json::to_value(&notification).unwrap()["method"],
            "processes/changed"
        );
    }
}
