Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions clients/go/ahp/reducers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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
})
Expand Down
2 changes: 2 additions & 0 deletions clients/go/ahptypes/actions.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions clients/go/ahptypes/state.generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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(
/**
Expand Down Expand Up @@ -5163,6 +5204,55 @@ internal object ToolCallContributorSerializer : KSerializer<ToolCallContributor>
}
}

@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<ToolCallRiskAssessment> {
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

Expand Down
6 changes: 5 additions & 1 deletion clients/rust/crates/ahp-types/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -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<StringOrMarkdown>,
/// Risk assessment that informed the confirmation requirement.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risk_assessment: Option<ToolCallRiskAssessment>,
/// File edits that this tool call will perform, for preview before confirmation
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edits: Option<AnyValue>,
Expand Down
Loading
Loading