| author | |
| committer | |
| log | 27ab45593fdcaffe718c1fa54dcbb2df4bd769a2 |
| tree | 34886c62a786e6beecda02bd904e20ae47312b00 |
| parent | 3da82d5d384a5f45e3e4a9c5bdb8336eb54e49ff |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>2 files changed, 65 insertions(+), 34 deletions(-)
apps/server/src/provider/acp/AcpRuntimeModel.test.ts+58| ... | ... | @@ -13,6 +13,7 @@ import { |
| 13 | 13 | parsePermissionRequest, |
| 14 | 14 | parseSessionModeState, |
| 15 | 15 | parseSessionUpdateEvent, |
| 16 | sessionModelStateFromInitialize, | |
| 16 | 17 | sessionUpdateCountsAsLoadReplayActivity, |
| 17 | 18 | sessionUpdateIsReplay, |
| 18 | 19 | syntheticLoadSessionResponseFromInitialize, |
| ... | ... | @@ -216,6 +217,63 @@ describe("AcpRuntimeModel", () => { |
| 216 | 217 | expect(response.modes?.availableModes).toHaveLength(2); |
| 217 | 218 | }); |
| 218 | 219 | |
| 220 | it("keeps initialize mode state whose modes have null descriptions", () => { | |
| 221 | const response = syntheticLoadSessionResponseFromInitialize({ | |
| 222 | protocolVersion: 1, | |
| 223 | _meta: { | |
| 224 | modeState: { | |
| 225 | currentModeId: "code", | |
| 226 | availableModes: [ | |
| 227 | { id: "ask", name: "Ask", description: null }, | |
| 228 | { id: "code", name: "Code", description: "Edit files", _meta: null }, | |
| 229 | ], | |
| 230 | _meta: null, | |
| 231 | }, | |
| 232 | }, | |
| 233 | } satisfies EffectAcpSchema.InitializeResponse); | |
| 234 | ||
| 235 | expect(parseSessionModeState(response)).toEqual({ | |
| 236 | currentModeId: "code", | |
| 237 | availableModes: [ | |
| 238 | { id: "ask", name: "Ask" }, | |
| 239 | { id: "code", name: "Code", description: "Edit files" }, | |
| 240 | ], | |
| 241 | }); | |
| 242 | }); | |
| 243 | ||
| 244 | it("reads initialize model state and ignores malformed model state", () => { | |
| 245 | const modelState = { | |
| 246 | currentModelId: "grok-4.7", | |
| 247 | availableModels: [ | |
| 248 | { | |
| 249 | modelId: "grok-4.7", | |
| 250 | name: "Grok 4.7", | |
| 251 | description: null, | |
| 252 | _meta: { reasoningEfforts: [{ id: "high", default: true }] }, | |
| 253 | }, | |
| 254 | { modelId: "grok-4.6", name: "Grok 4.6" }, | |
| 255 | ], | |
| 256 | }; | |
| 257 | ||
| 258 | expect(sessionModelStateFromInitialize({ protocolVersion: 1, _meta: { modelState } })).toEqual( | |
| 259 | modelState, | |
| 260 | ); | |
| 261 | expect( | |
| 262 | sessionModelStateFromInitialize({ | |
| 263 | protocolVersion: 1, | |
| 264 | _meta: { | |
| 265 | modelState: { currentModelId: "grok-4.7", availableModels: [{ modelId: "grok-4.7" }] }, | |
| 266 | }, | |
| 267 | }), | |
| 268 | ).toBeUndefined(); | |
| 269 | expect( | |
| 270 | sessionModelStateFromInitialize({ | |
| 271 | protocolVersion: 1, | |
| 272 | _meta: { modelState: { ...modelState, _meta: "not an object" } }, | |
| 273 | }), | |
| 274 | ).toBeUndefined(); | |
| 275 | }); | |
| 276 | ||
| 219 | 277 | it("projects typed ACP tool call updates into runtime events", () => { |
| 220 | 278 | const created = parseSessionUpdateEvent({ |
| 221 | 279 | sessionId: "session-1", |
apps/server/src/provider/acp/AcpRuntimeModel.ts+7-34| ... | ... | @@ -3,7 +3,9 @@ import * as Duration from "effect/Duration"; |
| 3 | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | import * as Option from "effect/Option"; |
| 5 | 5 | import * as Ref from "effect/Ref"; |
| 6 | import type * as EffectAcpSchema from "effect-acp/compat"; | |
| 6 | import * as Schema from "effect/Schema"; | |
| 7 | import * as EffectAcpSchema from "effect-acp/compat"; | |
| 8 | import * as EffectAcpSchemaV1 from "effect-acp/schema-v1"; | |
| 7 | 9 | import { |
| 8 | 10 | deriveToolActivityPresentation, |
| 9 | 11 | mergeToolActivityData, |
| ... | ... | @@ -19,39 +21,10 @@ function isRecord(value: unknown): value is Record<string, unknown> { |
| 19 | 21 | return typeof value === "object" && value !== null && !Array.isArray(value); |
| 20 | 22 | } |
| 21 | 23 | |
| 22 | function isSessionModelState(value: unknown): value is EffectAcpSchema.SessionModelState { | |
| 23 | if (!isRecord(value) || typeof value.currentModelId !== "string") { | |
| 24 | return false; | |
| 25 | } | |
| 26 | if (!Array.isArray(value.availableModels)) { | |
| 27 | return false; | |
| 28 | } | |
| 29 | return value.availableModels.every( | |
| 30 | (model) => | |
| 31 | isRecord(model) && | |
| 32 | typeof model.modelId === "string" && | |
| 33 | typeof model.name === "string" && | |
| 34 | (model.description === undefined || | |
| 35 | model.description === null || | |
| 36 | typeof model.description === "string"), | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | function isSessionModeState(value: unknown): value is EffectAcpSchema.SessionModeState { | |
| 41 | if (!isRecord(value) || typeof value.currentModeId !== "string") { | |
| 42 | return false; | |
| 43 | } | |
| 44 | if (!Array.isArray(value.availableModes)) { | |
| 45 | return false; | |
| 46 | } | |
| 47 | return value.availableModes.every( | |
| 48 | (mode) => | |
| 49 | isRecord(mode) && | |
| 50 | typeof mode.id === "string" && | |
| 51 | typeof mode.name === "string" && | |
| 52 | (mode.description === undefined || typeof mode.description === "string"), | |
| 53 | ); | |
| 54 | } | |
| 24 | // Guards for the untyped `initialize._meta` states some agents (Grok) advertise. | |
| 25 | // Modes were removed from ACP v2, so the v1 wire schema is the source of truth. | |
| 26 | const isSessionModelState = Schema.is(EffectAcpSchema.SessionModelState); | |
| 27 | const isSessionModeState = Schema.is(EffectAcpSchemaV1.SessionModeState); | |
| 55 | 28 | |
| 56 | 29 | export interface AcpSessionMode { |
| 57 | 30 | readonly id: string; |