diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 978608c97..f912928ec 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -91,6 +91,7 @@ type toolCallCommon struct { name string displayName string intention *string + toolInput *ahptypes.ToolInput contributor *ahptypes.ToolCallContributor meta ahptypes.JSONObject } @@ -98,44 +99,43 @@ type toolCallCommon struct { func toolCallMeta(tc ahptypes.ToolCallState) toolCallCommon { switch v := tc.Value.(type) { case *ahptypes.ToolCallStreamingState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, nil, v.Contributor, v.Meta} case *ahptypes.ToolCallPendingConfirmationState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} case *ahptypes.ToolCallRunningState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} case *ahptypes.ToolCallAuthRequiredState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} case *ahptypes.ToolCallPendingResultConfirmationState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} case *ahptypes.ToolCallCompletedState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} case *ahptypes.ToolCallCancelledState: - return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.Contributor, v.Meta} + return toolCallCommon{v.ToolCallId, v.ToolName, v.DisplayName, v.Intention, v.ToolInput, v.Contributor, v.Meta} } return toolCallCommon{} } -// toolCallInvocationAndInput pulls the optional invocationMessage and -// toolInput fields out of whichever non-terminal variant tc points to. -// Returns (StringOrMarkdown{}, nil) for variants that don't carry them. -func toolCallInvocationAndInput(tc ahptypes.ToolCallState) (ahptypes.StringOrMarkdown, *string) { +// toolCallInvocationMessage pulls the invocation message from a non-terminal +// tool-call variant. +func toolCallInvocationMessage(tc ahptypes.ToolCallState) ahptypes.StringOrMarkdown { switch v := tc.Value.(type) { case *ahptypes.ToolCallStreamingState: var im ahptypes.StringOrMarkdown if v.InvocationMessage != nil { im = *v.InvocationMessage } - return im, nil + return im case *ahptypes.ToolCallPendingConfirmationState: - return v.InvocationMessage, v.ToolInput + return v.InvocationMessage case *ahptypes.ToolCallRunningState: - return v.InvocationMessage, v.ToolInput + return v.InvocationMessage case *ahptypes.ToolCallAuthRequiredState: - return v.InvocationMessage, v.ToolInput + return v.InvocationMessage case *ahptypes.ToolCallPendingResultConfirmationState: - return v.InvocationMessage, v.ToolInput + return v.InvocationMessage } - return ahptypes.StringOrMarkdown{}, nil + return ahptypes.StringOrMarkdown{} } func toolCallID(tc ahptypes.ToolCallState) string { @@ -221,17 +221,17 @@ func endTurn(state *ahptypes.ChatState, turnID string, duration int64, turnState continue } common := toolCallMeta(tc.ToolCall) - invocation, toolInput := toolCallInvocationAndInput(tc.ToolCall) + invocation := toolCallInvocationMessage(tc.ToolCall) cancelled := &ahptypes.ToolCallCancelledState{ Status: ahptypes.ToolCallStatusCancelled, ToolCallId: common.id, ToolName: common.name, DisplayName: common.displayName, Intention: common.intention, + ToolInput: common.toolInput, Contributor: common.contributor, Meta: common.meta, InvocationMessage: invocation, - ToolInput: toolInput, Reason: ahptypes.ToolCallCancellationReasonSkipped, } parts = append(parts, ahptypes.ResponsePart{Value: &ahptypes.ToolCallResponsePart{ @@ -1026,18 +1026,20 @@ func applyToolCallDelta(state *ahptypes.ChatState, a *ahptypes.ChatToolCallDelta if !ok { return tc } - current := "" - if s.PartialInput != nil { - current = *s.PartialInput - } - joined := current + a.Content - s.PartialInput = &joined if a.Meta != nil { s.Meta = a.Meta } + if a.Content != nil { + partial := "" + if s.PartialInput != nil { + partial = *s.PartialInput + } + partial += *a.Content + s.PartialInput = &partial + } if a.InvocationMessage != nil { - im := *a.InvocationMessage - s.InvocationMessage = &im + invocationMessage := *a.InvocationMessage + s.InvocationMessage = &invocationMessage } return tc }) @@ -1067,6 +1069,9 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady if a.Intention != nil { common.intention = a.Intention } + if a.ToolInput != nil { + common.toolInput = a.ToolInput + } common.contributor = refineToolCallContributor(common.contributor, a.Contributor) if a.Meta != nil { common.meta = a.Meta @@ -1086,10 +1091,10 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady ToolName: common.name, DisplayName: common.displayName, Intention: common.intention, + ToolInput: common.toolInput, Contributor: common.contributor, Meta: common.meta, InvocationMessage: a.InvocationMessage, - ToolInput: a.ToolInput, Confirmed: *a.Confirmed, }} } @@ -1099,10 +1104,10 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady ToolName: common.name, DisplayName: common.displayName, Intention: common.intention, + ToolInput: common.toolInput, Contributor: common.contributor, Meta: common.meta, InvocationMessage: a.InvocationMessage, - ToolInput: a.ToolInput, ConfirmationTitle: a.ConfirmationTitle, RiskAssessment: a.RiskAssessment, Edits: a.Edits, @@ -1110,9 +1115,6 @@ func applyToolCallReady(state *ahptypes.ChatState, a *ahptypes.ChatToolCallReady Options: a.Options, } if pending != nil { - if next.ToolInput == nil { - next.ToolInput = pending.ToolInput - } if next.ConfirmationTitle == nil { next.ConfirmationTitle = pending.ConfirmationTitle } @@ -1157,8 +1159,9 @@ func applyToolCallConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallC selected := resolveSelectedOption(s.Options, a.SelectedOptionId) if a.Approved { toolInput := s.ToolInput - if a.EditedToolInput != nil { - toolInput = a.EditedToolInput + if a.EditedToolInput != nil && toolInput != nil && toolInput.Inline != nil { + inline := *a.EditedToolInput + toolInput = &ahptypes.ToolInput{Inline: &inline} } meta := s.Meta if a.Meta != nil { @@ -1174,10 +1177,10 @@ func applyToolCallConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallC ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: toolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: toolInput, Confirmed: confirmed, SelectedOption: selected, }} @@ -1196,10 +1199,10 @@ func applyToolCallConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToolCallC ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: s.ToolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: s.ToolInput, Reason: reason, ReasonMessage: a.ReasonMessage, UserSuggestion: a.UserSuggestion, @@ -1216,7 +1219,7 @@ func applyToolCallComplete(state *ahptypes.ChatState, a *ahptypes.ChatToolCallCo } var ( invocation ahptypes.StringOrMarkdown - toolInput *string + toolInput *ahptypes.ToolInput confirmed = ahptypes.ToolCallConfirmationReasonNotNeeded selectedOption *ahptypes.ConfirmationOption preAuthContent []ahptypes.ToolResultContent @@ -1266,10 +1269,10 @@ func applyToolCallComplete(state *ahptypes.ChatState, a *ahptypes.ChatToolCallCo ToolName: common.name, DisplayName: common.displayName, Intention: common.intention, + ToolInput: toolInput, Contributor: common.contributor, Meta: common.meta, InvocationMessage: invocation, - ToolInput: toolInput, Success: a.Result.Success, PastTenseMessage: a.Result.PastTenseMessage, Content: append([]ahptypes.ToolResultContent(nil), content...), @@ -1285,10 +1288,10 @@ func applyToolCallComplete(state *ahptypes.ChatState, a *ahptypes.ChatToolCallCo ToolName: common.name, DisplayName: common.displayName, Intention: common.intention, + ToolInput: toolInput, Contributor: common.contributor, Meta: common.meta, InvocationMessage: invocation, - ToolInput: toolInput, Success: a.Result.Success, PastTenseMessage: a.Result.PastTenseMessage, Content: append([]ahptypes.ToolResultContent(nil), content...), @@ -1317,10 +1320,10 @@ func applyToolCallResultConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToo ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: s.ToolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: s.ToolInput, Success: s.Success, PastTenseMessage: s.PastTenseMessage, Content: s.Content, @@ -1340,10 +1343,10 @@ func applyToolCallResultConfirmed(state *ahptypes.ChatState, a *ahptypes.ChatToo ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: s.ToolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: s.ToolInput, Reason: ahptypes.ToolCallCancellationReasonResultDenied, SelectedOption: s.SelectedOption, }} @@ -1372,10 +1375,10 @@ func applyToolCallAuthRequired(state *ahptypes.ChatState, a *ahptypes.ChatToolCa ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: s.ToolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: s.ToolInput, Confirmed: s.Confirmed, SelectedOption: s.SelectedOption, Auth: a.Auth, @@ -1400,10 +1403,10 @@ func applyToolCallAuthResolved(state *ahptypes.ChatState, a *ahptypes.ChatToolCa ToolName: s.ToolName, DisplayName: s.DisplayName, Intention: s.Intention, + ToolInput: s.ToolInput, Contributor: s.Contributor, Meta: meta, InvocationMessage: s.InvocationMessage, - ToolInput: s.ToolInput, Confirmed: s.Confirmed, SelectedOption: s.SelectedOption, Content: append([]ahptypes.ToolResultContent(nil), s.Content...), diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 4071bd50a..fccdb904b 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -318,8 +318,8 @@ type ChatToolCallDeltaAction struct { // contain escape sequences). Meta map[string]json.RawMessage `json:"_meta,omitempty"` Type ActionType `json:"type"` - // Partial parameter content to append - Content string `json:"content"` + // Partial parameter content to append, if provided by the host. + Content *string `json:"content,omitempty"` // Updated progress message InvocationMessage *StringOrMarkdown `json:"invocationMessage,omitempty"` } @@ -358,8 +358,8 @@ type ChatToolCallReadyAction struct { Intention *string `json:"intention,omitempty"` // Message describing what the tool will do or what confirmation is needed InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input + ToolInput *ToolInput `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. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index e0618669b..d800edbe9 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1920,7 +1920,7 @@ type ToolCallStreamingState struct { // with the {@link contributor} to serve MCP Apps. Meta map[string]json.RawMessage `json:"_meta,omitempty"` Status ToolCallStatus `json:"status"` - // Partial parameters accumulated so far + // Partial parameters accumulated from tool-call deltas. PartialInput *string `json:"partialInput,omitempty"` // Progress message shown while parameters are streaming InvocationMessage *StringOrMarkdown `json:"invocationMessage,omitempty"` @@ -1947,8 +1947,13 @@ type ToolCallPendingConfirmationState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` Status ToolCallStatus `json:"status"` // Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) ConfirmationTitle *StringOrMarkdown `json:"confirmationTitle,omitempty"` @@ -1985,8 +1990,13 @@ type ToolCallRunningState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` // How the tool was confirmed for execution Confirmed ToolCallConfirmationReason `json:"confirmed"` // The confirmation option the user selected, if confirmation options were provided @@ -2045,8 +2055,13 @@ type ToolCallAuthRequiredState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` // How the tool was confirmed for execution Confirmed ToolCallConfirmationReason `json:"confirmed"` // The confirmation option the user selected, if confirmation options were provided @@ -2078,8 +2093,13 @@ type ToolCallPendingResultConfirmationState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` // Whether the tool succeeded Success bool `json:"success"` // Past-tense description of what the tool did @@ -2121,8 +2141,13 @@ type ToolCallCompletedState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` // Whether the tool succeeded Success bool `json:"success"` // Past-tense description of what the tool did @@ -2164,8 +2189,13 @@ type ToolCallCancelledState struct { Meta map[string]json.RawMessage `json:"_meta,omitempty"` // Message describing what the tool will do InvocationMessage StringOrMarkdown `json:"invocationMessage"` - // Raw tool input - ToolInput *string `json:"toolInput,omitempty"` + // Final tool input. + // + // Referenced input is mutable until the tool call leaves + // `pending-confirmation`. When the client confirms with `editedToolInput`, + // the host MUST replace the resource contents before echoing the accepted + // confirmation action. Clients MUST NOT cache tool input across confirmation. + ToolInput *ToolInput `json:"toolInput,omitempty"` Status ToolCallStatus `json:"status"` // Why the tool was cancelled Reason ToolCallCancellationReason `json:"reason"` @@ -3519,6 +3549,37 @@ type ResourceChange struct { Type ResourceChangeType `json:"type"` } +// ToolInput is raw tool input represented inline or by content reference. +type ToolInput struct { + Inline *string + ContentRef *ContentRef +} + +func (t ToolInput) MarshalJSON() ([]byte, error) { + if t.Inline != nil { + return json.Marshal(*t.Inline) + } + if t.ContentRef != nil { + return json.Marshal(t.ContentRef) + } + return []byte("null"), nil +} + +func (t *ToolInput) UnmarshalJSON(data []byte) error { + *t = ToolInput{} + var inline string + if err := json.Unmarshal(data, &inline); err == nil { + t.Inline = &inline + return nil + } + var ref ContentRef + if err := json.Unmarshal(data, &ref); err != nil { + return err + } + t.ContentRef = &ref + return nil +} + // ─── Discriminated Unions ───────────────────────────────────────────── // ResponsePart is a single part of a response stream (text, tool call, reasoning, content reference). @@ -4888,7 +4949,7 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { } // SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, or annotations state. The active +// chat, terminal, changeset, resource-watch, annotations, or content state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order // (session → chat → terminal → changeset → resourceWatch → annotations → root). 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 b62d7a617..d1579ce14 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -70,6 +70,7 @@ public object ResourceWatchReducer : Reducer { resourceWatchReducer(state, action) } + // ─── Timestamp Provider ───────────────────────────────────────────────────── /** @@ -165,6 +166,7 @@ private data class ToolCallBase( val toolName: String, val displayName: String, val intention: String?, + val toolInput: ToolInput?, val contributor: ToolCallContributor?, val meta: Map?, ) { @@ -188,31 +190,31 @@ private fun refineToolCallContributor( private fun toolCallBase(tc: ToolCallState): ToolCallBase = when (tc) { is ToolCallStateStreaming -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, null, it.contributor, it.meta) } is ToolCallStatePendingConfirmation -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } is ToolCallStateRunning -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } is ToolCallStateAuthRequired -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } is ToolCallStatePendingResultConfirmation -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } is ToolCallStateCompleted -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } is ToolCallStateCancelled -> tc.value.let { - ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.contributor, it.meta) + ToolCallBase(it.toolCallId, it.toolName, it.displayName, it.intention, it.toolInput, it.contributor, it.meta) } // Forward-compat: unknown lifecycle variants have no extractable base; mirror // Rust's `ToolCallState::Unknown(_) => (String::new(), ...)`. Combined with // `toolCallIdOf` returning `""`, this guarantees an unknown tool call never // matches a real `toolCallId` in delta/lookup paths. - is ToolCallStateUnknown -> ToolCallBase("", "", "", null, null, null) + is ToolCallStateUnknown -> ToolCallBase("", "", "", null, null, null, null) } /** Resolves a selected confirmation option by ID from a pending-confirmation state. */ @@ -389,15 +391,6 @@ private fun endTurn( // invocation message — destructive, but matches Rust parity exactly. is ToolCallStateUnknown -> com.microsoft.agenthostprotocol.generated.StringOrMarkdown.Plain("") } - val toolInput: String? = when (tc) { - is ToolCallStateStreaming -> null - is ToolCallStatePendingConfirmation -> tc.value.toolInput - is ToolCallStateRunning -> tc.value.toolInput - is ToolCallStateAuthRequired -> tc.value.toolInput - is ToolCallStatePendingResultConfirmation -> tc.value.toolInput - is ToolCallStateCompleted, is ToolCallStateCancelled -> error("filtered above") - is ToolCallStateUnknown -> null - } ResponsePartToolCall( part.value.copy( toolCall = ToolCallStateCancelled( @@ -406,10 +399,10 @@ private fun endTurn( toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = base.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, - toolInput = toolInput, status = ToolCallStatus.CANCELLED, reason = ToolCallCancellationReason.SKIPPED, ), @@ -935,7 +928,8 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when ToolCallStateStreaming( tc.value.copy( meta = a.meta ?: tc.value.meta, - partialInput = (tc.value.partialInput ?: "") + a.content, + partialInput = a.content?.let { (tc.value.partialInput ?: "") + it } + ?: tc.value.partialInput, invocationMessage = a.invocationMessage ?: tc.value.invocationMessage, ), ) @@ -957,6 +951,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val initialBase = toolCallBase(tc) val base = initialBase.copy( intention = a.intention ?: initialBase.intention, + toolInput = a.toolInput ?: initialBase.toolInput, contributor = refineToolCallContributor(initialBase.contributor, a.contributor), ).withMeta(a.meta) val pending = (tc as? ToolCallStatePendingConfirmation)?.value @@ -967,10 +962,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = base.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = a.invocationMessage, - toolInput = a.toolInput, status = ToolCallStatus.RUNNING, confirmed = a.confirmed, ), @@ -982,10 +977,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = base.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = a.invocationMessage, - toolInput = a.toolInput ?: pending?.toolInput, status = ToolCallStatus.PENDING_CONFIRMATION, confirmationTitle = a.confirmationTitle ?: pending?.confirmationTitle, riskAssessment = a.riskAssessment ?: pending?.riskAssessment, @@ -1008,16 +1003,24 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val base = toolCallBase(tc).withMeta(a.meta) val selectedOption = resolveSelectedOption(tc.value.options, a.selectedOptionId) if (a.approved) { + val toolInput = if ( + a.editedToolInput != null + && tc.value.toolInput is ToolInput.Inline + ) { + ToolInput.Inline(a.editedToolInput) + } else { + tc.value.toolInput + } ToolCallStateRunning( ToolCallRunningState( toolCallId = base.toolCallId, toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = a.editedToolInput ?: tc.value.toolInput, status = ToolCallStatus.RUNNING, confirmed = a.confirmed ?: ToolCallConfirmationReason.USER_ACTION, @@ -1031,10 +1034,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = tc.value.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = tc.value.toolInput, status = ToolCallStatus.CANCELLED, reason = a.reason ?: ToolCallCancellationReason.DENIED, reasonMessage = a.reasonMessage, @@ -1102,10 +1105,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, - toolInput = toolInput, success = result.success, pastTenseMessage = result.pastTenseMessage, content = content, @@ -1123,10 +1126,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = invocationMessage, - toolInput = toolInput, success = result.success, pastTenseMessage = result.pastTenseMessage, content = content, @@ -1155,10 +1158,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = tc.value.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = tc.value.toolInput, success = tc.value.success, pastTenseMessage = tc.value.pastTenseMessage, content = tc.value.content, @@ -1176,10 +1179,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = tc.value.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = tc.value.toolInput, status = ToolCallStatus.CANCELLED, reason = ToolCallCancellationReason.RESULT_DENIED, selectedOption = tc.value.selectedOption, @@ -1218,10 +1221,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = tc.value.toolInput, contributor = contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = tc.value.toolInput, confirmed = tc.value.confirmed, selectedOption = tc.value.selectedOption, status = ToolCallStatus.AUTH_REQUIRED, @@ -1249,10 +1252,10 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when toolName = base.toolName, displayName = base.displayName, intention = base.intention, + toolInput = tc.value.toolInput, contributor = base.contributor, meta = base.meta, invocationMessage = tc.value.invocationMessage, - toolInput = tc.value.toolInput, confirmed = tc.value.confirmed, selectedOption = tc.value.selectedOption, status = ToolCallStatus.RUNNING, @@ -1451,7 +1454,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when */ private data class CompleteCtx( val invocationMessage: com.microsoft.agenthostprotocol.generated.StringOrMarkdown, - val toolInput: String?, + val toolInput: ToolInput?, val confirmed: ToolCallConfirmationReason, val selectedOption: ConfirmationOption?, val preAuthContent: List?, 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 f696130b9..cde9f9ede 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 @@ -441,9 +441,9 @@ data class ChatToolCallDeltaAction( val meta: Map? = null, val type: ActionType, /** - * Partial parameter content to append + * Partial parameter content to append, if provided by the host. */ - val content: String, + val content: String? = null, /** * Updated progress message */ @@ -486,9 +486,9 @@ data class ChatToolCallReadyAction( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, /** * Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ 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 840fe3318..eae16d99c 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 @@ -2569,7 +2569,7 @@ data class ToolCallStreamingState( val meta: Map? = null, val status: ToolCallStatus, /** - * Partial parameters accumulated so far + * Partial parameters accumulated from tool-call deltas. */ val partialInput: String? = null, /** @@ -2614,9 +2614,14 @@ data class ToolCallPendingConfirmationState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, val status: ToolCallStatus, /** * Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) @@ -2679,9 +2684,14 @@ data class ToolCallRunningState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, /** * How the tool was confirmed for execution */ @@ -2736,9 +2746,14 @@ data class ToolCallAuthRequiredState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, /** * How the tool was confirmed for execution */ @@ -2794,9 +2809,14 @@ data class ToolCallPendingResultConfirmationState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, /** * Whether the tool succeeded */ @@ -2868,9 +2888,14 @@ data class ToolCallCompletedState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, /** * Whether the tool succeeded */ @@ -2942,9 +2967,14 @@ data class ToolCallCancelledState( */ val invocationMessage: StringOrMarkdown, /** - * Raw tool input + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. */ - val toolInput: String? = null, + val toolInput: ToolInput? = null, val status: ToolCallStatus, /** * Why the tool was cancelled @@ -4629,6 +4659,47 @@ data class ResourceChange( val type: ResourceChangeType ) +// ─── Tool Input ────────────────────────────────────────────────────────────── + +/** + * Raw tool input represented inline or by content reference. + */ +@Serializable(with = ToolInputSerializer::class) +sealed interface ToolInput { + @JvmInline value class Inline(val value: String) : ToolInput + @JvmInline value class ContentReference(val value: ContentRef) : ToolInput +} + +internal object ToolInputSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("ToolInput") + + override fun deserialize(decoder: Decoder): ToolInput { + val input = decoder as? JsonDecoder + ?: error("ToolInput can only be deserialized from JSON") + return when (val element = input.decodeJsonElement()) { + is JsonPrimitive -> ToolInput.Inline( + element.contentOrNull ?: error("ToolInput string must not be null"), + ) + is JsonObject -> ToolInput.ContentReference( + input.json.decodeFromJsonElement(ContentRef.serializer(), element), + ) + else -> error("ToolInput must be a string or ContentRef object") + } + } + + override fun serialize(encoder: Encoder, value: ToolInput) { + val output = encoder as? JsonEncoder + ?: error("ToolInput can only be serialized to JSON") + val element = when (value) { + is ToolInput.Inline -> JsonPrimitive(value.value) + is ToolInput.ContentReference -> + output.json.encodeToJsonElement(ContentRef.serializer(), value.value) + } + output.encodeJsonElement(element) + } +} + // ─── Discriminated Unions ─────────────────────────────────────────────────── @Serializable(with = ChatOriginSerializer::class) @@ -5661,7 +5732,7 @@ internal object ToolResultContentSerializer : KSerializer { /** * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, or annotations state. + * resource-watch, annotations, or content state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt index 37cf486bc..a908a84c4 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/FixtureDrivenReducerTest.kt @@ -216,6 +216,7 @@ class FixtureDrivenReducerTest { }, ) + else -> fail("${file.name}: unsupported reducer '$reducer'") } } diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 8cd2c8f45..849a339c5 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -16,11 +16,11 @@ use crate::state::{ AgentInfo, AgentSelection, Annotation, AnnotationEntry, Changeset, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, ChangesetStatus, ChatInputAnswer, ChatInputRequest, ChatInputResponseKind, ChatInteractivity, ChatOrigin, ChatSummary, - ConfirmationOption, Customization, ErrorInfo, McpAuthRequirement, McpServerState, Message, - ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, SessionInputRequest, - SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallCancellationReason, - ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, - ToolDefinition, ToolResultContent, Turn, UsageInfo, + ConfirmationOption, ContentRef, Customization, ErrorInfo, McpAuthRequirement, McpServerState, + Message, ModelSelection, PendingMessageKind, ResponsePart, SessionActiveClient, + SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, + ToolCallCancellationReason, ToolCallConfirmationReason, ToolCallContributor, ToolCallResult, + ToolCallRiskAssessment, ToolDefinition, ToolInput, ToolResultContent, Turn, UsageInfo, }; // ─── ActionType ────────────────────────────────────────────────────── @@ -446,8 +446,9 @@ pub struct ChatToolCallDeltaAction { /// contain escape sequences). #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Partial parameter content to append - pub content: String, + /// Partial parameter content to append, if provided by the host. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub content: Option, /// Updated progress message #[serde(default, skip_serializing_if = "Option::is_none")] pub invocation_message: Option, @@ -491,9 +492,9 @@ pub struct ChatToolCallReadyAction { pub intention: Option, /// Message describing what the tool will do or what confirmation is needed pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// 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, diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index bd9787516..c4cef0026 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2361,7 +2361,7 @@ pub struct ToolCallStreamingState { /// with the {@link contributor} to serve MCP Apps. #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] pub meta: Option, - /// Partial parameters accumulated so far + /// Partial parameters accumulated from tool-call deltas. #[serde(default, skip_serializing_if = "Option::is_none")] pub partial_input: Option, /// Progress message shown while parameters are streaming @@ -2395,9 +2395,14 @@ pub struct ToolCallPendingConfirmationState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// 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, @@ -2443,9 +2448,14 @@ pub struct ToolCallRunningState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// How the tool was confirmed for execution pub confirmed: ToolCallConfirmationReason, /// The confirmation option the user selected, if confirmation options were provided @@ -2510,9 +2520,14 @@ pub struct ToolCallAuthRequiredState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// How the tool was confirmed for execution pub confirmed: ToolCallConfirmationReason, /// The confirmation option the user selected, if confirmation options were provided @@ -2551,9 +2566,14 @@ pub struct ToolCallPendingResultConfirmationState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// Whether the tool succeeded pub success: bool, /// Past-tense description of what the tool did @@ -2603,9 +2623,14 @@ pub struct ToolCallCompletedState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// Whether the tool succeeded pub success: bool, /// Past-tense description of what the tool did @@ -2655,9 +2680,14 @@ pub struct ToolCallCancelledState { pub meta: Option, /// Message describing what the tool will do pub invocation_message: StringOrMarkdown, - /// Raw tool input + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_input: Option, + pub tool_input: Option, /// Why the tool was cancelled pub reason: ToolCallCancellationReason, /// Optional message explaining the cancellation @@ -4220,6 +4250,14 @@ pub struct ResourceChange { pub r#type: ResourceChangeType, } +/// Raw tool input represented inline or by content reference. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ToolInput { + Inline(String), + ContentRef(ContentRef), +} + // ─── Discriminated Unions ───────────────────────────────────────────── /// How a chat came into existence. @@ -4576,7 +4614,7 @@ pub enum SessionInputRequest { } /// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, or annotations state. +/// changeset, resource-watch, annotations, or content state. /// /// Deserialized by trying session first (has required `lifecycle`), then /// chat (has required `turns`), then terminal (has required `content`), diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 59c62518b..a2cf5109a 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -67,7 +67,7 @@ use ahp_types::state::{ ToolCallCancellationReason, ToolCallCancelledState, ToolCallCompletedState, ToolCallConfirmationReason, ToolCallContributor, ToolCallPendingConfirmationState, ToolCallPendingResultConfirmationState, ToolCallResponsePart, ToolCallRunningState, - ToolCallState, ToolCallStatus, ToolCallStreamingState, Turn, TurnState, + ToolCallState, ToolCallStatus, ToolCallStreamingState, ToolInput, Turn, TurnState, }; /// What happened when an action was applied. @@ -133,6 +133,7 @@ struct ToolCallBase { tool_name: String, display_name: String, intention: Option, + tool_input: Option, contributor: Option, meta: Option>, } @@ -165,6 +166,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: None, contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -173,6 +175,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -181,6 +184,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -189,6 +193,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -197,6 +202,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -205,6 +211,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -213,6 +220,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: s.tool_name.clone(), display_name: s.display_name.clone(), intention: s.intention.clone(), + tool_input: s.tool_input.clone(), contributor: s.contributor.clone(), meta: s.meta.clone(), }, @@ -221,6 +229,7 @@ fn tool_call_meta(tc: &ToolCallState) -> ToolCallBase { tool_name: String::new(), display_name: String::new(), intention: None, + tool_input: None, contributor: None, meta: None, }, @@ -354,23 +363,15 @@ fn end_turn( } _ => Default::default(), }; - let tool_input = match &tc { - ToolCallState::Streaming(_) => None, - ToolCallState::PendingConfirmation(s) => s.tool_input.clone(), - ToolCallState::Running(s) => s.tool_input.clone(), - ToolCallState::AuthRequired(s) => s.tool_input.clone(), - ToolCallState::PendingResultConfirmation(s) => s.tool_input.clone(), - _ => None, - }; let cancelled = ToolCallCancelledState { tool_call_id: base.tool_call_id, tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input: base.tool_input, contributor: base.contributor, meta: base.meta, invocation_message, - tool_input, reason: ToolCallCancellationReason::Skipped, reason_message: None, user_suggestion: None, @@ -546,10 +547,10 @@ where tool_name: String::new(), display_name: String::new(), intention: None, + tool_input: None, contributor: None, meta: None, invocation_message: Default::default(), - tool_input: None, reason: ToolCallCancellationReason::Skipped, reason_message: None, user_suggestion: None, @@ -1239,13 +1240,16 @@ fn apply_turn_started(state: &mut ChatState, a: &ChatTurnStartedAction) -> Reduc fn apply_tool_call_delta(state: &mut ChatState, a: &ChatToolCallDeltaAction) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| match tc { ToolCallState::Streaming(mut s) => { - let current = s.partial_input.unwrap_or_default(); - s.partial_input = Some(current + &a.content); + if let Some(content) = &a.content { + let mut partial = s.partial_input.unwrap_or_default(); + partial.push_str(content); + s.partial_input = Some(partial); + } if let Some(meta) = &a.meta { s.meta = Some(meta.clone()); } - if let Some(im) = &a.invocation_message { - s.invocation_message = Some(im.clone()); + if let Some(invocation_message) = &a.invocation_message { + s.invocation_message = Some(invocation_message.clone()); } ToolCallState::Streaming(s) } @@ -1257,6 +1261,7 @@ fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let mut base = tool_call_meta(&tc); base.intention = a.intention.clone().or(base.intention); + base.tool_input = a.tool_input.clone().or(base.tool_input); base.contributor = refine_tool_call_contributor(base.contributor, a.contributor.clone()); let meta = a.meta.clone().or(base.meta); let pending = match &tc { @@ -1273,10 +1278,10 @@ fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input: base.tool_input, contributor: base.contributor, meta, invocation_message: a.invocation_message.clone(), - tool_input: a.tool_input.clone(), confirmed, selected_option: None, content: None, @@ -1287,13 +1292,10 @@ fn apply_tool_call_ready(state: &mut ChatState, a: &ChatToolCallReadyAction) -> tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input: base.tool_input, contributor: base.contributor, meta, invocation_message: a.invocation_message.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()) }), @@ -1348,15 +1350,19 @@ fn apply_tool_call_confirmed( let invocation_message = s.invocation_message; let tool_input = s.tool_input; if a.approved { + let tool_input = match (a.edited_tool_input.clone(), tool_input) { + (Some(edited), Some(ToolInput::Inline(_))) => Some(ToolInput::Inline(edited)), + (_, tool_input) => tool_input, + }; ToolCallState::Running(ToolCallRunningState { tool_call_id, tool_name, display_name, intention, + tool_input, contributor, meta, invocation_message, - tool_input: a.edited_tool_input.clone().or(tool_input), confirmed: a.confirmed.unwrap_or(ToolCallConfirmationReason::NotNeeded), selected_option, content: None, @@ -1367,10 +1373,10 @@ fn apply_tool_call_confirmed( tool_name, display_name, intention, + tool_input, contributor, meta, invocation_message, - tool_input, reason: a.reason.unwrap_or(ToolCallCancellationReason::Denied), reason_message: a.reason_message.clone(), user_suggestion: a.user_suggestion.clone(), @@ -1443,10 +1449,10 @@ fn apply_tool_call_complete( tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input, contributor: base.contributor, meta, invocation_message, - tool_input, success: a.result.success, past_tense_message: a.result.past_tense_message.clone(), content, @@ -1461,10 +1467,10 @@ fn apply_tool_call_complete( tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input, contributor: base.contributor, meta, invocation_message, - tool_input, success: a.result.success, past_tense_message: a.result.past_tense_message.clone(), content, @@ -1491,10 +1497,10 @@ fn apply_tool_call_result_confirmed( tool_name: s.tool_name, display_name: s.display_name, intention: s.intention, + tool_input: s.tool_input, contributor: s.contributor, meta: a.meta.clone().or(s.meta), invocation_message: s.invocation_message, - tool_input: s.tool_input, success: s.success, past_tense_message: s.past_tense_message, content: s.content, @@ -1509,10 +1515,10 @@ fn apply_tool_call_result_confirmed( tool_name: s.tool_name, display_name: s.display_name, intention: s.intention, + tool_input: s.tool_input, contributor: s.contributor, meta: a.meta.clone().or(s.meta), invocation_message: s.invocation_message, - tool_input: s.tool_input, reason: ToolCallCancellationReason::ResultDenied, reason_message: None, user_suggestion: None, @@ -1555,10 +1561,10 @@ fn apply_tool_call_auth_required( tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input: s.tool_input, contributor: s.contributor, meta, invocation_message: s.invocation_message, - tool_input: s.tool_input, confirmed: s.confirmed, selected_option: s.selected_option, status: ToolCallStatus::AuthRequired, @@ -1584,10 +1590,10 @@ fn apply_tool_call_auth_resolved( tool_name: base.tool_name, display_name: base.display_name, intention: base.intention, + tool_input: s.tool_input, contributor: s.contributor, meta, invocation_message: s.invocation_message, - tool_input: s.tool_input, confirmed: s.confirmed, selected_option: s.selected_option, content: s.content, diff --git a/clients/swift/AHPApp/AHPApp/Views/ChatView.swift b/clients/swift/AHPApp/AHPApp/Views/ChatView.swift index d20e9202d..060e4b9b1 100644 --- a/clients/swift/AHPApp/AHPApp/Views/ChatView.swift +++ b/clients/swift/AHPApp/AHPApp/Views/ChatView.swift @@ -1128,7 +1128,7 @@ private struct InputBarPreviewWrapper: View { toolName: "readFile", displayName: "Read file", invocationMessage: .string("Reading src/auth.swift"), - toolInput: "{\"path\": \"src/auth.swift\"}", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc1.json")), success: true, pastTenseMessage: .string("Read src/auth.swift"), status: .completed, @@ -1139,7 +1139,7 @@ private struct InputBarPreviewWrapper: View { toolName: "editFile", displayName: "Edit file", invocationMessage: .string("Editing src/auth.swift"), - toolInput: "{\"path\": \"src/auth.swift\"}", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc2.json")), success: true, pastTenseMessage: .string("Edited src/auth.swift"), status: .completed, @@ -1161,7 +1161,7 @@ private struct InputBarPreviewWrapper: View { toolName: "bash", displayName: "Run command", invocationMessage: .string("Run: npm run deploy --production"), - toolInput: "{\"command\": \"npm run deploy --production\"}", + toolInput: .inline("{\"command\": \"npm run deploy --production\"}"), status: .pendingConfirmation, confirmationTitle: .string("Allow production deployment?") ))) @@ -1176,7 +1176,7 @@ private struct InputBarPreviewWrapper: View { toolName: "readFile", displayName: "Read file", invocationMessage: .string("Reading src/auth/token.swift"), - toolInput: "{\"path\": \"src/auth/token.swift\"}", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc4.json")), status: .running, confirmed: .notNeeded ))) diff --git a/clients/swift/AHPApp/AHPApp/Views/ResponsePartView.swift b/clients/swift/AHPApp/AHPApp/Views/ResponsePartView.swift index 9fcecc951..fd59358dd 100644 --- a/clients/swift/AHPApp/AHPApp/Views/ResponsePartView.swift +++ b/clients/swift/AHPApp/AHPApp/Views/ResponsePartView.swift @@ -2,6 +2,14 @@ import AgentHostProtocol import SwiftUI import UIKit +private func toolInputText(_ input: ToolInput?) -> String? { + switch input { + case .inline(let value): return value + case .contentRef(let ref): return ref.uri + case nil: return nil + } +} + /// Renders a single response part: markdown text, reasoning, tool call, or content ref. struct ResponsePartView: View { let part: ResponsePart @@ -439,11 +447,11 @@ struct ToolCallPartView: View { private var toolInput: String? { switch toolCall { - case .streaming(let s): return s.partialInput - case .pendingConfirmation(let s): return s.toolInput - case .running(let s): return s.toolInput - case .pendingResultConfirmation(let s): return s.toolInput - case .completed(let s): return s.toolInput + case .pendingConfirmation(let s): return toolInputText(s.toolInput) + case .running(let s): return toolInputText(s.toolInput) + case .pendingResultConfirmation(let s): return toolInputText(s.toolInput) + case .completed(let s): return toolInputText(s.toolInput) + case .cancelled(let s): return toolInputText(s.toolInput) default: return nil } } @@ -576,11 +584,11 @@ struct ToolCallDetailSheet: View { private var toolInput: String? { switch toolCall { - case .streaming(let s): return s.partialInput - case .pendingConfirmation(let s): return s.toolInput - case .running(let s): return s.toolInput - case .pendingResultConfirmation(let s): return s.toolInput - case .completed(let s): return s.toolInput + case .pendingConfirmation(let s): return toolInputText(s.toolInput) + case .running(let s): return toolInputText(s.toolInput) + case .pendingResultConfirmation(let s): return toolInputText(s.toolInput) + case .completed(let s): return toolInputText(s.toolInput) + case .cancelled(let s): return toolInputText(s.toolInput) default: return nil } } @@ -796,8 +804,8 @@ struct ContentRefView: View { toolCallId: "tc0", toolName: "editFile", displayName: "Edit file", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc0.json")), status: .streaming, - partialInput: "{\"path\": \"src/main.ts\", \"content\": \"...", invocationMessage: .string("Editing src/main.ts") ))) @@ -808,7 +816,7 @@ struct ContentRefView: View { toolName: "bash", displayName: "Run command", invocationMessage: .string("Run: npm run deploy"), - toolInput: "{\"command\": \"npm run deploy\"}", + toolInput: .inline("{\"command\": \"npm run deploy\"}"), status: .pendingConfirmation, confirmationTitle: .string("Allow deployment?") ))) @@ -820,7 +828,7 @@ struct ContentRefView: View { toolName: "bash", displayName: "Run command", invocationMessage: .string("Running: npm test"), - toolInput: "{\"command\": \"npm test\"}", + toolInput: .inline("{\"command\": \"npm test\"}"), status: .running, confirmed: .notNeeded ))) @@ -832,7 +840,7 @@ struct ContentRefView: View { toolName: "readFile", displayName: "Read file", invocationMessage: .string("Reading package.json"), - toolInput: "{\"path\": \"package.json\"}", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc2.json")), success: true, pastTenseMessage: .string("Read package.json"), content: [.text(ToolResultTextContent(type: .text, text: "{\"name\": \"my-app\"}"))], @@ -847,7 +855,7 @@ struct ContentRefView: View { toolName: "bash", displayName: "Run command", invocationMessage: .string("Running: rm -rf /"), - toolInput: "{\"command\": \"rm -rf /\"}", + toolInput: .inline("{\"command\": \"rm -rf /\"}"), success: false, pastTenseMessage: .string("Command failed"), status: .completed, @@ -861,7 +869,7 @@ struct ContentRefView: View { toolName: "writeFile", displayName: "Write file", invocationMessage: .string("Writing config.json"), - toolInput: "{\"path\": \"config.json\"}", + toolInput: .contentRef(ContentRef(uri: "file:///tool-inputs/tc4.json")), success: true, pastTenseMessage: .string("Wrote config.json"), content: [.text(ToolResultTextContent(type: .text, text: "File written successfully"))], @@ -876,7 +884,7 @@ struct ContentRefView: View { toolName: "bash", displayName: "Run command", invocationMessage: .string("Running: git push --force"), - toolInput: "{\"command\": \"git push --force\"}", + toolInput: .inline("{\"command\": \"git push --force\"}"), status: .cancelled, reason: .denied, reasonMessage: .string("User denied force push") diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 809dd13b8..da101cfd4 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -436,8 +436,8 @@ public struct ChatToolCallDeltaAction: Codable, Sendable { /// contain escape sequences). public var meta: [String: AnyCodable]? public var type: ActionType - /// Partial parameter content to append - public var content: String + /// Partial parameter content to append, if provided by the host. + public var content: String? /// Updated progress message public var invocationMessage: StringOrMarkdown? @@ -455,7 +455,7 @@ public struct ChatToolCallDeltaAction: Codable, Sendable { toolCallId: String, meta: [String: AnyCodable]? = nil, type: ActionType, - content: String, + content: String? = nil, invocationMessage: StringOrMarkdown? = nil ) { self.turnId = turnId @@ -488,8 +488,8 @@ public struct ChatToolCallReadyAction: Codable, Sendable { public var intention: String? /// Message describing what the tool will do or what confirmation is needed public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input + public var toolInput: ToolInput? /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) public var confirmationTitle: StringOrMarkdown? /// Risk assessment that informed the confirmation requirement. @@ -531,7 +531,7 @@ public struct ChatToolCallReadyAction: Codable, Sendable { contributor: ToolCallContributor? = nil, intention: String? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, confirmationTitle: StringOrMarkdown? = nil, riskAssessment: ToolCallRiskAssessment? = nil, edits: AnyCodable? = nil, diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 515f9c4a1..ffa760e89 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -2576,7 +2576,7 @@ public struct ToolCallStreamingState: Codable, Sendable { /// with the {@link contributor} to serve MCP Apps. public var meta: [String: AnyCodable]? public var status: ToolCallStatus - /// Partial parameters accumulated so far + /// Partial parameters accumulated from tool-call deltas. public var partialInput: String? /// Progress message shown while parameters are streaming public var invocationMessage: StringOrMarkdown? @@ -2635,8 +2635,13 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? public var status: ToolCallStatus /// Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) public var confirmationTitle: StringOrMarkdown? @@ -2677,7 +2682,7 @@ public struct ToolCallPendingConfirmationState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, status: ToolCallStatus, confirmationTitle: StringOrMarkdown? = nil, riskAssessment: ToolCallRiskAssessment? = nil, @@ -2721,8 +2726,13 @@ public struct ToolCallRunningState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? /// How the tool was confirmed for execution public var confirmed: ToolCallConfirmationReason /// The confirmation option the user selected, if confirmation options were provided @@ -2757,7 +2767,7 @@ public struct ToolCallRunningState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, confirmed: ToolCallConfirmationReason, selectedOption: ConfirmationOption? = nil, status: ToolCallStatus, @@ -2797,8 +2807,13 @@ public struct ToolCallAuthRequiredState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? /// How the tool was confirmed for execution public var confirmed: ToolCallConfirmationReason /// The confirmation option the user selected, if confirmation options were provided @@ -2833,7 +2848,7 @@ public struct ToolCallAuthRequiredState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, confirmed: ToolCallConfirmationReason, selectedOption: ConfirmationOption? = nil, status: ToolCallStatus, @@ -2875,8 +2890,13 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? /// Whether the tool succeeded public var success: Bool /// Past-tense description of what the tool did @@ -2924,7 +2944,7 @@ public struct ToolCallPendingResultConfirmationState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, success: Bool, pastTenseMessage: StringOrMarkdown, content: [ToolResultContent]? = nil, @@ -2972,8 +2992,13 @@ public struct ToolCallCompletedState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? /// Whether the tool succeeded public var success: Bool /// Past-tense description of what the tool did @@ -3021,7 +3046,7 @@ public struct ToolCallCompletedState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, success: Bool, pastTenseMessage: StringOrMarkdown, content: [ToolResultContent]? = nil, @@ -3069,8 +3094,13 @@ public struct ToolCallCancelledState: Codable, Sendable { public var meta: [String: AnyCodable]? /// Message describing what the tool will do public var invocationMessage: StringOrMarkdown - /// Raw tool input - public var toolInput: String? + /// Final tool input. + /// + /// Referenced input is mutable until the tool call leaves + /// `pending-confirmation`. When the client confirms with `editedToolInput`, + /// the host MUST replace the resource contents before echoing the accepted + /// confirmation action. Clients MUST NOT cache tool input across confirmation. + public var toolInput: ToolInput? public var status: ToolCallStatus /// Why the tool was cancelled public var reason: ToolCallCancellationReason @@ -3105,7 +3135,7 @@ public struct ToolCallCancelledState: Codable, Sendable { contributor: ToolCallContributor? = nil, meta: [String: AnyCodable]? = nil, invocationMessage: StringOrMarkdown, - toolInput: String? = nil, + toolInput: ToolInput? = nil, status: ToolCallStatus, reason: ToolCallCancellationReason, reasonMessage: StringOrMarkdown? = nil, @@ -5194,6 +5224,31 @@ public struct ResourceChange: Codable, Sendable { } } +// MARK: - Tool Input + +/// Raw tool input represented inline or by content reference. +public enum ToolInput: Codable, Sendable { + case inline(String) + case contentRef(ContentRef) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(String.self) { + self = .inline(value) + } else { + self = .contentRef(try container.decode(ContentRef.self)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .inline(let value): try container.encode(value) + case .contentRef(let value): try container.encode(value) + } + } +} + // MARK: - Discriminated Unions public struct ChatOriginUser: Codable, Sendable { @@ -5976,7 +6031,7 @@ public enum ToolResultContent: Codable, Sendable { } } -/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. +/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, annotations, or content state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index fc0f03fb2..4e22c8e23 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -207,10 +207,10 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { case .chatToolCallDelta(let a): return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .streaming(var s) = tc else { return tc } - s.partialInput = (s.partialInput ?? "") + a.content - if let msg = a.invocationMessage { - s.invocationMessage = msg + if let content = a.content { + s.partialInput = (s.partialInput ?? "") + content } + s.invocationMessage = a.invocationMessage ?? s.invocationMessage s.meta = a.meta ?? s.meta return .streaming(s) } @@ -223,6 +223,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { } let base = tc.baseFields let intention = a.intention ?? base.intention + let toolInput = a.toolInput ?? base.toolInput let contributor = refineToolCallContributor(base.contributor, a.contributor) let meta = a.meta ?? base.meta let pending: ToolCallPendingConfirmationState? @@ -240,7 +241,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { contributor: contributor, meta: meta, invocationMessage: a.invocationMessage, - toolInput: a.toolInput, + toolInput: toolInput, confirmed: confirmed, status: .running )) @@ -253,7 +254,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { contributor: contributor, meta: meta, invocationMessage: a.invocationMessage, - toolInput: a.toolInput ?? pending?.toolInput, + toolInput: toolInput, status: .pendingConfirmation, confirmationTitle: a.confirmationTitle ?? pending?.confirmationTitle, riskAssessment: a.riskAssessment ?? pending?.riskAssessment, @@ -270,6 +271,12 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { let meta = a.meta ?? base.meta let selectedOption = resolveSelectedOption(pending.options, id: a.selectedOptionId) if a.approved { + let toolInput: ToolInput? + if let edited = a.editedToolInput, case .inline = pending.toolInput { + toolInput = .inline(edited) + } else { + toolInput = pending.toolInput + } return .running(ToolCallRunningState( toolCallId: base.toolCallId, toolName: base.toolName, @@ -278,7 +285,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { contributor: base.contributor, meta: meta, invocationMessage: pending.invocationMessage, - toolInput: a.editedToolInput ?? pending.toolInput, + toolInput: toolInput, confirmed: a.confirmed ?? .notNeeded, selectedOption: selectedOption, status: .running @@ -307,7 +314,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { let meta = a.meta ?? base.meta let confirmed: ToolCallConfirmationReason let invocationMessage: StringOrMarkdown - let toolInput: String? + let toolInput: ToolInput? let selectedOption: ConfirmationOption? let preAuthContent: [ToolResultContent]? let fromAuthRequired: Bool @@ -1043,26 +1050,19 @@ private func endTurn( default: let base = tc.baseFields let invocationMessage: StringOrMarkdown - let toolInput: String? switch tc { case .streaming(let s): invocationMessage = s.invocationMessage ?? .string("") - toolInput = nil case .pendingConfirmation(let p): invocationMessage = p.invocationMessage - toolInput = p.toolInput case .running(let r): invocationMessage = r.invocationMessage - toolInput = r.toolInput case .authRequired(let a): invocationMessage = a.invocationMessage - toolInput = a.toolInput case .pendingResultConfirmation(let r): invocationMessage = r.invocationMessage - toolInput = r.toolInput default: invocationMessage = .string("") - toolInput = nil } return .toolCall(ToolCallResponsePart( kind: .toolCall, @@ -1074,7 +1074,7 @@ private func endTurn( contributor: base.contributor, meta: base.meta, invocationMessage: invocationMessage, - toolInput: toolInput, + toolInput: base.toolInput, status: .cancelled, reason: .skipped )) diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift index 72113daa6..93e7ecb20 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/ToolCallStateExtensions.swift @@ -32,6 +32,7 @@ public struct ToolCallBaseFields: Sendable { public let toolName: String public let displayName: String public let intention: String? + public let toolInput: ToolInput? public let contributor: ToolCallContributor? public let meta: [String: AnyCodable]? } @@ -45,30 +46,37 @@ extension ToolCallState { case .streaming(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: nil, contributor: s.contributor, meta: s.meta) case .pendingConfirmation(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .running(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .authRequired(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .pendingResultConfirmation(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .completed(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .cancelled(let s): return ToolCallBaseFields(toolCallId: s.toolCallId, toolName: s.toolName, displayName: s.displayName, intention: s.intention, + toolInput: s.toolInput, contributor: s.contributor, meta: s.meta) case .unknown: // All callers guard on a known variant before reaching here. diff --git a/clients/typescript/src/client/hosts/state-mirror.ts b/clients/typescript/src/client/hosts/state-mirror.ts index 2722ff0c1..e418205ba 100644 --- a/clients/typescript/src/client/hosts/state-mirror.ts +++ b/clients/typescript/src/client/hosts/state-mirror.ts @@ -119,6 +119,7 @@ export class MultiHostStateMirror { return this.changesetsMap; } + /** Look up the root state for `hostId`. */ getRoot(hostId: HostId): RootState | undefined { return this.rootStatesMap.get(hostId); @@ -139,6 +140,7 @@ export class MultiHostStateMirror { return this.changesetsMap.get(hostedResourceKey(hostId, uri)); } + /** * Convenience: apply a {@link HostSubscriptionEvent} produced by * {@link MultiHostClient.events}. Action envelopes are routed through diff --git a/clients/typescript/src/client/state-mirror.ts b/clients/typescript/src/client/state-mirror.ts index 3722ca59f..c10839f34 100644 --- a/clients/typescript/src/client/state-mirror.ts +++ b/clients/typescript/src/client/state-mirror.ts @@ -61,6 +61,7 @@ export class AhpStateMirror { return this.changesetsMap; } + /** Look up a session by URI. */ getSession(uri: URI): SessionState | undefined { return this.sessionsMap.get(uri); @@ -71,6 +72,7 @@ export class AhpStateMirror { return this.terminalsMap.get(uri); } + /** * Apply a server snapshot, replacing the state for the resource the * snapshot covers. diff --git a/clients/typescript/test/hosts.test.ts b/clients/typescript/test/hosts.test.ts index 58dc72cef..830564b0c 100644 --- a/clients/typescript/test/hosts.test.ts +++ b/clients/typescript/test/hosts.test.ts @@ -334,6 +334,7 @@ test('MultiHostStateMirror applies root snapshots scoped to host', () => { state: { agents: [makeAgent('copilot')] }, fromSeq: 0, }); + mirror.applySnapshot('host-b', { resource: ROOT, state: { agents: [makeAgent('vscode')] }, diff --git a/docs/.changes/20260730-tool-input-content-ref.json b/docs/.changes/20260730-tool-input-content-ref.json new file mode 100644 index 000000000..81e9d255b --- /dev/null +++ b/docs/.changes/20260730-tool-input-content-ref.json @@ -0,0 +1,4 @@ +{ + "type": "changed", + "message": "Final tool input may be inline or a lazy `ContentRef`, and streaming deltas may include partial parameters." +} diff --git a/docs/guide/actions.md b/docs/guide/actions.md index 8f08f3d05..b863ff52e 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -66,7 +66,7 @@ Tool calls follow a discriminated-union state machine — see [State Model — T | Type | Client-dispatchable? | When | |---|---|---| | `chat/toolCallStart` | No | Tool call created; LM begins streaming parameters | -| `chat/toolCallDelta` | No | Streaming partial parameters appended | +| `chat/toolCallDelta` | No | Invocation message updated, optionally with partial parameters | | `chat/toolCallReady` | No | Parameters complete (or running tool needs re-confirmation) | | `chat/toolCallConfirmed` | **Yes** | Client approves or denies a pending tool call | | `chat/toolCallComplete` | **Yes**¹ | Tool execution finished | diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index e07037098..4b51c1a6b 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -288,9 +288,9 @@ sequenceDiagram participant Client Note over Server: LLM selects a client tool - Server->>Client: toolCallStart (contributor.clientId = client's clientId) - Server->>Client: toolCallDelta (streaming parameters) - Server->>Client: toolCallReady (confirmed: 'not-needed') + Server->>Client: toolCallStart (client contributor) + Server->>Client: toolCallDelta (invocation message, optional partial args) + Server->>Client: toolCallReady (inline or referenced toolInput, confirmed: 'not-needed') Note over Client: Client sees contributor.clientId matches,
begins execution @@ -300,11 +300,11 @@ sequenceDiagram 1. **`chat/toolCallStart`** — The server dispatches this with the tool call's `contributor` set to a client contributor whose `clientId` is the owning client's. This tells the client it owns the tool call. -2. **`chat/toolCallDelta`** (zero or more) — The server streams partial parameters as the LLM generates them. The client can observe `partialInput` on the tool call state to preview the arguments. +2. **`chat/toolCallDelta`** (zero or more) — The server updates the invocation message while parameters stream and MAY include partial arguments. 3. **`chat/toolCallReady`** — Parameters are complete. For client-provided tools, the server typically sets `confirmed: 'not-needed'` so the tool transitions directly to `running`. If the server wants user confirmation first, it omits `confirmed` and the standard confirmation flow applies. -4. **Client executes** — When the tool call reaches `running` status, the owning client begins execution using the `toolInput` from the tool call state. +4. **Client executes** — When the tool call reaches `running` status, the owning client uses inline `toolInput` directly or reads a referenced input with `resourceRead`. If confirmation carried `editedToolInput`, inline state is updated by the reducer; for referenced input, the host updates the resource before echoing the accepted confirmation. 5. **`chat/toolCallContentChanged`** (zero or more, client-dispatched) — While executing, the client MAY stream intermediate content (e.g. terminal output, partial results) by dispatching this action. This replaces the `content` array on the running tool call state. diff --git a/docs/guide/state-model.md b/docs/guide/state-model.md index ab9f1458d..642952e75 100644 --- a/docs/guide/state-model.md +++ b/docs/guide/state-model.md @@ -322,12 +322,13 @@ ToolCallResponsePart { toolCall: ToolCallState // full lifecycle state } -// Reference to large content stored outside the state tree -ContentRef { +// Response-part wrapper around a large content reference +ResourceReponsePart { kind: 'contentRef' - uri: string // scheme://sessionId/contentId + uri: string sizeHint?: number contentType?: string + nonce?: string // changes when the referenced content changes } // Harness-authored notification surfaced in the stream (e.g. "subagent finished") @@ -386,8 +387,8 @@ stateDiagram-v2 | Status | Key Fields | Description | | ----------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `streaming` | `partialInput?` | LM is streaming tool call parameters. `partialInput` accumulates via `toolCallDelta`. | -| `pending-confirmation` | `invocationMessage`, `toolInput?`, `edits?`, `editable?`, `options?` | Parameters complete or mid-execution confirmation needed. `edits` previews file changes. `editable` indicates the client may edit parameters before confirming. `options` provides server-defined choices beyond simple approve/deny (see below). Uses `_meta` for additional context. | +| `streaming` | `partialInput?`, `invocationMessage?` | LM is streaming tool call parameters. `partialInput` accumulates when deltas include parameter content. | +| `pending-confirmation` | `invocationMessage`, `toolInput?`, `edits?`, `editable?`, `options?` | Parameters complete or mid-execution confirmation needed. `toolInput` may be inline or a `ContentRef`, at the host's discretion. `edits` previews file changes. `editable` indicates the client may edit parameters before confirming. `options` provides server-defined choices beyond simple approve/deny (see below). Uses `_meta` for additional context. | | `running` | `confirmed`, `selectedOption?` | Tool is executing. `confirmed` records how it was approved. `selectedOption` holds the chosen confirmation option, if any. | | `auth-required` | `confirmed`, `selectedOption?`, `contributor` (MCP), `auth` | Execution paused pending MCP authentication (see below). Only reachable for MCP-contributed tool calls. | | `pending-result-confirmation` | `success`, `pastTenseMessage`, `content?`, `selectedOption?` | Execution finished, waiting for client to approve the result. | @@ -414,7 +415,7 @@ This is deliberately **separate** from `McpServerAuthRequiredState` (the MCP ser ### Editable Parameters -When `editable` is `true` on a `pending-confirmation` tool call, the client may allow the user to modify the tool's input parameters before confirming. If the user edits the parameters, the client includes `editedToolInput` on the `chat/toolCallConfirmed` action. The reducer uses `editedToolInput` (if present) in place of the original `toolInput` when transitioning to `running`. +When `editable` is `true` on a `pending-confirmation` tool call, the client may allow the user to modify the tool's input parameters before confirming. If the user edits the parameters, the client includes `editedToolInput` on the `chat/toolCallConfirmed` action. For inline input, the reducer replaces the state value directly. For referenced input, the host MUST replace the resource contents before echoing the accepted action; the reducer keeps the same reference, and subsequent `resourceRead` calls return the input that was actually executed. Clients MUST NOT cache referenced tool input across confirmation. When a turn completes, non-terminal tool calls in `responseParts` are force-cancelled with reason `'skipped'`. diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 9a046769f..c65bec234 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -837,7 +837,7 @@ }, "content": { "type": "string", - "description": "Partial parameter content to append" + "description": "Partial parameter content to append, if provided by the host." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -847,8 +847,7 @@ "required": [ "turnId", "toolCallId", - "type", - "content" + "type" ] }, "ChatToolCallReadyAction": { @@ -884,8 +883,8 @@ "description": "Message describing what the tool will do or what confirmation is needed" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input" }, "confirmationTitle": { "$ref": "#/$defs/StringOrMarkdown", @@ -962,7 +961,7 @@ }, "editedToolInput": { "type": "string", - "description": "Edited tool input parameters, if the client modified them before confirming" + "description": "Edited tool input parameters, if the client modified them before confirming.\n\nFor inline `toolInput`, the reducer replaces the state value directly.\nFor referenced input, the host MUST replace the resource contents before\nechoing the accepted action." }, "selectedOptionId": { "type": "string", @@ -5755,8 +5754,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ @@ -5842,7 +5841,7 @@ }, "partialInput": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Partial parameters accumulated from tool-call deltas." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -5890,8 +5889,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "pending-confirmation" @@ -5990,8 +5989,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -6055,8 +6054,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -6126,8 +6125,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -6221,8 +6220,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -6316,8 +6315,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "cancelled" @@ -7420,6 +7419,17 @@ } ] }, + "ToolInput": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ContentRef" + } + ], + "description": "Tool input represented inline or by reference." + }, "ToolCallState": { "oneOf": [ { diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 775ee1285..1d8e841f6 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4918,8 +4918,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ @@ -5005,7 +5005,7 @@ }, "partialInput": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Partial parameters accumulated from tool-call deltas." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -5053,8 +5053,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "pending-confirmation" @@ -5153,8 +5153,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -5218,8 +5218,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -5289,8 +5289,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -5384,8 +5384,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -5479,8 +5479,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "cancelled" @@ -7077,7 +7077,7 @@ }, "content": { "type": "string", - "description": "Partial parameter content to append" + "description": "Partial parameter content to append, if provided by the host." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -7087,8 +7087,7 @@ "required": [ "turnId", "toolCallId", - "type", - "content" + "type" ] }, "ChatToolCallReadyAction": { @@ -7124,8 +7123,8 @@ "description": "Message describing what the tool will do or what confirmation is needed" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input" }, "confirmationTitle": { "$ref": "#/$defs/StringOrMarkdown", @@ -7202,7 +7201,7 @@ }, "editedToolInput": { "type": "string", - "description": "Edited tool input parameters, if the client modified them before confirming" + "description": "Edited tool input parameters, if the client modified them before confirming.\n\nFor inline `toolInput`, the reducer replaces the state value directly.\nFor referenced input, the host MUST replace the resource contents before\nechoing the accepted action." }, "selectedOptionId": { "type": "string", @@ -9161,6 +9160,17 @@ } ] }, + "ToolInput": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ContentRef" + } + ], + "description": "Tool input represented inline or by reference." + }, "ToolResultContent": { "oneOf": [ { diff --git a/schema/errors.schema.json b/schema/errors.schema.json index c770002fc..1ba061e40 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -3654,8 +3654,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ @@ -3741,7 +3741,7 @@ }, "partialInput": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Partial parameters accumulated from tool-call deltas." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3789,8 +3789,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "pending-confirmation" @@ -3889,8 +3889,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -3954,8 +3954,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -4025,8 +4025,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4120,8 +4120,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4215,8 +4215,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "cancelled" @@ -6794,6 +6794,17 @@ } ] }, + "ToolInput": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ContentRef" + } + ], + "description": "Tool input represented inline or by reference." + }, "ToolResultContent": { "oneOf": [ { @@ -8070,7 +8081,7 @@ }, "content": { "type": "string", - "description": "Partial parameter content to append" + "description": "Partial parameter content to append, if provided by the host." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -8080,8 +8091,7 @@ "required": [ "turnId", "toolCallId", - "type", - "content" + "type" ] }, "ChatToolCallReadyAction": { @@ -8117,8 +8127,8 @@ "description": "Message describing what the tool will do or what confirmation is needed" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input" }, "confirmationTitle": { "$ref": "#/$defs/StringOrMarkdown", @@ -9267,7 +9277,7 @@ }, "editedToolInput": { "type": "string", - "description": "Edited tool input parameters, if the client modified them before confirming" + "description": "Edited tool input parameters, if the client modified them before confirming.\n\nFor inline `toolInput`, the reducer replaces the state value directly.\nFor referenced input, the host MUST replace the resource contents before\nechoing the accepted action." }, "selectedOptionId": { "type": "string", diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index a11260fd0..9ffe995fe 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3817,8 +3817,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ @@ -3904,7 +3904,7 @@ }, "partialInput": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Partial parameters accumulated from tool-call deltas." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3952,8 +3952,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "pending-confirmation" @@ -4052,8 +4052,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -4117,8 +4117,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -4188,8 +4188,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4283,8 +4283,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4378,8 +4378,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "cancelled" @@ -5612,6 +5612,17 @@ } ] }, + "ToolInput": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ContentRef" + } + ], + "description": "Tool input represented inline or by reference." + }, "ToolResultContent": { "oneOf": [ { diff --git a/schema/state.schema.json b/schema/state.schema.json index 0d347e2ed..ab63a1da9 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -3565,8 +3565,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." } }, "required": [ @@ -3652,7 +3652,7 @@ }, "partialInput": { "type": "string", - "description": "Partial parameters accumulated so far" + "description": "Partial parameters accumulated from tool-call deltas." }, "invocationMessage": { "$ref": "#/$defs/StringOrMarkdown", @@ -3700,8 +3700,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "pending-confirmation" @@ -3800,8 +3800,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -3865,8 +3865,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "confirmed": { "$ref": "#/$defs/ToolCallConfirmationReason", @@ -3936,8 +3936,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4031,8 +4031,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "success": { "type": "boolean", @@ -4126,8 +4126,8 @@ "description": "Message describing what the tool will do" }, "toolInput": { - "type": "string", - "description": "Raw tool input" + "$ref": "#/$defs/ToolInput", + "description": "Final tool input.\n\nReferenced input is mutable until the tool call leaves\n`pending-confirmation`. When the client confirms with `editedToolInput`,\nthe host MUST replace the resource contents before echoing the accepted\nconfirmation action. Clients MUST NOT cache tool input across confirmation." }, "status": { "const": "cancelled" @@ -5230,6 +5230,17 @@ } ] }, + "ToolInput": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/$defs/ContentRef" + } + ], + "description": "Tool input represented inline or by reference." + }, "ToolCallState": { "oneOf": [ { diff --git a/scripts/generate-go.ts b/scripts/generate-go.ts index 33d7009c9..25753a2e2 100644 --- a/scripts/generate-go.ts +++ b/scripts/generate-go.ts @@ -155,6 +155,7 @@ function mapType(tsType: string): string { if (tsType === 'URI') return 'URI'; if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown'; + if (tsType === 'ToolInput') return 'ToolInput'; // ChildCustomizationType is a TS-only subset alias of CustomizationType. if (tsType === 'ChildCustomizationType') return 'CustomizationType'; @@ -1153,7 +1154,7 @@ func (o ChatOrigin) MarshalJSON() ([]byte, error) { function generateSnapshotState(): string { return `// SnapshotState is the state payload of a snapshot — root, session, -// chat, terminal, changeset, resource-watch, or annotations state. The active +// chat, terminal, changeset, resource-watch, annotations, or content state. The active // variant is chosen by which pointer field is non-nil; UnmarshalJSON probes // for required fields in the canonical order // (session → chat → terminal → changeset → resourceWatch → annotations → root). @@ -1254,6 +1255,39 @@ func containsAll(m map[string]json.RawMessage, keys ...string) bool { }`; } +function generateToolInput(): string { + return `// ToolInput is raw tool input represented inline or by content reference. +type ToolInput struct { +\tInline *string +\tContentRef *ContentRef +} + +func (t ToolInput) MarshalJSON() ([]byte, error) { +\tif t.Inline != nil { +\t\treturn json.Marshal(*t.Inline) +\t} +\tif t.ContentRef != nil { +\t\treturn json.Marshal(t.ContentRef) +\t} +\treturn []byte("null"), nil +} + +func (t *ToolInput) UnmarshalJSON(data []byte) error { +\t*t = ToolInput{} +\tvar inline string +\tif err := json.Unmarshal(data, &inline); err == nil { +\t\tt.Inline = &inline +\t\treturn nil +\t} +\tvar ref ContentRef +\tif err := json.Unmarshal(data, &ref); err != nil { +\t\treturn err +\t} +\tt.ContentRef = &ref +\treturn nil +}`; +} + function generateStateFile(project: Project): string { const lines: string[] = [HEADER_WITH_IMPORTS]; @@ -1281,6 +1315,9 @@ function generateStateFile(project: Project): string { } } + lines.push(generateToolInput()); + lines.push(''); + lines.push('// ─── Discriminated Unions ─────────────────────────────────────────────\n'); lines.push(generateDiscriminatedUnion(RESPONSE_PART_UNION)); lines.push(''); @@ -2014,6 +2051,7 @@ function checkExhaustiveness(project: Project): void { 'PaginatedParams', 'PaginatedResult', 'StringOrMarkdown', + 'ToolInput', 'ToolCallState', 'StateAction', 'ActionEnvelope', diff --git a/scripts/generate-kotlin.ts b/scripts/generate-kotlin.ts index 4fad6f133..e86113b77 100644 --- a/scripts/generate-kotlin.ts +++ b/scripts/generate-kotlin.ts @@ -136,6 +136,7 @@ function mapType(tsType: string): string { // Type aliases if (tsType === 'URI') return 'String'; if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown'; + if (tsType === 'ToolInput') return 'ToolInput'; // ChildCustomizationType is a TS-only subset alias of CustomizationType. if (tsType === 'ChildCustomizationType') return 'CustomizationType'; @@ -723,10 +724,51 @@ internal object StringOrMarkdownSerializer : KSerializer { }`; } +function generateToolInput(): string { + return `/** + * Raw tool input represented inline or by content reference. + */ +@Serializable(with = ToolInputSerializer::class) +sealed interface ToolInput { + @JvmInline value class Inline(val value: String) : ToolInput + @JvmInline value class ContentReference(val value: ContentRef) : ToolInput +} + +internal object ToolInputSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("ToolInput") + + override fun deserialize(decoder: Decoder): ToolInput { + val input = decoder as? JsonDecoder + ?: error("ToolInput can only be deserialized from JSON") + return when (val element = input.decodeJsonElement()) { + is JsonPrimitive -> ToolInput.Inline( + element.contentOrNull ?: error("ToolInput string must not be null"), + ) + is JsonObject -> ToolInput.ContentReference( + input.json.decodeFromJsonElement(ContentRef.serializer(), element), + ) + else -> error("ToolInput must be a string or ContentRef object") + } + } + + override fun serialize(encoder: Encoder, value: ToolInput) { + val output = encoder as? JsonEncoder + ?: error("ToolInput can only be serialized to JSON") + val element = when (value) { + is ToolInput.Inline -> JsonPrimitive(value.value) + is ToolInput.ContentReference -> + output.json.encodeToJsonElement(ContentRef.serializer(), value.value) + } + output.encodeJsonElement(element) + } +}`; +} + function generateSnapshotState(): string { return `/** * The state payload of a snapshot — root, session, chat, terminal, changeset, - * resource-watch, or annotations state. + * resource-watch, annotations, or content state. */ @Serializable(with = SnapshotStateSerializer::class) sealed interface SnapshotState { @@ -1214,6 +1256,11 @@ function generateStateFile(project: Project): string { } } + lines.push('// ─── Tool Input ──────────────────────────────────────────────────────────────'); + lines.push(''); + lines.push(generateToolInput()); + lines.push(''); + lines.push('// ─── Discriminated Unions ───────────────────────────────────────────────────'); lines.push(''); lines.push(generateChatOriginKotlin()); @@ -2028,6 +2075,7 @@ function checkExhaustiveness(project: Project): void { 'ActionType', // emitted directly by generateActionsFile(), not via STATE_ENUMS 'ChangesetOperationTargetKind', // discriminator enum embedded in the hand-rolled ChangesetOperationTarget union 'StringOrMarkdown', // generateStringOrMarkdown() + 'ToolInput', // generateToolInput() 'ToolCallState', // TOOL_CALL_STATE_UNION discriminated union 'StateAction', // StateAction enum in generateActionsFile() 'ActionEnvelope', // generateDataClassFromInterface() call in generateActionsFile() diff --git a/scripts/generate-markdown.ts b/scripts/generate-markdown.ts index 0e08372f4..38153690f 100644 --- a/scripts/generate-markdown.ts +++ b/scripts/generate-markdown.ts @@ -1078,6 +1078,7 @@ function generateOtlpChannelPage(project: Project): string { lines.push('The `ahp-otlp:` channels are stateless; the only state type is the capability descriptor the host advertises on `InitializeResult.telemetry`.\n'); lines.push(emitStateTypesSection([stateSf])); } + if (notificationsSf) { lines.push('## Notifications\n'); lines.push(schemaLink('notifications.schema.json')); @@ -1086,6 +1087,7 @@ function generateOtlpChannelPage(project: Project): string { return lines.join('\n'); } + // ─── Error Codes Page ──────────────────────────────────────────────────────── function generateErrorCodesPage(project: Project): string { diff --git a/scripts/generate-rust.ts b/scripts/generate-rust.ts index eaba75920..7fb9044dd 100644 --- a/scripts/generate-rust.ts +++ b/scripts/generate-rust.ts @@ -145,6 +145,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str if (tsType === 'URI') return 'Uri'; if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown'; + if (tsType === 'ToolInput') return 'ToolInput'; // ChildCustomizationType is a TS-only subset alias of CustomizationType. if (tsType === 'ChildCustomizationType') return 'CustomizationType'; @@ -1088,7 +1089,7 @@ pub enum ChatOrigin { function generateSnapshotState(): string { return `/// The state payload of a snapshot — root, session, chat, terminal, -/// changeset, resource-watch, or annotations state. +/// changeset, resource-watch, annotations, or content state. /// /// Deserialized by trying session first (has required \`lifecycle\`), then /// chat (has required \`turns\`), then terminal (has required \`content\`), @@ -1108,6 +1109,16 @@ pub enum SnapshotState { }`; } +function generateToolInput(): string { + return `/// Raw tool input represented inline or by content reference. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ToolInput { + Inline(String), + ContentRef(ContentRef), +}`; +} + function generateStateFile(project: Project): string { const lines: string[] = [GENERATED_HEADER]; @@ -1140,6 +1151,9 @@ function generateStateFile(project: Project): string { } } + lines.push(generateToolInput()); + lines.push(''); + lines.push('// ─── Discriminated Unions ─────────────────────────────────────────────\n'); lines.push(generateChatOrigin()); lines.push(''); @@ -1316,7 +1330,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, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, 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, ContentRef, Customization, ErrorInfo, McpAuthRequirement, McpServerState, ModelSelection, ResponsePart, SessionActiveClient, SessionInputRequest, SideChatSelection, TerminalClaim, TerminalInfo, TextRange, ToolCallContributor, ToolCallResult, ToolCallRiskAssessment, ToolCallConfirmationReason, ToolCallCancellationReason, ToolDefinition, ToolInput, ToolResultContent, UsageInfo, Message, PendingMessageKind, Turn, ChangesetStatus, ChangesetFile, ChangesetOperation, ChangesetOperationStatus, Changeset, ChatSummary};'); lines.push(''); // ActionType enum @@ -1866,6 +1880,7 @@ function checkExhaustiveness(project: Project): void { 'PaginatedParams', // base interface; flattened into each paginated command params struct 'PaginatedResult', // base interface; flattened into each paginated command result struct 'StringOrMarkdown', + 'ToolInput', 'ToolCallState', 'StateAction', 'ActionEnvelope', diff --git a/scripts/generate-swift.ts b/scripts/generate-swift.ts index 88d307d64..086f86593 100644 --- a/scripts/generate-swift.ts +++ b/scripts/generate-swift.ts @@ -104,6 +104,7 @@ function mapType(tsType: string, propName?: string, containerName?: string): str // Type aliases if (tsType === 'URI') return 'String'; if (tsType === 'StringOrMarkdown') return 'StringOrMarkdown'; + if (tsType === 'ToolInput') return 'ToolInput'; // ChildCustomizationType is a TS-only subset alias of CustomizationType. if (tsType === 'ChildCustomizationType') return 'CustomizationType'; @@ -968,8 +969,33 @@ public enum StringOrMarkdown: Codable, Sendable, Equatable { }`; } +function generateToolInput(): string { + return `/// Raw tool input represented inline or by content reference. +public enum ToolInput: Codable, Sendable { + case inline(String) + case contentRef(ContentRef) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(String.self) { + self = .inline(value) + } else { + self = .contentRef(try container.decode(ContentRef.self)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .inline(let value): try container.encode(value) + case .contentRef(let value): try container.encode(value) + } + } +}`; +} + function generateSnapshotState(): string { - return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, or annotations state. + return `/// The state payload of a snapshot — root, session, chat, terminal, changeset, resource-watch, annotations, or content state. public enum SnapshotState: Codable, Sendable { case root(RootState) case session(SessionState) @@ -1125,6 +1151,10 @@ function generateStateFile(project: Project): string { } } + lines.push('// MARK: - Tool Input\n'); + lines.push(generateToolInput()); + lines.push(''); + lines.push('// MARK: - Discriminated Unions\n'); lines.push(generateChatOriginSwift()); lines.push(''); @@ -2063,6 +2093,7 @@ function checkExhaustiveness(project: Project): void { 'PaginatedParams', // base interface; flattened into each paginated command params struct 'PaginatedResult', // base interface; flattened into each paginated command result struct 'StringOrMarkdown', // generateStringOrMarkdown() + 'ToolInput', // generateToolInput() 'ToolCallState', // TOOL_CALL_STATE_UNION discriminated union 'StateAction', // StateAction enum in generateActionsFile() 'ActionEnvelope', // generateStructFromInterface() call in generateActionsFile() diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index 35307ec19..caecaeb11 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -18,6 +18,7 @@ import type { ConfirmationOption, ToolCallContributor, ToolCallRiskAssessment, + ToolInput, Turn, } from './state.js'; import { @@ -173,8 +174,8 @@ export interface ChatToolCallStartAction extends ToolCallActionBase { */ export interface ChatToolCallDeltaAction extends ToolCallActionBase { type: ActionType.ChatToolCallDelta; - /** Partial parameter content to append */ - content: string; + /** Partial parameter content to append, if provided by the host. */ + content?: string; /** Updated progress message */ invocationMessage?: StringOrMarkdown; } @@ -211,8 +212,8 @@ export interface ChatToolCallReadyAction extends ToolCallActionBase { intention?: string; /** Message describing what the tool will do or what confirmation is needed */ invocationMessage: StringOrMarkdown; - /** Raw tool input */ - toolInput?: string; + /** Final tool input */ + toolInput?: ToolInput; /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */ confirmationTitle?: StringOrMarkdown; /** Risk assessment that informed the confirmation requirement. */ @@ -245,7 +246,13 @@ export interface ChatToolCallApprovedAction extends ToolCallActionBase { approved: true; /** How the tool was confirmed */ confirmed: ToolCallConfirmationReason; - /** Edited tool input parameters, if the client modified them before confirming */ + /** + * Edited tool input parameters, if the client modified them before confirming. + * + * For inline `toolInput`, the reducer replaces the state value directly. + * For referenced input, the host MUST replace the resource contents before + * echoing the accepted action. + */ editedToolInput?: string; /** ID of the selected confirmation option, if the server provided options */ selectedOptionId?: string; diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 332eaaec0..b170cefb2 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -465,7 +465,9 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st return { ...tc, ...(action._meta !== undefined ? { _meta: action._meta } : {}), - partialInput: (tc.partialInput ?? '') + action.content, + ...(action.content !== undefined + ? { partialInput: (tc.partialInput ?? '') + action.content } + : {}), invocationMessage: action.invocationMessage ?? tc.invocationMessage, }; }); @@ -484,12 +486,14 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st contributor: refineToolCallContributor(tc.contributor, action.contributor, log), intention: action.intention ?? tc.intention, }; + const toolInput = action.toolInput + ?? (tc.status === ToolCallStatus.Streaming ? undefined : tc.toolInput); if (action.confirmed) { return { status: ToolCallStatus.Running, ...base, invocationMessage: action.invocationMessage, - toolInput: action.toolInput, + toolInput, confirmed: action.confirmed, }; } @@ -499,7 +503,7 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st status: ToolCallStatus.PendingConfirmation, ...base, invocationMessage: action.invocationMessage, - toolInput: action.toolInput ?? pending?.toolInput, + toolInput, confirmationTitle: action.confirmationTitle ?? pending?.confirmationTitle, riskAssessment: action.riskAssessment ?? pending?.riskAssessment, edits: action.edits ?? pending?.edits, @@ -516,11 +520,14 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st const base = tcBaseWithMeta(tc, action._meta); const selectedOption = resolveSelectedOption(tc.options, action.selectedOptionId); if (action.approved) { + const toolInput = action.editedToolInput !== undefined && typeof tc.toolInput === 'string' + ? action.editedToolInput + : tc.toolInput; return { status: ToolCallStatus.Running, ...base, invocationMessage: tc.invocationMessage, - toolInput: action.editedToolInput ?? tc.toolInput, + toolInput, confirmed: action.confirmed, ...(selectedOption ? { selectedOption } : {}), }; diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index c26449958..726443173 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -1185,10 +1185,24 @@ interface ToolCallBase { interface ToolCallParameterFields { /** Message describing what the tool will do */ invocationMessage: StringOrMarkdown; - /** Raw tool input */ - toolInput?: string; + /** + * Final tool input. + * + * Referenced input is mutable until the tool call leaves + * `pending-confirmation`. When the client confirms with `editedToolInput`, + * the host MUST replace the resource contents before echoing the accepted + * confirmation action. Clients MUST NOT cache tool input across confirmation. + */ + toolInput?: ToolInput; } +/** + * Tool input represented inline or by reference. + * + * @category Tool Call Types + */ +export type ToolInput = string | ContentRef; + /** * Tool execution result details, available after execution completes. * @@ -1222,7 +1236,7 @@ export interface ToolCallResult { */ export interface ToolCallStreamingState extends ToolCallBase { status: ToolCallStatus.Streaming; - /** Partial parameters accumulated so far */ + /** Partial parameters accumulated from tool-call deltas. */ partialInput?: string; /** Progress message shown while parameters are streaming */ invocationMessage?: StringOrMarkdown; diff --git a/types/index.ts b/types/index.ts index b91e0dd4a..172400d52 100644 --- a/types/index.ts +++ b/types/index.ts @@ -50,6 +50,7 @@ export type { SystemNotificationResponsePart, ResponsePart, ToolCallResult, + ToolInput, ToolCallStreamingState, ToolCallPendingConfirmationState, ToolCallRunningState, diff --git a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json index 093915793..8d2d4a5f5 100644 --- a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json +++ b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json @@ -33,7 +33,6 @@ "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", - "content": "ls -la", "invocationMessage": "Listing files" }, { @@ -41,7 +40,9 @@ "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: ls -la", - "toolInput": "ls -la" + "toolInput": { + "uri": "file:///tool-inputs/tc-1.json" + } }, { "type": "chat/toolCallConfirmed", @@ -83,7 +84,9 @@ "contributor": null, "_meta": null, "invocationMessage": "Run: ls -la", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tc-1.json" + }, "confirmed": "user-action", "success": true, "pastTenseMessage": "Ran command" diff --git a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json index 543212f61..fca33067b 100644 --- a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json +++ b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json @@ -25,7 +25,7 @@ "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "nonexistent", - "content": "data" + "invocationMessage": "Working" } ], "expected": { diff --git a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json index 2cb785045..a15874b67 100644 --- a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json +++ b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -69,7 +71,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "content": [ { diff --git a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json index 18582ffe4..69432a59a 100644 --- a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json +++ b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Ran command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "success": true, "pastTenseMessage": "Ran command" @@ -71,7 +73,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Ran command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "success": true, "pastTenseMessage": "Ran command" diff --git a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json index 905071151..4647c7260 100644 --- a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json +++ b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "content": [ { @@ -80,7 +82,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "content": [ { diff --git a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json index b640dc88f..3549288df 100644 --- a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json @@ -36,7 +36,7 @@ "type": "chat/toolCallDelta", "turnId": "wrong-turn", "toolCallId": "tc-1", - "content": "data" + "invocationMessage": "Working" } ], "expected": { diff --git a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json index 93a0910e9..1c68eae20 100644 --- a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -39,7 +41,7 @@ "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", - "content": "extra data" + "invocationMessage": "Working" } ], "expected": { @@ -63,7 +65,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } diff --git a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json index f616ee6bb..812040255 100644 --- a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -64,7 +66,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } diff --git a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json index f4bc585f4..3cce8b9db 100644 --- a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Ran command", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "success": true, "pastTenseMessage": "Ran command" @@ -68,7 +70,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Ran command", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "success": true, "pastTenseMessage": "Ran command" diff --git a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json index 44f6c44d4..9112938a9 100644 --- a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -63,7 +65,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } diff --git a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json index 937e2c6f2..5ab99dc42 100644 --- a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json +++ b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -65,7 +67,9 @@ "contributor": null, "_meta": null, "invocationMessage": "Running command", - "toolInput": "ls -la", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "reason": "skipped" } } diff --git a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json index 2eee6a8d8..6091b04ea 100644 --- a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json +++ b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json @@ -22,7 +22,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } @@ -63,7 +65,9 @@ "displayName": "Run Command", "intention": null, "invocationMessage": "Running", - "toolInput": "ls", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action" } } diff --git a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json index f8c0e1f56..5892b15fd 100644 --- a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json +++ b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json @@ -1,5 +1,5 @@ { - "description": "toolCallDelta without invocationMessage preserves existing", + "description": "toolCallDelta appends input without changing invocationMessage when omitted", "reducer": "chat", "initial": { "turns": [], @@ -37,7 +37,7 @@ "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-1", - "content": "data" + "content": "{\"command\":\"ls" } ], "expected": { @@ -60,8 +60,8 @@ "toolName": "bash", "displayName": "Run Command", "intention": null, - "invocationMessage": "Original message", - "partialInput": "data" + "partialInput": "{\"command\":\"ls", + "invocationMessage": "Original message" } } ], 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 f2d01c3f7..072edf95e 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 @@ -33,7 +33,9 @@ "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Run: rm -rf /tmp/test", - "toolInput": "rm -rf /tmp/test" + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + } } ], "expected": { @@ -59,7 +61,9 @@ "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /tmp/test", - "toolInput": "rm -rf /tmp/test", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": null, "riskAssessment": null, "edits": null, diff --git a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json index 4fd274f36..022c4f911 100644 --- a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json +++ b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json @@ -1,5 +1,5 @@ { - "description": "toolCallConfirmed approved with editedToolInput overrides the original toolInput in the running state", + "description": "toolCallConfirmed replaces inline tool input with edited input", "reducer": "chat", "initial": { "turns": [], diff --git a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json index a01886f2e..77699f890 100644 --- a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json +++ b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json @@ -37,7 +37,9 @@ "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" }, { @@ -76,7 +78,9 @@ }, "_meta": null, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "success": true, "pastTenseMessage": "Searched files" 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 e46ac0358..ce28c10f9 100644 --- a/types/test-cases/reducers/220-toolcall-actions-update-meta.json +++ b/types/test-cases/reducers/220-toolcall-actions-update-meta.json @@ -35,7 +35,6 @@ "type": "chat/toolCallDelta", "turnId": "turn-1", "toolCallId": "tc-delta", - "content": "abc", "invocationMessage": "Streaming input", "_meta": { "phase": "delta" @@ -53,7 +52,9 @@ "turnId": "turn-1", "toolCallId": "tc-ready-running", "invocationMessage": "Run now", - "toolInput": "{}", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "_meta": { "phase": "ready-running" @@ -237,7 +238,6 @@ "phase": "delta" }, "status": "streaming", - "partialInput": "abc", "invocationMessage": "Streaming input" } }, @@ -254,7 +254,9 @@ "phase": "ready-running" }, "invocationMessage": "Run now", - "toolInput": "{}", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" } }, diff --git a/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json b/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json index 56d548d52..f2e736985 100644 --- a/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json +++ b/types/test-cases/reducers/242-toolcallready-completes-risk-assessment.json @@ -24,7 +24,9 @@ "contributor": null, "_meta": null, "invocationMessage": "Checking command safety", - "toolInput": "rm -rf /tmp/test", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Run in terminal", "riskAssessment": { "kind": "judge", @@ -88,7 +90,9 @@ "contributor": null, "_meta": null, "invocationMessage": "Run: rm -rf /tmp/test", - "toolInput": "rm -rf /tmp/test", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Run in terminal", "riskAssessment": { "kind": "judge", diff --git a/types/test-cases/reducers/243-toolcallauthrequired-full-lifecycle-running-authrequired-running-completed.json b/types/test-cases/reducers/243-toolcallauthrequired-full-lifecycle-running-authrequired-running-completed.json index 02abe2960..55549fc33 100644 --- a/types/test-cases/reducers/243-toolcallauthrequired-full-lifecycle-running-authrequired-running-completed.json +++ b/types/test-cases/reducers/243-toolcallauthrequired-full-lifecycle-running-authrequired-running-completed.json @@ -37,7 +37,9 @@ "turnId": "turn-1", "toolCallId": "tc-1", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" }, { @@ -49,7 +51,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"], + "requiredScopes": [ + "repo:read" + ], "description": "Needs repo:read" } }, @@ -94,7 +98,9 @@ }, "_meta": null, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "success": true, "pastTenseMessage": "Searched repo" diff --git a/types/test-cases/reducers/247-toolcallauthresolved-noop-when-not-auth-required.json b/types/test-cases/reducers/247-toolcallauthresolved-noop-when-not-auth-required.json index 095a26fe0..cceed08f3 100644 --- a/types/test-cases/reducers/247-toolcallauthresolved-noop-when-not-auth-required.json +++ b/types/test-cases/reducers/247-toolcallauthresolved-noop-when-not-auth-required.json @@ -37,7 +37,9 @@ "turnId": "turn-1", "toolCallId": "tc-4", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" }, { @@ -72,7 +74,9 @@ }, "_meta": null, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" } } diff --git a/types/test-cases/reducers/248-toolcallauthrequired-sets-input-needed-status-and-preserves-metadata.json b/types/test-cases/reducers/248-toolcallauthrequired-sets-input-needed-status-and-preserves-metadata.json index 33e2ab02d..9859badc2 100644 --- a/types/test-cases/reducers/248-toolcallauthrequired-sets-input-needed-status-and-preserves-metadata.json +++ b/types/test-cases/reducers/248-toolcallauthrequired-sets-input-needed-status-and-preserves-metadata.json @@ -40,7 +40,9 @@ "turnId": "turn-1", "toolCallId": "tc-5", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Search repo", "options": [ { @@ -78,7 +80,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] }, "_meta": { "phase": "auth-required" @@ -113,7 +117,9 @@ "phase": "auth-required" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "selectedOption": { "id": "opt-1", @@ -131,7 +137,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] } } } diff --git a/types/test-cases/reducers/249-toolcallauthresolved-preserves-fields-and-clears-input-needed.json b/types/test-cases/reducers/249-toolcallauthresolved-preserves-fields-and-clears-input-needed.json index cdc94c6a6..54586a464 100644 --- a/types/test-cases/reducers/249-toolcallauthresolved-preserves-fields-and-clears-input-needed.json +++ b/types/test-cases/reducers/249-toolcallauthresolved-preserves-fields-and-clears-input-needed.json @@ -37,7 +37,9 @@ "turnId": "turn-1", "toolCallId": "tc-6", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "options": [ { "id": "opt-1", @@ -113,7 +115,9 @@ "phase": "resolved" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "selectedOption": { "id": "opt-1", diff --git a/types/test-cases/reducers/250-turncomplete-force-cancels-auth-required-tool-call.json b/types/test-cases/reducers/250-turncomplete-force-cancels-auth-required-tool-call.json index f2e0b7d0b..ae0ddefaa 100644 --- a/types/test-cases/reducers/250-turncomplete-force-cancels-auth-required-tool-call.json +++ b/types/test-cases/reducers/250-turncomplete-force-cancels-auth-required-tool-call.json @@ -37,7 +37,9 @@ "turnId": "turn-1", "toolCallId": "tc-7", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed" }, { @@ -84,7 +86,9 @@ }, "_meta": null, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "reason": "skipped" } } diff --git a/types/test-cases/reducers/251-session-inputneededset-appends-tool-authentication.json b/types/test-cases/reducers/251-session-inputneededset-appends-tool-authentication.json index a8725b335..0ca318124 100644 --- a/types/test-cases/reducers/251-session-inputneededset-appends-tool-authentication.json +++ b/types/test-cases/reducers/251-session-inputneededset-appends-tool-authentication.json @@ -13,7 +13,10 @@ "kind": "chatInput", "id": "copilot:/test-session/chat-1#req-1", "chat": "copilot:/test-session/chat-1", - "request": { "id": "req-1", "message": "Which environment?" } + "request": { + "id": "req-1", + "message": "Which environment?" + } } ] }, @@ -35,15 +38,21 @@ "customizationId": "mcp-1" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "auth": { "reason": "insufficientScope", "resource": { "resource": "https://mcp.example.com", - "authorization_servers": ["https://auth.example.com"] + "authorization_servers": [ + "https://auth.example.com" + ] }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] } } } @@ -61,7 +70,10 @@ "kind": "chatInput", "id": "copilot:/test-session/chat-1#req-1", "chat": "copilot:/test-session/chat-1", - "request": { "id": "req-1", "message": "Which environment?" } + "request": { + "id": "req-1", + "message": "Which environment?" + } }, { "kind": "toolAuthentication", @@ -78,15 +90,21 @@ "customizationId": "mcp-1" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "auth": { "reason": "insufficientScope", "resource": { "resource": "https://mcp.example.com", - "authorization_servers": ["https://auth.example.com"] + "authorization_servers": [ + "https://auth.example.com" + ] }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] } } } diff --git a/types/test-cases/reducers/252-session-inputneededremoved-removes-tool-authentication.json b/types/test-cases/reducers/252-session-inputneededremoved-removes-tool-authentication.json index 3b51b8479..b6862ea0a 100644 --- a/types/test-cases/reducers/252-session-inputneededremoved-removes-tool-authentication.json +++ b/types/test-cases/reducers/252-session-inputneededremoved-removes-tool-authentication.json @@ -24,7 +24,9 @@ "customizationId": "mcp-1" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "not-needed", "auth": { "reason": "required", diff --git a/types/test-cases/reducers/253-toolcallcomplete-cancels-auth-required-tool-call.json b/types/test-cases/reducers/253-toolcallcomplete-cancels-auth-required-tool-call.json index 6e6d7e017..37b66b56d 100644 --- a/types/test-cases/reducers/253-toolcallcomplete-cancels-auth-required-tool-call.json +++ b/types/test-cases/reducers/253-toolcallcomplete-cancels-auth-required-tool-call.json @@ -40,7 +40,9 @@ "turnId": "turn-1", "toolCallId": "tc-8", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Search repo", "options": [ { @@ -78,7 +80,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] }, "_meta": { "phase": "auth-required" @@ -129,7 +133,9 @@ "phase": "cancelled" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "selectedOption": { "id": "opt-1", diff --git a/types/test-cases/reducers/254-toolcallcomplete-ignores-successful-result-from-auth-required.json b/types/test-cases/reducers/254-toolcallcomplete-ignores-successful-result-from-auth-required.json index 74c8a207a..6a2d14865 100644 --- a/types/test-cases/reducers/254-toolcallcomplete-ignores-successful-result-from-auth-required.json +++ b/types/test-cases/reducers/254-toolcallcomplete-ignores-successful-result-from-auth-required.json @@ -40,7 +40,9 @@ "turnId": "turn-1", "toolCallId": "tc-9", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Search repo", "options": [ { @@ -78,7 +80,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] }, "_meta": { "phase": "auth-required" @@ -131,7 +135,9 @@ "phase": "auth-required" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "selectedOption": { "id": "opt-1", @@ -149,7 +155,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] } } } diff --git a/types/test-cases/reducers/255-toolcallcomplete-ignores-requiresresultconfirmation-when-cancelling-auth-required.json b/types/test-cases/reducers/255-toolcallcomplete-ignores-requiresresultconfirmation-when-cancelling-auth-required.json index c8e1f44dc..d518f65c1 100644 --- a/types/test-cases/reducers/255-toolcallcomplete-ignores-requiresresultconfirmation-when-cancelling-auth-required.json +++ b/types/test-cases/reducers/255-toolcallcomplete-ignores-requiresresultconfirmation-when-cancelling-auth-required.json @@ -40,7 +40,9 @@ "turnId": "turn-1", "toolCallId": "tc-10", "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmationTitle": "Search repo", "options": [ { @@ -78,7 +80,9 @@ "resource": { "resource": "https://mcp.example.com" }, - "requiredScopes": ["repo:read"] + "requiredScopes": [ + "repo:read" + ] }, "_meta": { "phase": "auth-required" @@ -130,7 +134,9 @@ "phase": "cancelled" }, "invocationMessage": "Search: foo", - "toolInput": "foo", + "toolInput": { + "uri": "file:///tool-inputs/tool-input.json" + }, "confirmed": "user-action", "selectedOption": { "id": "opt-1", diff --git a/types/test-cases/reducers/260-edited-tool-input-survives-reconfirmation.json b/types/test-cases/reducers/260-edited-tool-input-survives-reconfirmation.json new file mode 100644 index 000000000..4b58a9e41 --- /dev/null +++ b/types/test-cases/reducers/260-edited-tool-input-survives-reconfirmation.json @@ -0,0 +1,99 @@ +{ + "description": "tool input ref survives re-confirmation after an edited confirmation", + "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": "Run", + "toolInput": { + "uri": "file:///tool-inputs/tc-1.json" + } + }, + { + "type": "chat/toolCallConfirmed", + "turnId": "turn-1", + "toolCallId": "tc-1", + "approved": true, + "confirmed": "user-action", + "editedToolInput": "{\"command\":\"echo edited\"}" + }, + { + "type": "chat/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-1", + "invocationMessage": "Additional permission needed" + }, + { + "type": "chat/toolCallConfirmed", + "turnId": "turn-1", + "toolCallId": "tc-1", + "approved": true, + "confirmed": "user-action" + } + ], + "expected": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "running", + "toolCallId": "tc-1", + "toolName": "bash", + "displayName": "Run Command", + "intention": null, + "toolInput": { + "uri": "file:///tool-inputs/tc-1.json" + }, + "contributor": null, + "_meta": null, + "invocationMessage": "Additional permission needed", + "confirmed": "user-action" + } + } + ], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:02.000Z" + } +}