diff --git a/CHANGELOG.md b/CHANGELOG.md index f7530d3a..f9b92744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,11 @@ changes accumulate. Track in-flight protocol changes via PRs touching point-in-time snapshot. Existing variants (root, session, terminal, changeset, annotations) are unchanged. +### Fixed + +- Session reducers now apply `_meta` updates from every tool-call-scoped + action, not only `session/toolCallStart`. + ## [0.4.0] — Unreleased Spec version: `0.4.0` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index 175beb3e..d40b650a 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -21,6 +21,11 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. `root` + `recursive` keys (ordered between the existing changeset and annotations probes). +### Fixed + +- Reducer parity fixtures now require `_meta` updates from every + tool-call-scoped action, not only `session/toolCallStart`. + ### Added - `AnnotationsUpdatedAction` (`annotations/updated`) — partially updates an diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index ed14c284..70e35df1 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -463,6 +463,9 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct case *ahptypes.SessionToolCallContentChangedAction: return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { if r, ok := tc.Value.(*ahptypes.ToolCallRunningState); ok { + if a.Meta != nil { + r.Meta = a.Meta + } r.Content = append([]ahptypes.ToolResultContent(nil), a.Content...) } return tc @@ -760,6 +763,9 @@ func applyToolCallDelta(state *ahptypes.SessionState, a *ahptypes.SessionToolCal } joined := current + a.Content s.PartialInput = &joined + if a.Meta != nil { + s.Meta = a.Meta + } if a.InvocationMessage != nil { im := *a.InvocationMessage s.InvocationMessage = &im @@ -771,6 +777,9 @@ func applyToolCallDelta(state *ahptypes.SessionState, a *ahptypes.SessionToolCal func applyToolCallReady(state *ahptypes.SessionState, a *ahptypes.SessionToolCallReadyAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { common := toolCallMeta(tc) + if a.Meta != nil { + common.meta = a.Meta + } switch tc.Value.(type) { case *ahptypes.ToolCallStreamingState, *ahptypes.ToolCallRunningState: if a.Confirmed != nil { @@ -830,6 +839,10 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo if a.EditedToolInput != nil { toolInput = a.EditedToolInput } + meta := s.Meta + if a.Meta != nil { + meta = a.Meta + } confirmed := ahptypes.ToolCallConfirmationReasonNotNeeded if a.Confirmed != nil { confirmed = *a.Confirmed @@ -840,7 +853,7 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo ToolName: s.ToolName, DisplayName: s.DisplayName, Contributor: s.Contributor, - Meta: s.Meta, + Meta: meta, InvocationMessage: s.InvocationMessage, ToolInput: toolInput, Confirmed: confirmed, @@ -851,13 +864,17 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo if a.Reason != nil { reason = *a.Reason } + meta := s.Meta + if a.Meta != nil { + meta = a.Meta + } return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCancelledState{ Status: ahptypes.ToolCallStatusCancelled, ToolCallId: s.ToolCallId, ToolName: s.ToolName, DisplayName: s.DisplayName, Contributor: s.Contributor, - Meta: s.Meta, + Meta: meta, InvocationMessage: s.InvocationMessage, ToolInput: s.ToolInput, Reason: reason, @@ -871,6 +888,9 @@ func applyToolCallConfirmed(state *ahptypes.SessionState, a *ahptypes.SessionToo func applyToolCallComplete(state *ahptypes.SessionState, a *ahptypes.SessionToolCallCompleteAction) ReduceOutcome { return updateToolCall(state, a.TurnId, a.ToolCallId, func(tc ahptypes.ToolCallState) ahptypes.ToolCallState { common := toolCallMeta(tc) + if a.Meta != nil { + common.meta = a.Meta + } var ( invocation ahptypes.StringOrMarkdown toolInput *string @@ -936,13 +956,17 @@ func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.Sess return tc } if a.Approved { + meta := s.Meta + if a.Meta != nil { + meta = a.Meta + } return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCompletedState{ Status: ahptypes.ToolCallStatusCompleted, ToolCallId: s.ToolCallId, ToolName: s.ToolName, DisplayName: s.DisplayName, Contributor: s.Contributor, - Meta: s.Meta, + Meta: meta, InvocationMessage: s.InvocationMessage, ToolInput: s.ToolInput, Success: s.Success, @@ -954,13 +978,17 @@ func applyToolCallResultConfirmed(state *ahptypes.SessionState, a *ahptypes.Sess SelectedOption: s.SelectedOption, }} } + meta := s.Meta + if a.Meta != nil { + meta = a.Meta + } return ahptypes.ToolCallState{Value: &ahptypes.ToolCallCancelledState{ Status: ahptypes.ToolCallStatusCancelled, ToolCallId: s.ToolCallId, ToolName: s.ToolName, DisplayName: s.DisplayName, Contributor: s.Contributor, - Meta: s.Meta, + Meta: meta, InvocationMessage: s.InvocationMessage, ToolInput: s.ToolInput, Reason: ahptypes.ToolCallCancellationReasonResultDenied, diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 076dc32c..25586707 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -22,6 +22,11 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump channel decodes via the existing `SnapshotStateSerializer` shape probe (required `root` + `recursive` keys). +### Fixed + +- `sessionReducer` now applies `_meta` (`meta`) updates from every + tool-call-scoped action, not only `session/toolCallStart`. + ### Added - `AnnotationsUpdatedAction` (`annotations/updated`) — partially updates an 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 f28d2e2a..c7a3b74f 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -269,7 +269,9 @@ private data class ToolCallBase( val displayName: String, val contributor: ToolCallContributor?, val meta: Map?, -) +) { + fun withMeta(meta: Map?): ToolCallBase = copy(meta = meta ?: this.meta) +} private fun toolCallBase(tc: ToolCallState): ToolCallBase = when (tc) { is ToolCallStateStreaming -> tc.value.let { @@ -672,6 +674,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat if (tc !is ToolCallStateStreaming) tc else { ToolCallStateStreaming( tc.value.copy( + meta = a.meta ?: tc.value.meta, partialInput = (tc.value.partialInput ?: "") + a.content, invocationMessage = a.invocationMessage ?: tc.value.invocationMessage, ), @@ -687,7 +690,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat if (tc !is ToolCallStateStreaming && tc !is ToolCallStateRunning) { tc } else { - val base = toolCallBase(tc) + val base = toolCallBase(tc).withMeta(a.meta) if (a.confirmed != null) { ToolCallStateRunning( ToolCallRunningState( @@ -730,7 +733,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat refreshSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStatePendingConfirmation) tc else { - val base = toolCallBase(tc) + val base = toolCallBase(tc).withMeta(a.meta) val selectedOption = resolveSelectedOption(tc.value.options, a.selectedOptionId) if (a.approved) { ToolCallStateRunning( @@ -791,7 +794,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat ) else -> return@updateToolCallInParts tc } - val base = toolCallBase(tc) + val base = toolCallBase(tc).withMeta(a.meta) if (a.requiresResultConfirmation == true) { ToolCallStatePendingResultConfirmation( ToolCallPendingResultConfirmationState( @@ -842,7 +845,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat refreshSummaryStatus( updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStatePendingResultConfirmation) tc else { - val base = toolCallBase(tc) + val base = toolCallBase(tc).withMeta(a.meta) if (a.approved) { ToolCallStateCompleted( ToolCallCompletedState( @@ -888,7 +891,7 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val a = action.value updateToolCallInParts(state, a.turnId, a.toolCallId) { tc -> if (tc !is ToolCallStateRunning) tc else { - ToolCallStateRunning(tc.value.copy(content = a.content)) + ToolCallStateRunning(tc.value.copy(meta = a.meta ?: tc.value.meta, content = a.content)) } } } diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 93f95a14..120512d0 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -24,6 +24,11 @@ matching `## [X.Y.Z]` heading is missing from this file. terminal / changeset / annotations slots. `reset_host` / `reset` clear the new slot. +### Fixed + +- Session reducers now apply `_meta` (`meta`) updates from every + tool-call-scoped action, not only `session/toolCallStart`. + ### Added - `AnnotationsUpdatedAction` (`annotations/updated`) — partially updates an diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 3cf00693..b7d57853 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -875,6 +875,9 @@ fn apply_tool_call_delta( ToolCallState::Streaming(mut s) => { let current = s.partial_input.unwrap_or_default(); s.partial_input = Some(current + &a.content); + if let Some(meta) = &a.meta { + s.meta = Some(meta.clone()); + } if let Some(im) = &a.invocation_message { s.invocation_message = Some(im.clone()); } @@ -890,6 +893,7 @@ fn apply_tool_call_ready( ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); + let meta = a.meta.clone().or(meta); match tc { ToolCallState::Streaming(_) | ToolCallState::Running(_) => { if let Some(confirmed) = a.confirmed { @@ -949,7 +953,7 @@ fn apply_tool_call_confirmed( let tool_name = s.tool_name; let display_name = s.display_name; let contributor = s.contributor; - let meta = s.meta; + let meta = a.meta.clone().or(s.meta); let invocation_message = s.invocation_message; let tool_input = s.tool_input; if a.approved { @@ -989,6 +993,7 @@ fn apply_tool_call_complete( ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| { let (tool_call_id, tool_name, display_name, contributor, meta) = tool_call_meta(&tc); + let meta = a.meta.clone().or(meta); let (invocation_message, tool_input, confirmed, selected_option) = match tc { ToolCallState::Running(s) => ( s.invocation_message, @@ -1056,7 +1061,7 @@ fn apply_tool_call_result_confirmed( tool_name: s.tool_name, display_name: s.display_name, contributor: s.contributor, - meta: s.meta, + meta: a.meta.clone().or(s.meta), invocation_message: s.invocation_message, tool_input: s.tool_input, success: s.success, @@ -1073,7 +1078,7 @@ fn apply_tool_call_result_confirmed( tool_name: s.tool_name, display_name: s.display_name, contributor: s.contributor, - meta: s.meta, + meta: a.meta.clone().or(s.meta), invocation_message: s.invocation_message, tool_input: s.tool_input, reason: ToolCallCancellationReason::ResultDenied, @@ -1091,6 +1096,9 @@ fn apply_tool_call_content_changed( ) -> ReduceOutcome { update_tool_call(state, &a.turn_id, &a.tool_call_id, |tc| match tc { ToolCallState::Running(mut s) => { + if let Some(meta) = &a.meta { + s.meta = Some(meta.clone()); + } s.content = Some(a.content.clone()); ToolCallState::Running(s) } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index 6ed7b835..2afac4ce 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -200,6 +200,7 @@ public struct AHPSessionReducer: Reducer { case .sessionToolCallDelta(let a): Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .streaming(var s) = tc else { return } + s.meta = a.meta ?? s.meta s.partialInput = (s.partialInput ?? "") + a.content if let msg = a.invocationMessage { s.invocationMessage = msg @@ -215,13 +216,14 @@ public struct AHPSessionReducer: Reducer { default: return } let base = tc.baseFields + let meta = a.meta ?? base.meta if let confirmed = a.confirmed { tc = .running(ToolCallRunningState( toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, status: .running, @@ -233,7 +235,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, status: .pendingConfirmation, @@ -250,6 +252,7 @@ public struct AHPSessionReducer: Reducer { Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingConfirmation(let pending) = tc else { return } let base = tc.baseFields + let meta = a.meta ?? base.meta let selectedOption = Self.resolveSelectedOption(pending.options, id: a.selectedOptionId) if a.approved { tc = .running(ToolCallRunningState( @@ -257,7 +260,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: pending.invocationMessage, toolInput: a.editedToolInput ?? pending.toolInput, status: .running, @@ -270,7 +273,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: pending.invocationMessage, toolInput: pending.toolInput, status: .cancelled, @@ -286,6 +289,7 @@ public struct AHPSessionReducer: Reducer { case .sessionToolCallComplete(let a): Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in let base = tc.baseFields + let meta = a.meta ?? base.meta let confirmed: ToolCallConfirmationReason let invocationMessage: StringOrMarkdown let toolInput: String? @@ -311,7 +315,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: invocationMessage, toolInput: toolInput, success: a.result.success, @@ -329,7 +333,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: invocationMessage, toolInput: toolInput, success: a.result.success, @@ -349,13 +353,14 @@ public struct AHPSessionReducer: Reducer { Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingResultConfirmation(let prc) = tc else { return } let base = tc.baseFields + let meta = a.meta ?? base.meta if a.approved { tc = .completed(ToolCallCompletedState( toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, success: prc.success, @@ -373,7 +378,7 @@ public struct AHPSessionReducer: Reducer { toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, status: .cancelled, @@ -526,6 +531,7 @@ public struct AHPSessionReducer: Reducer { case .sessionToolCallContentChanged(let a): Self.updateToolCallInPlace(state: &state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .running(var r) = tc else { return } + r.meta = a.meta ?? r.meta r.content = a.content tc = .running(r) } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index 4e541486..0b91b0ca 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -157,6 +157,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS case .sessionToolCallDelta(let a): return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .streaming(var s) = tc else { return tc } + s.meta = a.meta ?? s.meta s.partialInput = (s.partialInput ?? "") + a.content if let msg = a.invocationMessage { s.invocationMessage = msg @@ -172,13 +173,14 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS default: return tc } let base = tc.baseFields + let meta = a.meta ?? base.meta if let confirmed = a.confirmed { return .running(ToolCallRunningState( toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, status: .running, @@ -190,7 +192,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: a.invocationMessage, toolInput: a.toolInput, status: .pendingConfirmation, @@ -205,6 +207,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingConfirmation(let pending) = tc else { return tc } let base = tc.baseFields + let meta = a.meta ?? base.meta let selectedOption = resolveSelectedOption(pending.options, id: a.selectedOptionId) if a.approved { return .running(ToolCallRunningState( @@ -212,7 +215,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: pending.invocationMessage, toolInput: a.editedToolInput ?? pending.toolInput, status: .running, @@ -225,7 +228,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: pending.invocationMessage, toolInput: pending.toolInput, status: .cancelled, @@ -239,6 +242,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS case .sessionToolCallComplete(let a): return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in let base = tc.baseFields + let meta = a.meta ?? base.meta let confirmed: ToolCallConfirmationReason let invocationMessage: StringOrMarkdown let toolInput: String? @@ -264,7 +268,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: invocationMessage, toolInput: toolInput, success: a.result.success, @@ -282,7 +286,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: invocationMessage, toolInput: toolInput, success: a.result.success, @@ -300,13 +304,14 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS return refreshSummaryStatus(updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .pendingResultConfirmation(let prc) = tc else { return tc } let base = tc.baseFields + let meta = a.meta ?? base.meta if a.approved { return .completed(ToolCallCompletedState( toolCallId: base.toolCallId, toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, success: prc.success, @@ -324,7 +329,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS toolName: base.toolName, displayName: base.displayName, contributor: base.contributor, - meta: base.meta, + meta: meta, invocationMessage: prc.invocationMessage, toolInput: prc.toolInput, status: .cancelled, @@ -524,6 +529,7 @@ public func sessionReducer(state: SessionState, action: StateAction) -> SessionS case .sessionToolCallContentChanged(let a): return updateToolCall(state: state, turnId: a.turnId, toolCallId: a.toolCallId) { tc in guard case .running(var r) = tc else { return tc } + r.meta = a.meta ?? r.meta r.content = a.content return .running(r) } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index e7a331ad..f53cc391 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -26,6 +26,11 @@ the tag matches the version pinned in [`VERSION`](VERSION). terminal / changeset / annotations slots. `reset(host:)` / `reset()` clear the new slot. +### Fixed + +- Session reducers now apply `_meta` (`meta`) updates from every + tool-call-scoped action, not only `session/toolCallStart`. + ### Added - `AnnotationsUpdatedAction` (`annotations/updated`) — partially updates an diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 654d2dc8..14a92501 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -27,6 +27,11 @@ hotfix escape hatch. `ahp-resource-watch:` channel's descriptor alongside the existing root / session / terminal / changeset / annotations variants. +### Fixed + +- `sessionReducer` now applies `_meta` updates from every tool-call-scoped + action, not only `session/toolCallStart`. + ### Added - `AnnotationsUpdatedAction` (`annotations/updated`) — partially updates an diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 7f657a05..49b04d01 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -45,6 +45,13 @@ function tcBase(tc: ToolCallState) { }; } +function tcBaseWithMeta(tc: ToolCallState, meta: Record | undefined) { + return { + ...tcBase(tc), + _meta: meta ?? tc._meta, + }; +} + /** Resolves a selected option from the confirmation options array by ID. */ function resolveSelectedOption(options: ConfirmationOption[] | undefined, id: string | undefined): ConfirmationOption | undefined { if (!id || !options) { @@ -377,6 +384,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: } return { ...tc, + ...(action._meta !== undefined ? { _meta: action._meta } : {}), partialInput: (tc.partialInput ?? '') + action.content, invocationMessage: action.invocationMessage ?? tc.invocationMessage, }; @@ -387,7 +395,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (tc.status !== ToolCallStatus.Streaming && tc.status !== ToolCallStatus.Running) { return tc; } - const base = tcBase(tc); + const base = tcBaseWithMeta(tc, action._meta); if (action.confirmed) { return { status: ToolCallStatus.Running, @@ -414,7 +422,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (tc.status !== ToolCallStatus.PendingConfirmation) { return tc; } - const base = tcBase(tc); + const base = tcBaseWithMeta(tc, action._meta); const selectedOption = resolveSelectedOption(tc.options, action.selectedOptionId); if (action.approved) { return { @@ -443,7 +451,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (tc.status !== ToolCallStatus.Running && tc.status !== ToolCallStatus.PendingConfirmation) { return tc; } - const base = tcBase(tc); + const base = tcBaseWithMeta(tc, action._meta); const confirmed = tc.status === ToolCallStatus.Running ? tc.confirmed : ToolCallConfirmationReason.NotNeeded; @@ -477,7 +485,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (tc.status !== ToolCallStatus.PendingResultConfirmation) { return tc; } - const base = tcBase(tc); + const base = tcBaseWithMeta(tc, action._meta); if (action.approved) { return { status: ToolCallStatus.Completed, @@ -510,6 +518,7 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: } return { ...tc, + ...(action._meta !== undefined ? { _meta: action._meta } : {}), content: action.content, }; }); diff --git a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json index b06f89c3..90433f29 100644 --- a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json +++ b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json @@ -78,7 +78,10 @@ "toolName": "bash", "displayName": "Run Command", "contributor": null, - "_meta": null, + "_meta": { + "permissionKind": "shell", + "fullCommandText": "rm -rf /tmp/test" + }, "invocationMessage": "Run: rm -rf /tmp/test", "toolInput": null, "confirmationTitle": null, diff --git a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json index 66458da7..7fd6ebde 100644 --- a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json +++ b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json @@ -194,7 +194,10 @@ "toolName": "write", "displayName": "Write File", "contributor": null, - "_meta": null, + "_meta": { + "permissionKind": "write", + "path": "/tmp/out" + }, "invocationMessage": "Write to /tmp/out", "toolInput": null, "confirmed": "user-action", diff --git a/types/test-cases/reducers/220-toolcall-actions-update-meta.json b/types/test-cases/reducers/220-toolcall-actions-update-meta.json new file mode 100644 index 00000000..e173a13e --- /dev/null +++ b/types/test-cases/reducers/220-toolcall-actions-update-meta.json @@ -0,0 +1,389 @@ +{ + "description": "tool-call-scoped actions update tool call _meta", + "reducer": "session", + "initial": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 8, + "createdAt": 1000, + "modifiedAt": 2000 + }, + "lifecycle": "ready", + "turns": [], + "activeTurn": { + "id": "turn-1", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null + } + }, + "actions": [ + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-delta", + "toolName": "stream", + "displayName": "Stream Tool", + "_meta": { + "phase": "start" + } + }, + { + "type": "session/toolCallDelta", + "turnId": "turn-1", + "toolCallId": "tc-delta", + "content": "abc", + "invocationMessage": "Streaming input", + "_meta": { + "phase": "delta" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-ready-running", + "toolName": "run", + "displayName": "Run Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-ready-running", + "invocationMessage": "Run now", + "toolInput": "{}", + "confirmed": "not-needed", + "_meta": { + "phase": "ready-running" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-ready-pending", + "toolName": "ask", + "displayName": "Ask Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-ready-pending", + "invocationMessage": "Needs approval", + "_meta": { + "phase": "ready-pending" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-confirm-approved", + "toolName": "approve", + "displayName": "Approve Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-confirm-approved", + "invocationMessage": "Approve this" + }, + { + "type": "session/toolCallConfirmed", + "turnId": "turn-1", + "toolCallId": "tc-confirm-approved", + "approved": true, + "confirmed": "user-action", + "_meta": { + "phase": "confirmed-approved" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-confirm-denied", + "toolName": "deny", + "displayName": "Deny Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-confirm-denied", + "invocationMessage": "Deny this" + }, + { + "type": "session/toolCallConfirmed", + "turnId": "turn-1", + "toolCallId": "tc-confirm-denied", + "approved": false, + "reason": "denied", + "_meta": { + "phase": "confirmed-denied" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-complete", + "toolName": "complete", + "displayName": "Complete Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-complete", + "invocationMessage": "Complete this", + "confirmed": "not-needed" + }, + { + "type": "session/toolCallComplete", + "turnId": "turn-1", + "toolCallId": "tc-complete", + "result": { + "success": true, + "pastTenseMessage": "Completed" + }, + "_meta": { + "phase": "complete" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-result-confirmed", + "toolName": "result", + "displayName": "Result Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-result-confirmed", + "invocationMessage": "Result this", + "confirmed": "not-needed" + }, + { + "type": "session/toolCallComplete", + "turnId": "turn-1", + "toolCallId": "tc-result-confirmed", + "result": { + "success": true, + "pastTenseMessage": "Produced result" + }, + "requiresResultConfirmation": true, + "_meta": { + "phase": "complete-before-result" + } + }, + { + "type": "session/toolCallResultConfirmed", + "turnId": "turn-1", + "toolCallId": "tc-result-confirmed", + "approved": true, + "_meta": { + "phase": "result-confirmed" + } + }, + { + "type": "session/toolCallStart", + "turnId": "turn-1", + "toolCallId": "tc-content", + "toolName": "content", + "displayName": "Content Tool" + }, + { + "type": "session/toolCallReady", + "turnId": "turn-1", + "toolCallId": "tc-content", + "invocationMessage": "Content this", + "confirmed": "not-needed" + }, + { + "type": "session/toolCallContentChanged", + "turnId": "turn-1", + "toolCallId": "tc-content", + "content": [ + { + "type": "terminal", + "resource": "agenthost:/terminal/meta", + "title": "meta" + } + ], + "_meta": { + "phase": "content-changed" + } + } + ], + "expected": { + "summary": { + "resource": "copilot:/test-session", + "provider": "copilot", + "title": "Test Session", + "status": 24, + "createdAt": 1000, + "modifiedAt": 2000 + }, + "lifecycle": "ready", + "turns": [], + "activeTurn": { + "id": "turn-1", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [ + { + "kind": "toolCall", + "toolCall": { + "status": "streaming", + "toolCallId": "tc-delta", + "toolName": "stream", + "displayName": "Stream Tool", + "contributor": null, + "_meta": { + "phase": "delta" + }, + "partialInput": "abc", + "invocationMessage": "Streaming input" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "running", + "toolCallId": "tc-ready-running", + "toolName": "run", + "displayName": "Run Tool", + "contributor": null, + "_meta": { + "phase": "ready-running" + }, + "invocationMessage": "Run now", + "toolInput": "{}", + "confirmed": "not-needed" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "pending-confirmation", + "toolCallId": "tc-ready-pending", + "toolName": "ask", + "displayName": "Ask Tool", + "contributor": null, + "_meta": { + "phase": "ready-pending" + }, + "invocationMessage": "Needs approval", + "toolInput": null, + "confirmationTitle": null, + "edits": null, + "editable": null + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "running", + "toolCallId": "tc-confirm-approved", + "toolName": "approve", + "displayName": "Approve Tool", + "contributor": null, + "_meta": { + "phase": "confirmed-approved" + }, + "invocationMessage": "Approve this", + "toolInput": null, + "confirmed": "user-action" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "cancelled", + "toolCallId": "tc-confirm-denied", + "toolName": "deny", + "displayName": "Deny Tool", + "contributor": null, + "_meta": { + "phase": "confirmed-denied" + }, + "invocationMessage": "Deny this", + "toolInput": null, + "reason": "denied", + "reasonMessage": null, + "userSuggestion": null + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "completed", + "toolCallId": "tc-complete", + "toolName": "complete", + "displayName": "Complete Tool", + "contributor": null, + "_meta": { + "phase": "complete" + }, + "invocationMessage": "Complete this", + "toolInput": null, + "confirmed": "not-needed", + "success": true, + "pastTenseMessage": "Completed" + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "completed", + "toolCallId": "tc-result-confirmed", + "toolName": "result", + "displayName": "Result Tool", + "contributor": null, + "_meta": { + "phase": "result-confirmed" + }, + "invocationMessage": "Result this", + "toolInput": null, + "confirmed": "not-needed", + "success": true, + "pastTenseMessage": "Produced result", + "content": null, + "structuredContent": null, + "error": null + } + }, + { + "kind": "toolCall", + "toolCall": { + "status": "running", + "toolCallId": "tc-content", + "toolName": "content", + "displayName": "Content Tool", + "contributor": null, + "_meta": { + "phase": "content-changed" + }, + "invocationMessage": "Content this", + "toolInput": null, + "confirmed": "not-needed", + "content": [ + { + "type": "terminal", + "resource": "agenthost:/terminal/meta", + "title": "meta" + } + ] + } + } + ], + "usage": null + } + } +} \ No newline at end of file