diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 4c9cdb18..2e111c4c 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -992,8 +992,14 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady if a.Meta != nil { common.meta = a.Meta } + var pending *ahptypes.ToolCallPendingConfirmationState + if value, ok := tc.Value.(*ahptypes.ToolCallPendingConfirmationState); ok { + pending = value + } switch tc.Value.(type) { - case *ahptypes.ToolCallStreamingState, *ahptypes.ToolCallRunningState: + case *ahptypes.ToolCallStreamingState, + *ahptypes.ToolCallRunningState, + *ahptypes.ToolCallPendingConfirmationState: if a.Confirmed != nil { return ahptypes.ToolCallState{Value: &ahptypes.ToolCallRunningState{ Status: ahptypes.ToolCallStatusRunning, @@ -1008,7 +1014,7 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady Confirmed: *a.Confirmed, }} } - return ahptypes.ToolCallState{Value: &ahptypes.ToolCallPendingConfirmationState{ + next := &ahptypes.ToolCallPendingConfirmationState{ Status: ahptypes.ToolCallStatusPendingConfirmation, ToolCallId: common.id, ToolName: common.name, @@ -1019,10 +1025,32 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady InvocationMessage: a.InvocationMessage, ToolInput: a.ToolInput, ConfirmationTitle: a.ConfirmationTitle, + RiskAssessment: a.RiskAssessment, Edits: a.Edits, Editable: a.Editable, Options: a.Options, - }} + } + if pending != nil { + if next.ToolInput == nil { + next.ToolInput = pending.ToolInput + } + if next.ConfirmationTitle == nil { + next.ConfirmationTitle = pending.ConfirmationTitle + } + if next.RiskAssessment == nil { + next.RiskAssessment = pending.RiskAssessment + } + if next.Edits == nil { + next.Edits = pending.Edits + } + if next.Editable == nil { + next.Editable = pending.Editable + } + if next.Options == nil { + next.Options = pending.Options + } + } + return ahptypes.ToolCallState{Value: next} } return tc }) diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 18de3b4f..0e291a7f 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -350,6 +350,8 @@ type ChatToolCallReadyAction struct { ToolInput *string `json:"toolInput,omitempty"` // Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) ConfirmationTitle *StringOrMarkdown `json:"confirmationTitle,omitempty"` + // Risk assessment that informed the confirmation requirement. + RiskAssessment *ToolCallRiskAssessment `json:"riskAssessment,omitempty"` // File edits that this tool call will perform, for preview before confirmation Edits *json.RawMessage `json:"edits,omitempty"` // Whether the agent host allows the client to edit the tool's input parameters before confirming diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 462087f3..1f2ba3c3 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -237,6 +237,21 @@ const ( ToolCallConfirmationReasonSetting ToolCallConfirmationReason = "setting" ) +// Identifies a model judge as the source of a confirmation requirement. +type ToolCallRiskAssessmentKind string + +const ( + ToolCallRiskAssessmentKindJudge ToolCallRiskAssessmentKind = "judge" +) + +// Lifecycle status of an asynchronous model-judge confirmation decision. +type ToolCallRiskAssessmentStatus string + +const ( + ToolCallRiskAssessmentStatusLoading ToolCallRiskAssessmentStatus = "loading" + ToolCallRiskAssessmentStatusComplete ToolCallRiskAssessmentStatus = "complete" +) + // Why a tool call was cancelled. type ToolCallCancellationReason string @@ -1702,6 +1717,21 @@ type ToolCallResult struct { Error *json.RawMessage `json:"error,omitempty"` } +// The model judge is still evaluating the tool call. +type ToolCallRiskAssessmentLoadingState struct { + Kind ToolCallRiskAssessmentKind `json:"kind"` + Status ToolCallRiskAssessmentStatus `json:"status"` +} + +// The model judge has completed its evaluation. +type ToolCallRiskAssessmentCompleteState struct { + Kind ToolCallRiskAssessmentKind `json:"kind"` + Status ToolCallRiskAssessmentStatus `json:"status"` + Reason StringOrMarkdown `json:"reason"` + // The judge's normalized safety score, where `0` is unsafe and `1` is safe. + Safety float64 `json:"safety"` +} + // A confirmation option that the server offers for a tool call awaiting // approval. Allows richer choices beyond simple approve/deny — for example, // "Approve in this Session" or "Deny with reason." @@ -1771,6 +1801,8 @@ type ToolCallPendingConfirmationState struct { Status ToolCallStatus `json:"status"` // Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) ConfirmationTitle *StringOrMarkdown `json:"confirmationTitle,omitempty"` + // Risk assessment that informed the confirmation requirement. + RiskAssessment *ToolCallRiskAssessment `json:"riskAssessment,omitempty"` // File edits that this tool call will perform, for preview before confirmation Edits *json.RawMessage `json:"edits,omitempty"` // Whether the agent host allows the client to edit the tool's input parameters before confirming @@ -4353,6 +4385,66 @@ func (u ToolCallContributor) MarshalJSON() ([]byte, error) { return json.Marshal(u.Value) } +// ToolCallRiskAssessment is an asynchronous model-judge risk assessment. +type ToolCallRiskAssessment struct { + Value isToolCallRiskAssessment +} + +// isToolCallRiskAssessment is the marker interface implemented by every +// concrete variant of ToolCallRiskAssessment. +type isToolCallRiskAssessment interface{ isToolCallRiskAssessment() } + +func (*ToolCallRiskAssessmentLoadingState) isToolCallRiskAssessment() {} +func (*ToolCallRiskAssessmentCompleteState) isToolCallRiskAssessment() {} + +// ToolCallRiskAssessmentUnknown carries an unrecognized ToolCallRiskAssessment variant — typically a discriminator value introduced by a newer protocol version. The original JSON object is preserved verbatim so that re-encoding round-trips faithfully. +type ToolCallRiskAssessmentUnknown struct { + Raw json.RawMessage +} + +func (*ToolCallRiskAssessmentUnknown) isToolCallRiskAssessment() {} + +// UnmarshalJSON decodes the variant indicated by the "status" discriminator. +func (u *ToolCallRiskAssessment) UnmarshalJSON(data []byte) error { + disc, _, err := readDiscriminator(data, "status") + if err != nil { + return err + } + switch disc { + case "loading": + var value ToolCallRiskAssessmentLoadingState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + case "complete": + var value ToolCallRiskAssessmentCompleteState + if err := json.Unmarshal(data, &value); err != nil { + return err + } + u.Value = &value + default: + raw := make(json.RawMessage, len(data)) + copy(raw, data) + u.Value = &ToolCallRiskAssessmentUnknown{Raw: raw} + } + return nil +} + +// MarshalJSON encodes the active variant back to JSON. +func (u ToolCallRiskAssessment) MarshalJSON() ([]byte, error) { + if unk, ok := u.Value.(*ToolCallRiskAssessmentUnknown); ok { + if len(unk.Raw) == 0 { + return []byte("null"), nil + } + return unk.Raw, nil + } + if u.Value == nil { + return []byte("null"), nil + } + return json.Marshal(u.Value) +} + // SessionInputRequest is one outstanding piece of input a session is blocked on, aggregated across all chats. type SessionInputRequest struct { Value isSessionInputRequest diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 712a688e..52dca063 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -860,10 +860,15 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val a = action.value refreshChatSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> - if (tc !is ToolCallStateStreaming && tc !is ToolCallStateRunning) { + if ( + tc !is ToolCallStateStreaming + && tc !is ToolCallStateRunning + && tc !is ToolCallStatePendingConfirmation + ) { tc } else { val base = toolCallBase(tc).withMeta(a.meta) + val pending = (tc as? ToolCallStatePendingConfirmation)?.value if (a.confirmed != null) { ToolCallStateRunning( ToolCallRunningState( @@ -889,12 +894,13 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when contributor = base.contributor, meta = base.meta, invocationMessage = a.invocationMessage, - toolInput = a.toolInput, + toolInput = a.toolInput ?: pending?.toolInput, status = ToolCallStatus.PENDING_CONFIRMATION, - confirmationTitle = a.confirmationTitle, - edits = a.edits, - editable = a.editable, - options = a.options, + confirmationTitle = a.confirmationTitle ?: pending?.confirmationTitle, + riskAssessment = a.riskAssessment ?: pending?.riskAssessment, + edits = a.edits ?: pending?.edits, + editable = a.editable ?: pending?.editable, + options = a.options ?: pending?.options, ), ) } diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index 4a9517c4..6df05eba 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -471,6 +471,10 @@ data class ChatToolCallReadyAction( * Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ val confirmationTitle: StringOrMarkdown? = null, + /** + * Risk assessment that informed the confirmation requirement. + */ + val riskAssessment: ToolCallRiskAssessment? = null, /** * File edits that this tool call will perform, for preview before confirmation */ diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 9a008418..2b330b4b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -433,6 +433,26 @@ enum class ToolCallConfirmationReason { SETTING } +/** + * Identifies a model judge as the source of a confirmation requirement. + */ +@Serializable +enum class ToolCallRiskAssessmentKind { + @SerialName("judge") + JUDGE +} + +/** + * Lifecycle status of an asynchronous model-judge confirmation decision. + */ +@Serializable +enum class ToolCallRiskAssessmentStatus { + @SerialName("loading") + LOADING, + @SerialName("complete") + COMPLETE +} + /** * Why a tool call was cancelled. */ @@ -2446,6 +2466,10 @@ data class ToolCallPendingConfirmationState( * Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ val confirmationTitle: StringOrMarkdown? = null, + /** + * Risk assessment that informed the confirmation requirement. + */ + val riskAssessment: ToolCallRiskAssessment? = null, /** * File edits that this tool call will perform, for preview before confirmation */ @@ -2726,6 +2750,23 @@ data class ToolCallCancelledState( val selectedOption: ConfirmationOption? = null ) +@Serializable +data class ToolCallRiskAssessmentLoadingState( + val kind: ToolCallRiskAssessmentKind, + val status: ToolCallRiskAssessmentStatus +) + +@Serializable +data class ToolCallRiskAssessmentCompleteState( + val kind: ToolCallRiskAssessmentKind, + val status: ToolCallRiskAssessmentStatus, + val reason: StringOrMarkdown, + /** + * The judge's normalized safety score, where `0` is unsafe and `1` is safe. + */ + val safety: Double +) + @Serializable data class ConfirmationOption( /** @@ -5163,6 +5204,55 @@ internal object ToolCallContributorSerializer : KSerializer } } +@Serializable(with = ToolCallRiskAssessmentSerializer::class) +sealed interface ToolCallRiskAssessment + +@JvmInline +value class ToolCallRiskAssessmentLoading(val value: ToolCallRiskAssessmentLoadingState) : ToolCallRiskAssessment +@JvmInline +value class ToolCallRiskAssessmentComplete(val value: ToolCallRiskAssessmentCompleteState) : ToolCallRiskAssessment +/** + * Forward-compat catch-all for unknown ToolCallRiskAssessment discriminators. + * + * Older clients may receive newer wire variants they don't recognise; capturing + * the raw `JsonObject` lets such payloads round-trip through the client unchanged. + * Reducers handle this variant conservatively on a per-union basis (typically + * as a no-op, but see `Reducers.kt` for the exact treatment). + */ +@JvmInline +value class ToolCallRiskAssessmentUnknown(val raw: JsonObject) : ToolCallRiskAssessment + +internal object ToolCallRiskAssessmentSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("ToolCallRiskAssessment") + + override fun deserialize(decoder: Decoder): ToolCallRiskAssessment { + val input = decoder as? JsonDecoder + ?: error("ToolCallRiskAssessment can only be deserialized from JSON") + val element = input.decodeJsonElement() + val obj = element as? JsonObject + ?: error("Expected JsonObject for ToolCallRiskAssessment") + val discriminant = (obj["status"] as? JsonPrimitive)?.content + ?: return ToolCallRiskAssessmentUnknown(obj) + return when (discriminant) { + "loading" -> ToolCallRiskAssessmentLoading(input.json.decodeFromJsonElement(ToolCallRiskAssessmentLoadingState.serializer(), element)) + "complete" -> ToolCallRiskAssessmentComplete(input.json.decodeFromJsonElement(ToolCallRiskAssessmentCompleteState.serializer(), element)) + else -> ToolCallRiskAssessmentUnknown(obj) + } + } + + override fun serialize(encoder: Encoder, value: ToolCallRiskAssessment) { + val output = encoder as? JsonEncoder + ?: error("ToolCallRiskAssessment can only be serialized to JSON") + val element: JsonElement = when (value) { + is ToolCallRiskAssessmentLoading -> output.json.encodeToJsonElement(ToolCallRiskAssessmentLoadingState.serializer(), value.value) + is ToolCallRiskAssessmentComplete -> output.json.encodeToJsonElement(ToolCallRiskAssessmentCompleteState.serializer(), value.value) + is ToolCallRiskAssessmentUnknown -> value.raw + } + output.encodeJsonElement(element) + } +} + @Serializable(with = SessionInputRequestSerializer::class) sealed interface SessionInputRequest diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index c76109d8..17570b98 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -19,7 +19,8 @@ use crate::state::{ ConfirmationOption, Customization, ErrorInfo, McpServerState, Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, ToolCallConfirmationReason, - ToolCallContributor, ToolCallResult, ToolDefinition, ToolResultContent, Turn, UsageInfo, + ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolDefinition, ToolResultContent, + Turn, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -476,6 +477,9 @@ pub struct ChatToolCallReadyAction { /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) #[serde(default, skip_serializing_if = "Option::is_none")] pub confirmation_title: Option, + /// Risk assessment that informed the confirmation requirement. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub risk_assessment: Option, /// File edits that this tool call will perform, for preview before confirmation #[serde(default, skip_serializing_if = "Option::is_none")] pub edits: Option, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 3c857eb6..311bb072 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -342,6 +342,22 @@ pub enum ToolCallConfirmationReason { Setting, } +/// Identifies a model judge as the source of a confirmation requirement. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ToolCallRiskAssessmentKind { + #[serde(rename = "judge")] + Judge, +} + +/// Lifecycle status of an asynchronous model-judge confirmation decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ToolCallRiskAssessmentStatus { + #[serde(rename = "loading")] + Loading, + #[serde(rename = "complete")] + Complete, +} + /// Why a tool call was cancelled. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ToolCallCancellationReason { @@ -2127,6 +2143,23 @@ pub struct ToolCallResult { pub error: Option, } +/// The model judge is still evaluating the tool call. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallRiskAssessmentLoadingState { + pub kind: ToolCallRiskAssessmentKind, +} + +/// The model judge has completed its evaluation. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallRiskAssessmentCompleteState { + pub kind: ToolCallRiskAssessmentKind, + pub reason: StringOrMarkdown, + /// The judge's normalized safety score, where `0` is unsafe and `1` is safe. + pub safety: f64, +} + /// A confirmation option that the server offers for a tool call awaiting /// approval. Allows richer choices beyond simple approve/deny — for example, /// "Approve in this Session" or "Deny with reason." @@ -2211,6 +2244,9 @@ pub struct ToolCallPendingConfirmationState { /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) #[serde(default, skip_serializing_if = "Option::is_none")] pub confirmation_title: Option, + /// Risk assessment that informed the confirmation requirement. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub risk_assessment: Option, /// File edits that this tool call will perform, for preview before confirmation #[serde(default, skip_serializing_if = "Option::is_none")] pub edits: Option, @@ -4203,6 +4239,20 @@ pub enum ToolCallContributor { Unknown(serde_json::Value), } +/// Asynchronous model-judge confirmation rationale. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "status")] +pub enum ToolCallRiskAssessment { + #[serde(rename = "loading")] + Loading(ToolCallRiskAssessmentLoadingState), + #[serde(rename = "complete")] + Complete(ToolCallRiskAssessmentCompleteState), + /// Unknown or future variant — preserved as raw JSON for round-trip fidelity. + /// Reducers treat this as a no-op. + #[serde(untagged)] + Unknown(serde_json::Value), +} + /// One outstanding piece of input a session is blocked on, aggregated across all chats. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "kind")] diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index ba975d70..0dbce2f5 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -1157,8 +1157,14 @@ fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let base = tool_call_meta(&tc); let meta = a.meta.clone().or(base.meta); + let pending = match &tc { + ToolCallState::PendingConfirmation(value) => Some(value.clone()), + _ => None, + }; match tc { - ToolCallState::Streaming(_) | ToolCallState::Running(_) => { + ToolCallState::Streaming(_) + | ToolCallState::Running(_) + | ToolCallState::PendingConfirmation(_) => { if let Some(confirmed) = a.confirmed { ToolCallState::Running(ToolCallRunningState { tool_call_id: base.tool_call_id, @@ -1182,11 +1188,28 @@ fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> contributor: base.contributor, meta, invocation_message: a.invocation_message.clone(), - tool_input: a.tool_input.clone(), - confirmation_title: a.confirmation_title.clone(), - edits: a.edits.clone(), - editable: a.editable, - options: a.options.clone(), + tool_input: a + .tool_input + .clone() + .or_else(|| pending.as_ref().and_then(|p| p.tool_input.clone())), + confirmation_title: a.confirmation_title.clone().or_else(|| { + pending.as_ref().and_then(|p| p.confirmation_title.clone()) + }), + risk_assessment: a + .risk_assessment + .clone() + .or_else(|| pending.as_ref().and_then(|p| p.risk_assessment.clone())), + edits: a + .edits + .clone() + .or_else(|| pending.as_ref().and_then(|p| p.edits.clone())), + editable: a + .editable + .or_else(|| pending.as_ref().and_then(|p| p.editable)), + options: a + .options + .clone() + .or_else(|| pending.as_ref().and_then(|p| p.options.clone())), }) } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 76473df3..6d5d0e6d 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -480,6 +480,8 @@ public struct ChatToolCallReadyAction: Codable, Sendable { public var toolInput: String? /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) public var confirmationTitle: StringOrMarkdown? + /// Risk assessment that informed the confirmation requirement. + public var riskAssessment: ToolCallRiskAssessment? /// File edits that this tool call will perform, for preview before confirmation public var edits: AnyCodable? /// Whether the agent host allows the client to edit the tool's input parameters before confirming @@ -500,6 +502,7 @@ public struct ChatToolCallReadyAction: Codable, Sendable { case invocationMessage case toolInput case confirmationTitle + case riskAssessment case edits case editable case confirmed @@ -514,6 +517,7 @@ public struct ChatToolCallReadyAction: Codable, Sendable { invocationMessage: StringOrMarkdown, toolInput: String? = nil, confirmationTitle: StringOrMarkdown? = nil, + riskAssessment: ToolCallRiskAssessment? = nil, edits: AnyCodable? = nil, editable: Bool? = nil, confirmed: ToolCallConfirmationReason? = nil, @@ -526,6 +530,7 @@ public struct ChatToolCallReadyAction: Codable, Sendable { self.invocationMessage = invocationMessage self.toolInput = toolInput self.confirmationTitle = confirmationTitle + self.riskAssessment = riskAssessment self.edits = edits self.editable = editable self.confirmed = confirmed diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 6af2ddf1..43684476 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -225,6 +225,17 @@ public enum ToolCallConfirmationReason: String, Codable, Sendable { case setting = "setting" } +/// Identifies a model judge as the source of a confirmation requirement. +public enum ToolCallRiskAssessmentKind: String, Codable, Sendable { + case judge = "judge" +} + +/// Lifecycle status of an asynchronous model-judge confirmation decision. +public enum ToolCallRiskAssessmentStatus: String, Codable, Sendable { + case loading = "loading" + case complete = "complete" +} + /// Why a tool call was cancelled. public enum ToolCallCancellationReason: String, Codable, Sendable { case denied = "denied" @@ -2462,6 +2473,8 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { public var status: ToolCallStatus /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) public var confirmationTitle: StringOrMarkdown? + /// Risk assessment that informed the confirmation requirement. + public var riskAssessment: ToolCallRiskAssessment? /// File edits that this tool call will perform, for preview before confirmation public var edits: AnyCodable? /// Whether the agent host allows the client to edit the tool's input parameters before confirming @@ -2483,6 +2496,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { case toolInput case status case confirmationTitle + case riskAssessment case edits case editable case options @@ -2499,6 +2513,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { toolInput: String? = nil, status: ToolCallStatus, confirmationTitle: StringOrMarkdown? = nil, + riskAssessment: ToolCallRiskAssessment? = nil, edits: AnyCodable? = nil, editable: Bool? = nil, options: [ConfirmationOption]? = nil @@ -2513,6 +2528,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { self.toolInput = toolInput self.status = status self.confirmationTitle = confirmationTitle + self.riskAssessment = riskAssessment self.edits = edits self.editable = editable self.options = options @@ -2867,6 +2883,39 @@ public struct ToolCallCancelledState: Codable, Sendable { } } +public struct ToolCallRiskAssessmentLoadingState: Codable, Sendable { + public var kind: ToolCallRiskAssessmentKind + public var status: ToolCallRiskAssessmentStatus + + public init( + kind: ToolCallRiskAssessmentKind, + status: ToolCallRiskAssessmentStatus + ) { + self.kind = kind + self.status = status + } +} + +public struct ToolCallRiskAssessmentCompleteState: Codable, Sendable { + public var kind: ToolCallRiskAssessmentKind + public var status: ToolCallRiskAssessmentStatus + public var reason: StringOrMarkdown + /// The judge's normalized safety score, where `0` is unsafe and `1` is safe. + public var safety: Double + + public init( + kind: ToolCallRiskAssessmentKind, + status: ToolCallRiskAssessmentStatus, + reason: StringOrMarkdown, + safety: Double + ) { + self.kind = kind + self.status = status + self.reason = reason + self.safety = safety + } +} + public struct ConfirmationOption: Codable, Sendable { /// Unique identifier for the option, returned in the confirmed action public var id: String @@ -5465,6 +5514,39 @@ public enum ToolCallContributor: Codable, Sendable { } } +public enum ToolCallRiskAssessment: Codable, Sendable { + case loading(ToolCallRiskAssessmentLoadingState) + case complete(ToolCallRiskAssessmentCompleteState) + /// Unknown or future discriminant; the raw payload is preserved + /// and re-encoded verbatim for forward-compatibility. + case unknown(AnyCodable) + + private enum DiscriminantKey: String, CodingKey { + case discriminant = "status" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: DiscriminantKey.self) + let discriminant = try container.decode(String.self, forKey: .discriminant) + switch discriminant { + case "loading": + self = .loading(try ToolCallRiskAssessmentLoadingState(from: decoder)) + case "complete": + self = .complete(try ToolCallRiskAssessmentCompleteState(from: decoder)) + default: + self = .unknown(try AnyCodable(from: decoder)) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case .loading(let value): try value.encode(to: encoder) + case .complete(let value): try value.encode(to: encoder) + case .unknown(let value): try value.encode(to: encoder) + } + } +} + public enum SessionInputRequest: Codable, Sendable { case chatInput(SessionChatInputRequest) case toolConfirmation(SessionToolConfirmationRequest) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 58c67fc3..e3df3979 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -189,11 +189,17 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { case .chatToolCallReady(let a): return refreshChatSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in switch tc { - case .streaming, .running: break + case .streaming, .running, .pendingConfirmation: break default: return tc } let base = tc.baseFields let meta = a.meta ?? base.meta + let pending: ToolCallPendingConfirmationState? + if case .pendingConfirmation(let value) = tc { + pending = value + } else { + pending = nil + } if let confirmed = a.confirmed { return .running(ToolCallRunningState( toolCallId: base.toolCallId, @@ -216,12 +222,13 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { contributor: base.contributor, meta: meta, invocationMessage: a.invocationMessage, - toolInput: a.toolInput, + toolInput: a.toolInput ?? pending?.toolInput, status: .pendingConfirmation, - confirmationTitle: a.confirmationTitle, - edits: a.edits, - editable: a.editable, - options: a.options + confirmationTitle: a.confirmationTitle ?? pending?.confirmationTitle, + riskAssessment: a.riskAssessment ?? pending?.riskAssessment, + edits: a.edits ?? pending?.edits, + editable: a.editable ?? pending?.editable, + options: a.options ?? pending?.options )) }) diff --git a/docs/.changes/20260713-tool-call-risk-assessment.json b/docs/.changes/20260713-tool-call-risk-assessment.json new file mode 100644 index 00000000..fafdbc5c --- /dev/null +++ b/docs/.changes/20260713-tool-call-risk-assessment.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "Asynchronous tool-call risk assessments with a model-provided explanation and normalized safety score." +} diff --git a/schema/actions.schema.json b/schema/actions.schema.json index bb4a1a28..db0fb3e1 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -846,6 +846,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -5266,6 +5270,58 @@ "content" ] }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + } + }, + "required": [ + "kind", + "status", + "reason", + "safety" + ] + }, "ConfirmationOption": { "type": "object", "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", @@ -5515,6 +5571,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -6888,6 +6948,16 @@ } ] }, + "ToolCallRiskAssessment": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallRiskAssessmentLoadingState" + }, + { + "$ref": "#/$defs/ToolCallRiskAssessmentCompleteState" + } + ] + }, "ToolCallContributor": { "oneOf": [ { @@ -7341,6 +7411,13 @@ "type": "string", "description": "Discriminant for {@link MessageOrigin} — identifies who produced a message." }, + "ToolCallRiskAssessmentKind": { + "enum": [ + "judge" + ], + "type": "string", + "description": "Identifies a model judge as the source of a confirmation requirement." + }, "ConfirmationOptionKind": { "enum": [ "approve", diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 2379858d..6c56861e 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4534,6 +4534,58 @@ "content" ] }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + } + }, + "required": [ + "kind", + "status", + "reason", + "safety" + ] + }, "ConfirmationOption": { "type": "object", "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", @@ -4783,6 +4835,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -6707,6 +6763,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -8541,6 +8601,13 @@ "type": "string", "description": "How a client completed an input request." }, + "ToolCallRiskAssessmentKind": { + "enum": [ + "judge" + ], + "type": "string", + "description": "Identifies a model judge as the source of a confirmation requirement." + }, "ConfirmationOptionKind": { "enum": [ "approve", @@ -8585,6 +8652,16 @@ ], "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output,\n`ToolResultTerminalCompleteContent` for terminal-style completion metadata, and\n`ToolResultSubagentContent` for tool-spawned worker chats (AHP extensions)." }, + "ToolCallRiskAssessment": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallRiskAssessmentLoadingState" + }, + { + "$ref": "#/$defs/ToolCallRiskAssessmentCompleteState" + } + ] + }, "ToolCallConfirmationReason": { "enum": [ "not-needed", diff --git a/schema/errors.schema.json b/schema/errors.schema.json index e5d2b1ef..3202dff0 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -3325,6 +3325,58 @@ "content" ] }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + } + }, + "required": [ + "kind", + "status", + "reason", + "safety" + ] + }, "ConfirmationOption": { "type": "object", "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", @@ -3574,6 +3626,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -6359,6 +6415,13 @@ ], "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." }, + "ToolCallRiskAssessmentKind": { + "enum": [ + "judge" + ], + "type": "string", + "description": "Identifies a model judge as the source of a confirmation requirement." + }, "ConfirmationOptionKind": { "enum": [ "approve", @@ -6403,6 +6466,16 @@ ], "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output,\n`ToolResultTerminalCompleteContent` for terminal-style completion metadata, and\n`ToolResultSubagentContent` for tool-spawned worker chats (AHP extensions)." }, + "ToolCallRiskAssessment": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallRiskAssessmentLoadingState" + }, + { + "$ref": "#/$defs/ToolCallRiskAssessmentCompleteState" + } + ] + }, "ToolCallConfirmationReason": { "enum": [ "not-needed", @@ -7618,6 +7691,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 85e21832..894e41e5 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3485,6 +3485,58 @@ "content" ] }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + } + }, + "required": [ + "kind", + "status", + "reason", + "safety" + ] + }, "ConfirmationOption": { "type": "object", "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", @@ -3734,6 +3786,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -5229,6 +5285,13 @@ ], "description": "A string that may optionally be rendered as Markdown.\n\n- A plain `string` is rendered as-is (no Markdown processing).\n- An object with `{ markdown: string }` is rendered with Markdown formatting." }, + "ToolCallRiskAssessmentKind": { + "enum": [ + "judge" + ], + "type": "string", + "description": "Identifies a model judge as the source of a confirmation requirement." + }, "ConfirmationOptionKind": { "enum": [ "approve", @@ -5273,6 +5336,16 @@ ], "description": "Content block in a tool result.\n\nMirrors the content blocks in MCP `CallToolResult.content`, plus\n`ToolResultResourceContent` for lazy-loading large results,\n`ToolResultFileEditContent` for file edit diffs,\n`ToolResultTerminalContent` for live terminal output,\n`ToolResultTerminalCompleteContent` for terminal-style completion metadata, and\n`ToolResultSubagentContent` for tool-spawned worker chats (AHP extensions)." }, + "ToolCallRiskAssessment": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallRiskAssessmentLoadingState" + }, + { + "$ref": "#/$defs/ToolCallRiskAssessmentCompleteState" + } + ] + }, "ToolCallConfirmationReason": { "enum": [ "not-needed", diff --git a/schema/state.schema.json b/schema/state.schema.json index 5017391e..0f4c947f 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -3236,6 +3236,58 @@ "content" ] }, + "ToolCallRiskAssessmentBase": { + "type": "object", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + } + }, + "required": [ + "kind" + ] + }, + "ToolCallRiskAssessmentLoadingState": { + "type": "object", + "description": "The model judge is still evaluating the tool call.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "loading" + } + }, + "required": [ + "kind", + "status" + ] + }, + "ToolCallRiskAssessmentCompleteState": { + "type": "object", + "description": "The model judge has completed its evaluation.", + "properties": { + "kind": { + "$ref": "#/$defs/ToolCallRiskAssessmentKind" + }, + "status": { + "const": "complete" + }, + "reason": { + "$ref": "#/$defs/StringOrMarkdown" + }, + "safety": { + "type": "number", + "description": "The judge's normalized safety score, where `0` is unsafe and `1` is safe." + } + }, + "required": [ + "kind", + "status", + "reason", + "safety" + ] + }, "ConfirmationOption": { "type": "object", "description": "A confirmation option that the server offers for a tool call awaiting\napproval. Allows richer choices beyond simple approve/deny — for example,\n\"Approve in this Session\" or \"Deny with reason.\"", @@ -3485,6 +3537,10 @@ "$ref": "#/$defs/StringOrMarkdown", "description": "Short title for the confirmation prompt (e.g. `\"Run in terminal\"`, `\"Write file\"`)" }, + "riskAssessment": { + "$ref": "#/$defs/ToolCallRiskAssessment", + "description": "Risk assessment that informed the confirmation requirement." + }, "edits": { "type": "object", "properties": { @@ -4858,6 +4914,16 @@ } ] }, + "ToolCallRiskAssessment": { + "oneOf": [ + { + "$ref": "#/$defs/ToolCallRiskAssessmentLoadingState" + }, + { + "$ref": "#/$defs/ToolCallRiskAssessmentCompleteState" + } + ] + }, "ToolCallContributor": { "oneOf": [ { @@ -5030,6 +5096,13 @@ "type": "string", "description": "How a client completed an input request." }, + "ToolCallRiskAssessmentKind": { + "enum": [ + "judge" + ], + "type": "string", + "description": "Identifies a model judge as the source of a confirmation requirement." + }, "ConfirmationOptionKind": { "enum": [ "approve", diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 19176699..112bb9e3 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -644,7 +644,9 @@ const STATE_ENUMS = [ 'ChatOriginKind', 'ChatInteractivity', 'PendingMessageKind', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', - 'ToolCallConfirmationReason', 'ToolCallCancellationReason', + 'ToolCallConfirmationReason', 'ToolCallRiskAssessmentKind', + 'ToolCallRiskAssessmentStatus', + 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', @@ -711,6 +713,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; goName?: strin { name: 'SystemNotificationResponsePart' }, { name: 'InputRequestResponsePart' }, { name: 'ToolCallResult' }, + { name: 'ToolCallRiskAssessmentLoadingState' }, + { name: 'ToolCallRiskAssessmentCompleteState' }, { name: 'ConfirmationOption' }, { name: 'ToolCallStreamingState' }, { name: 'ToolCallPendingConfirmationState' }, @@ -971,6 +975,17 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_RISK_ASSESSMENT_UNION: UnionConfig = { + name: 'ToolCallRiskAssessment', + discriminantField: 'status', + doc: 'ToolCallRiskAssessment is an asynchronous model-judge risk assessment.', + variants: [ + { variantName: 'Loading', innerType: 'ToolCallRiskAssessmentLoadingState', wireValue: 'loading' }, + { variantName: 'Complete', innerType: 'ToolCallRiskAssessmentCompleteState', wireValue: 'complete' }, + ], + unknown: true, +}; + const SESSION_INPUT_REQUEST_UNION: UnionConfig = { name: 'SessionInputRequest', discriminantField: 'kind', @@ -1227,6 +1242,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_RISK_ASSESSMENT_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); lines.push(generateChatOriginGo()); @@ -1931,6 +1948,7 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', 'McpServerState', 'ToolCallContributor', + 'ToolCallRiskAssessment', 'SessionInputRequest', 'ToolCallConfirmationState', 'ReconnectResult', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 9ab6b405..ad429635 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -791,7 +791,9 @@ const STATE_ENUMS = [ 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', - 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', + 'ToolCallConfirmationReason', 'ToolCallRiskAssessmentKind', + 'ToolCallRiskAssessmentStatus', + 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', @@ -825,7 +827,9 @@ const STATE_STRUCTS = [ 'ToolCallResult', 'ToolCallStreamingState', 'ToolCallPendingConfirmationState', 'ToolCallRunningState', 'ToolCallPendingResultConfirmationState', 'ToolCallCompletedState', - 'ToolCallCancelledState', 'ConfirmationOption', 'ToolDefinition', 'ToolAnnotations', + 'ToolCallCancelledState', 'ToolCallRiskAssessmentLoadingState', + 'ToolCallRiskAssessmentCompleteState', 'ConfirmationOption', + 'ToolDefinition', 'ToolAnnotations', 'ToolResultTextContent', 'ToolResultEmbeddedResourceContent', 'ToolResultResourceContent', 'ToolResultFileEditContent', 'ToolResultTerminalContent', 'ToolResultTerminalCompleteContent', @@ -1075,6 +1079,16 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_RISK_ASSESSMENT_UNION: UnionConfig = { + name: 'ToolCallRiskAssessment', + discriminantField: 'status', + variants: [ + { caseName: 'Loading', structName: 'ToolCallRiskAssessmentLoadingState', discriminantValue: 'loading' }, + { caseName: 'Complete', structName: 'ToolCallRiskAssessmentCompleteState', discriminantValue: 'complete' }, + ], + unknown: true, +}; + const SESSION_INPUT_REQUEST_UNION: UnionConfig = { name: 'SessionInputRequest', discriminantField: 'kind', @@ -1153,6 +1167,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_RISK_ASSESSMENT_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); lines.push(generateToolResultContentUnion()); @@ -1918,6 +1934,7 @@ function checkExhaustiveness(project: Project): void { 'ChildCustomization', // CHILD_CUSTOMIZATION_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ChildCustomizationType', // TS subset alias of CustomizationType; consumers reuse CustomizationType diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index d29e56f1..06c2097e 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -654,7 +654,9 @@ const STATE_ENUMS = [ 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', - 'ToolCallConfirmationReason', 'ToolCallCancellationReason', + 'ToolCallConfirmationReason', 'ToolCallRiskAssessmentKind', + 'ToolCallRiskAssessmentStatus', + 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', @@ -742,6 +744,8 @@ const STATE_STRUCTS: { name: string; omitDiscriminants?: boolean; rustName?: str { name: 'SystemNotificationResponsePart', omitDiscriminants: true }, { name: 'InputRequestResponsePart', omitDiscriminants: true }, { name: 'ToolCallResult' }, + { name: 'ToolCallRiskAssessmentLoadingState', omitDiscriminants: true }, + { name: 'ToolCallRiskAssessmentCompleteState', omitDiscriminants: true }, { name: 'ConfirmationOption' }, { name: 'ToolCallStreamingState', omitDiscriminants: true }, { name: 'ToolCallPendingConfirmationState', omitDiscriminants: true }, @@ -1007,6 +1011,17 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { unknown: true, }; +const TOOL_CALL_RISK_ASSESSMENT_UNION: UnionConfig = { + name: 'ToolCallRiskAssessment', + discriminantField: 'status', + doc: 'Asynchronous model-judge confirmation rationale.', + variants: [ + { variantName: 'Loading', innerType: 'ToolCallRiskAssessmentLoadingState', wireValue: 'loading' }, + { variantName: 'Complete', innerType: 'ToolCallRiskAssessmentCompleteState', wireValue: 'complete' }, + ], + unknown: true, +}; + const SESSION_INPUT_REQUEST_UNION: UnionConfig = { name: 'SessionInputRequest', discriminantField: 'kind', @@ -1139,6 +1154,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_RISK_ASSESSMENT_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); lines.push(generateSnapshotState()); @@ -1274,7 +1291,7 @@ pub struct ${scope}ToolCallConfirmedAction { function generateActionsFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; lines.push('#[allow(unused_imports)]'); - lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); + lines.push('use crate::state::{AgentInfo, AgentSelection, Annotation, AnnotationEntry, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ConfirmationOption, Customization, ErrorInfo, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum @@ -1839,6 +1856,7 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'ReconnectResult', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index d168d1e8..ac706d28 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -541,7 +541,9 @@ const STATE_ENUMS = [ 'ChatOriginKind', 'ChatInteractivity', 'ChatInputAnswerState', 'ChatInputAnswerValueKind', 'ChatInputQuestionKind', 'ChatInputResponseKind', 'SessionInputRequestKind', 'TurnState', 'MessageKind', 'MessageAttachmentKind', 'ResponsePartKind', 'ToolCallStatus', - 'ToolCallConfirmationReason', 'ToolCallCancellationReason', 'ConfirmationOptionKind', + 'ToolCallConfirmationReason', 'ToolCallRiskAssessmentKind', + 'ToolCallRiskAssessmentStatus', + 'ToolCallCancellationReason', 'ConfirmationOptionKind', 'ToolCallContributorKind', 'ToolResultContentType', 'CustomizationType', 'CustomizationLoadStatus', 'TerminalClaimKind', 'McpServerStatus', 'McpAuthRequiredReason', @@ -575,7 +577,9 @@ const STATE_STRUCTS = [ 'ToolCallResult', 'ToolCallStreamingState', 'ToolCallPendingConfirmationState', 'ToolCallRunningState', 'ToolCallPendingResultConfirmationState', 'ToolCallCompletedState', - 'ToolCallCancelledState', 'ConfirmationOption', 'ToolDefinition', 'ToolAnnotations', + 'ToolCallCancelledState', 'ToolCallRiskAssessmentLoadingState', + 'ToolCallRiskAssessmentCompleteState', 'ConfirmationOption', + 'ToolDefinition', 'ToolAnnotations', 'ToolResultTextContent', 'ToolResultEmbeddedResourceContent', 'ToolResultResourceContent', 'ToolResultFileEditContent', 'ToolResultTerminalContent', 'ToolResultTerminalCompleteContent', @@ -785,6 +789,16 @@ const TOOL_CALL_CONTRIBUTOR_UNION: UnionConfig = { ], }; +const TOOL_CALL_RISK_ASSESSMENT_UNION: UnionConfig = { + name: 'ToolCallRiskAssessment', + discriminantField: 'status', + allowUnknown: true, + variants: [ + { caseName: 'loading', structName: 'ToolCallRiskAssessmentLoadingState', discriminantValue: 'loading' }, + { caseName: 'complete', structName: 'ToolCallRiskAssessmentCompleteState', discriminantValue: 'complete' }, + ], +}; + const SESSION_INPUT_REQUEST_UNION: UnionConfig = { name: 'SessionInputRequest', discriminantField: 'kind', @@ -1061,6 +1075,8 @@ function generateStateFile(project: Project): string { lines.push(''); lines.push(generateDiscriminatedUnion(TOOL_CALL_CONTRIBUTOR_UNION)); lines.push(''); + lines.push(generateDiscriminatedUnion(TOOL_CALL_RISK_ASSESSMENT_UNION)); + lines.push(''); lines.push(generateDiscriminatedUnion(SESSION_INPUT_REQUEST_UNION)); lines.push(''); lines.push(generateToolResultContentUnion()); @@ -1940,6 +1956,7 @@ function checkExhaustiveness(project: Project): void { 'CustomizationLoadState', // CUSTOMIZATION_LOAD_STATE_UNION discriminated union 'McpServerState', // MCP_SERVER_STATUS_UNION discriminated union 'ToolCallContributor', // TOOL_CALL_CONTRIBUTOR_UNION discriminated union + 'ToolCallRiskAssessment', // TOOL_CALL_RISK_ASSESSMENT_UNION discriminated union 'SessionInputRequest', // SESSION_INPUT_REQUEST_UNION discriminated union 'ToolCallConfirmationState', // TOOL_CALL_CONFIRMATION_STATE_UNION discriminated union 'AuthRequiredErrorData', // emitted by generateErrorsFile() diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index c1f5e663..463f9614 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -16,6 +16,7 @@ import type { ChatInputResponseKind, ConfirmationOption, ToolCallContributor, + ToolCallRiskAssessment, Turn, } from './state.js'; import { @@ -203,6 +204,8 @@ export interface ChatToolCallReadyAction extends ToolCallActionBase { toolInput?: string; /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ confirmationTitle?: StringOrMarkdown; + /** Risk assessment that informed the confirmation requirement. */ + riskAssessment?: ToolCallRiskAssessment; /** File edits that this tool call will perform, for preview before confirmation */ edits?: { items: FileEdit[] }; /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 1da23f9a..7f843240 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -382,7 +382,11 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st case ActionType.ChatToolCallReady: return refreshSummaryStatus(updateToolCallInParts(state, action.turnId, action.toolCallId, tc => { - if (tc.status !== ToolCallStatus.Streaming && tc.status !== ToolCallStatus.Running) { + if ( + tc.status !== ToolCallStatus.Streaming + && tc.status !== ToolCallStatus.Running + && tc.status !== ToolCallStatus.PendingConfirmation + ) { return tc; } const base = tcBaseWithMeta(tc, action._meta); @@ -395,15 +399,18 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st confirmed: action.confirmed, }; } + const pending = tc.status === ToolCallStatus.PendingConfirmation ? tc : undefined; + const options = action.options ?? pending?.options; return { status: ToolCallStatus.PendingConfirmation, ...base, invocationMessage: action.invocationMessage, - toolInput: action.toolInput, - confirmationTitle: action.confirmationTitle, - edits: action.edits, - editable: action.editable, - ...(action.options ? { options: action.options } : {}), + toolInput: action.toolInput ?? pending?.toolInput, + confirmationTitle: action.confirmationTitle ?? pending?.confirmationTitle, + riskAssessment: action.riskAssessment ?? pending?.riskAssessment, + edits: action.edits ?? pending?.edits, + editable: action.editable ?? pending?.editable, + ...(options ? { options } : {}), }; })); diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 9dff723f..ddc3f681 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -934,6 +934,57 @@ export const enum ToolCallConfirmationReason { Setting = 'setting', } +/** + * Identifies a model judge as the source of a confirmation requirement. + * + * @category Tool Call Types + */ +export const enum ToolCallRiskAssessmentKind { + Judge = 'judge', +} + +/** + * Lifecycle status of an asynchronous model-judge confirmation decision. + * + * @category Tool Call Types + */ +export const enum ToolCallRiskAssessmentStatus { + Loading = 'loading', + Complete = 'complete', +} + +interface ToolCallRiskAssessmentBase { + kind: ToolCallRiskAssessmentKind; +} + +/** + * The model judge is still evaluating the tool call. + * + * @category Tool Call Types + */ +export interface ToolCallRiskAssessmentLoadingState extends ToolCallRiskAssessmentBase { + status: ToolCallRiskAssessmentStatus.Loading; +} + +/** + * The model judge has completed its evaluation. + * + * @category Tool Call Types + */ +export interface ToolCallRiskAssessmentCompleteState extends ToolCallRiskAssessmentBase { + status: ToolCallRiskAssessmentStatus.Complete; + reason: StringOrMarkdown; + /** + * The judge's normalized safety score, where `0` is unsafe and `1` is safe. + * @format float + */ + safety: number; +} + +export type ToolCallRiskAssessment = + | ToolCallRiskAssessmentLoadingState + | ToolCallRiskAssessmentCompleteState; + /** * Why a tool call was cancelled. * @@ -1100,6 +1151,8 @@ export interface ToolCallPendingConfirmationState extends ToolCallBase, ToolCall status: ToolCallStatus.PendingConfirmation; /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ confirmationTitle?: StringOrMarkdown; + /** Risk assessment that informed the confirmation requirement. */ + riskAssessment?: ToolCallRiskAssessment; /** File edits that this tool call will perform, for preview before confirmation */ edits?: { items: FileEdit[] }; /** Whether the agent host allows the client to edit the tool's input parameters before confirming */ diff --git a/types/index.ts b/types/index.ts index bc57f091..413b2eba 100644 --- a/types/index.ts +++ b/types/index.ts @@ -54,6 +54,9 @@ export type { ToolCallCancelledState, ToolCallState, ToolCallConfirmationState, + ToolCallRiskAssessmentLoadingState, + ToolCallRiskAssessmentCompleteState, + ToolCallRiskAssessment, ConfirmationOption, ToolDefinition, ToolAnnotations, @@ -125,6 +128,8 @@ export { ResponsePartKind, ToolCallStatus, ToolCallConfirmationReason, + ToolCallRiskAssessmentKind, + ToolCallRiskAssessmentStatus, ToolCallCancellationReason, ConfirmationOptionKind, ToolResultContentType, diff --git a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json index 94f7f058..e4f6401e 100644 --- a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json +++ b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json @@ -74,6 +74,7 @@ "invocationMessage": "Run: rm -rf /tmp/test", "toolInput": null, "confirmationTitle": null, + "riskAssessment": null, "edits": null, "editable": null } diff --git a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json b/types/test-cases/reducers/029-toolcallready-updates-pending-confirmation-tool-calls.json similarity index 88% rename from types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json rename to types/test-cases/reducers/029-toolcallready-updates-pending-confirmation-tool-calls.json index d7e4acd6..c63b3432 100644 --- a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json +++ b/types/test-cases/reducers/029-toolcallready-updates-pending-confirmation-tool-calls.json @@ -1,5 +1,5 @@ { - "description": "toolCallReady ignores non-streaming/non-running tool calls", + "description": "toolCallReady updates pending-confirmation tool calls", "reducer": "chat", "initial": { "turns": [], @@ -66,9 +66,12 @@ "intention": null, "contributor": null, "_meta": null, - "invocationMessage": "Run", + "invocationMessage": "Run again", "toolInput": null, - "confirmationTitle": null + "confirmationTitle": null, + "riskAssessment": null, + "edits": null, + "editable": null } } ], diff --git a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json index fbf03a73..f2d01c3f 100644 --- a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json @@ -61,6 +61,7 @@ "invocationMessage": "Run: rm -rf /tmp/test", "toolInput": "rm -rf /tmp/test", "confirmationTitle": null, + "riskAssessment": null, "edits": null, "editable": null } diff --git a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json index 0822e2fc..68f2cbf0 100644 --- a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json +++ b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json @@ -33,6 +33,12 @@ "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: echo hello", + "riskAssessment": { + "kind": "judge", + "status": "complete", + "reason": "The command can modify the workspace.", + "safety": 0.25 + }, "options": [ { "id": "approve-once", @@ -86,6 +92,12 @@ "invocationMessage": "Run: echo hello", "toolInput": null, "confirmationTitle": null, + "riskAssessment": { + "kind": "judge", + "status": "complete", + "reason": "The command can modify the workspace.", + "safety": 0.25 + }, "edits": null, "editable": null, "options": [ diff --git a/types/test-cases/reducers/220-toolcall-actions-update-meta.json b/types/test-cases/reducers/220-toolcall-actions-update-meta.json index 2eebdd14..e46ac035 100644 --- a/types/test-cases/reducers/220-toolcall-actions-update-meta.json +++ b/types/test-cases/reducers/220-toolcall-actions-update-meta.json @@ -273,6 +273,7 @@ "invocationMessage": "Needs approval", "toolInput": null, "confirmationTitle": null, + "riskAssessment": null, "edits": null, "editable": null } diff --git a/types/test-cases/reducers/241-toolcallready-stores-loading-risk-assessment.json b/types/test-cases/reducers/241-toolcallready-stores-loading-risk-assessment.json new file mode 100644 index 00000000..7af3f862 --- /dev/null +++ b/types/test-cases/reducers/241-toolcallready-stores-loading-risk-assessment.json @@ -0,0 +1,83 @@ +{ + "description": "toolCallReady stores a loading risk assessment", + "reducer": "chat", + "initial": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" + }, + "actions": [ + { + "type": "chat/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-1", + "toolName": "bash", + "displayName": "Run Command" + }, + { + "type": "chat/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-1", + "invocationMessage": "Checking command safety", + "riskAssessment": { + "kind": "judge", + "status": "loading" + } + } + ], + "expected": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "tc-1", + "toolName": "bash", + "displayName": "Run Command", + "intention": null, + "contributor": null, + "_meta": null, + "invocationMessage": "Checking command safety", + "toolInput": null, + "confirmationTitle": null, + "riskAssessment": { + "kind": "judge", + "status": "loading" + }, + "edits": null, + "editable": null + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" + } +} diff --git a/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json b/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json new file mode 100644 index 00000000..56d548d5 --- /dev/null +++ b/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json @@ -0,0 +1,120 @@ +{ + "description": "toolCallReady completes a risk assessment without clearing confirmation context", + "reducer": "chat", + "initial": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "tc-1", + "toolName": "bash", + "displayName": "Run Command", + "intention": null, + "contributor": null, + "_meta": null, + "invocationMessage": "Checking command safety", + "toolInput": "rm -rf /tmp/test", + "confirmationTitle": "Run in terminal", + "riskAssessment": { + "kind": "judge", + "status": "loading" + }, + "edits": { + "items": [] + }, + "editable": true, + "options": [ + { + "id": "approve", + "label": "Approve", + "kind": "approve" + } + ] + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" + }, + "actions": [ + { + "type": "chat/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-1", + "invocationMessage": "Run: rm -rf /tmp/test", + "riskAssessment": { + "kind": "judge", + "status": "complete", + "reason": "The command deletes files.", + "safety": 0.0 + } + } + ], + "expected": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "tc-1", + "toolName": "bash", + "displayName": "Run Command", + "intention": null, + "contributor": null, + "_meta": null, + "invocationMessage": "Run: rm -rf /tmp/test", + "toolInput": "rm -rf /tmp/test", + "confirmationTitle": "Run in terminal", + "riskAssessment": { + "kind": "judge", + "status": "complete", + "reason": "The command deletes files.", + "safety": 0.0 + }, + "edits": { + "items": [] + }, + "editable": true, + "options": [ + { + "id": "approve", + "label": "Approve", + "kind": "approve" + } + ] + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 24, + "modifiedAt": "1970-01-01T00:00:02.000Z" + } +} diff --git a/types/test-cases/reducers/243-toolcallready-ignores-finished-tool-calls.json b/types/test-cases/reducers/243-toolcallready-ignores-finished-tool-calls.json new file mode 100644 index 00000000..026a8b53 --- /dev/null +++ b/types/test-cases/reducers/243-toolcallready-ignores-finished-tool-calls.json @@ -0,0 +1,106 @@ +{ + "description": "toolCallReady ignores completed and cancelled tool calls", + "reducer": "chat", + "initial": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "completed", + "toolCallId": "tc-completed", + "toolName": "bash", + "displayName": "Run Command", + "invocationMessage": "Run", + "confirmed": "user-action", + "success": true, + "pastTenseMessage": "Ran command" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "cancelled", + "toolCallId": "tc-cancelled", + "toolName": "write_file", + "displayName": "Write File", + "invocationMessage": "Write file", + "reason": "denied" + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" + }, + "actions": [ + { + "type": "chat/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-completed", + "invocationMessage": "Late completed update" + }, + { + "type": "chat/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-cancelled", + "invocationMessage": "Late cancelled update" + } + ], + "expected": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "completed", + "toolCallId": "tc-completed", + "toolName": "bash", + "displayName": "Run Command", + "invocationMessage": "Run", + "confirmed": "user-action", + "success": true, + "pastTenseMessage": "Ran command" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "cancelled", + "toolCallId": "tc-cancelled", + "toolName": "write_file", + "displayName": "Write File", + "invocationMessage": "Write file", + "reason": "denied" + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" + } +}