diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts index f2d999520e02..27cb3234b375 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts @@ -13,6 +13,7 @@ import { parsePermissionRequest, parseSessionModeState, parseSessionUpdateEvent, + sessionModelStateFromInitialize, sessionUpdateCountsAsLoadReplayActivity, sessionUpdateIsReplay, syntheticLoadSessionResponseFromInitialize, @@ -216,6 +217,63 @@ describe("AcpRuntimeModel", () => { expect(response.modes?.availableModes).toHaveLength(2); }); + it("keeps initialize mode state whose modes have null descriptions", () => { + const response = syntheticLoadSessionResponseFromInitialize({ + protocolVersion: 1, + _meta: { + modeState: { + currentModeId: "code", + availableModes: [ + { id: "ask", name: "Ask", description: null }, + { id: "code", name: "Code", description: "Edit files", _meta: null }, + ], + _meta: null, + }, + }, + } satisfies EffectAcpSchema.InitializeResponse); + + expect(parseSessionModeState(response)).toEqual({ + currentModeId: "code", + availableModes: [ + { id: "ask", name: "Ask" }, + { id: "code", name: "Code", description: "Edit files" }, + ], + }); + }); + + it("reads initialize model state and ignores malformed model state", () => { + const modelState = { + currentModelId: "grok-4.7", + availableModels: [ + { + modelId: "grok-4.7", + name: "Grok 4.7", + description: null, + _meta: { reasoningEfforts: [{ id: "high", default: true }] }, + }, + { modelId: "grok-4.6", name: "Grok 4.6" }, + ], + }; + + expect(sessionModelStateFromInitialize({ protocolVersion: 1, _meta: { modelState } })).toEqual( + modelState, + ); + expect( + sessionModelStateFromInitialize({ + protocolVersion: 1, + _meta: { + modelState: { currentModelId: "grok-4.7", availableModels: [{ modelId: "grok-4.7" }] }, + }, + }), + ).toBeUndefined(); + expect( + sessionModelStateFromInitialize({ + protocolVersion: 1, + _meta: { modelState: { ...modelState, _meta: "not an object" } }, + }), + ).toBeUndefined(); + }); + it("projects typed ACP tool call updates into runtime events", () => { const created = parseSessionUpdateEvent({ sessionId: "session-1", diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.ts b/apps/server/src/provider/acp/AcpRuntimeModel.ts index 522d8783dec3..694f761ff8f7 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.ts @@ -3,7 +3,9 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; -import type * as EffectAcpSchema from "effect-acp/compat"; +import * as Schema from "effect/Schema"; +import * as EffectAcpSchema from "effect-acp/compat"; +import * as EffectAcpSchemaV1 from "effect-acp/schema-v1"; import { deriveToolActivityPresentation, mergeToolActivityData, @@ -19,39 +21,10 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } -function isSessionModelState(value: unknown): value is EffectAcpSchema.SessionModelState { - if (!isRecord(value) || typeof value.currentModelId !== "string") { - return false; - } - if (!Array.isArray(value.availableModels)) { - return false; - } - return value.availableModels.every( - (model) => - isRecord(model) && - typeof model.modelId === "string" && - typeof model.name === "string" && - (model.description === undefined || - model.description === null || - typeof model.description === "string"), - ); -} - -function isSessionModeState(value: unknown): value is EffectAcpSchema.SessionModeState { - if (!isRecord(value) || typeof value.currentModeId !== "string") { - return false; - } - if (!Array.isArray(value.availableModes)) { - return false; - } - return value.availableModes.every( - (mode) => - isRecord(mode) && - typeof mode.id === "string" && - typeof mode.name === "string" && - (mode.description === undefined || typeof mode.description === "string"), - ); -} +// Guards for the untyped `initialize._meta` states some agents (Grok) advertise. +// Modes were removed from ACP v2, so the v1 wire schema is the source of truth. +const isSessionModelState = Schema.is(EffectAcpSchema.SessionModelState); +const isSessionModeState = Schema.is(EffectAcpSchemaV1.SessionModeState); export interface AcpSessionMode { readonly id: string;