diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index bc7828dd854..d7dcb0ada9e 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -38,6 +38,7 @@ const emitOverlappingXAiPromptCompleteOutOfOrder = const failPrompt = process.env.T3_ACP_FAIL_PROMPT === "1"; const failSetConfigOption = process.env.T3_ACP_FAIL_SET_CONFIG_OPTION === "1"; const exitOnSetConfigOption = process.env.T3_ACP_EXIT_ON_SET_CONFIG_OPTION === "1"; +const emitSessionInfoUpdate = process.env.T3_ACP_EMIT_SESSION_INFO === "1"; const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT; const promptDelayMs = Number(process.env.T3_ACP_PROMPT_DELAY_MS ?? "0"); const permissionOptionIds = { @@ -68,6 +69,17 @@ function promptIdFromRequestMeta( return typeof promptId === "string" && promptId.length > 0 ? promptId : undefined; } +function promptTextFromRequest(request: AcpSchema.PromptRequest): string { + const parts = Array.isArray(request.prompt) ? request.prompt : []; + return parts + .flatMap((part) => + part && typeof part === "object" && "type" in part && part.type === "text" && "text" in part + ? [String(part.text)] + : [], + ) + .join(""); +} + function logExit(reason: string): void { if (!exitLogPath) { return; @@ -79,6 +91,10 @@ function writeJsonRpcNotification(method: string, params: unknown): void { process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`); } +function writeJsonRpcResponse(id: string | number, result: unknown): void { + process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`); +} + process.once("SIGTERM", () => { logExit("SIGTERM"); process.exit(0); @@ -300,9 +316,33 @@ const program = Effect.gen(function* () { Effect.sync(() => { parameterizedModelPicker = request.clientCapabilities?._meta?.parameterizedModelPicker === true; + // #4109-class: unsolicited response with non-numeric id must not crash the client. + if (process.env.T3_ACP_EMIT_SKILLS_RELOAD_ID === "1") { + queueMicrotask(() => { + writeJsonRpcResponse("skills-reload", { ok: true }); + }); + } + const initMeta = + process.env.T3_ACP_EMIT_INIT_AVAILABLE_COMMANDS === "1" + ? { + availableCommands: [ + { + name: "compact", + description: "Compress conversation history", + input: { hint: "optional context" }, + }, + { + name: "session-info", + description: "Show session details", + input: null, + }, + ], + } + : undefined; return { protocolVersion: 1, agentCapabilities: { loadSession: true }, + ...(initMeta ? { _meta: initMeta } : {}), }; }), ); @@ -465,6 +505,21 @@ const program = Effect.gen(function* () { return yield* AcpError.AcpRequestError.internalError("Mock prompt failure"); } + // Mirror real Grok: `/compact` is handled as a prompt and emits auto_compact_completed. + const promptText = promptTextFromRequest(request).trim(); + if (/^\/compact(?:\s|$)/i.test(promptText)) { + writeJsonRpcNotification("_x.ai/session_notification", { + sessionId: requestedSessionId, + update: { + sessionUpdate: "auto_compact_completed", + tokens_before: 12_000, + tokens_after: 4_000, + summary_preview: null, + }, + }); + return { stopReason: "end_turn" }; + } + if (emitStaleXAiPromptCompleteBeforeSecondHang && promptCount === 1) { return { stopReason: "end_turn", @@ -865,6 +920,16 @@ const program = Effect.gen(function* () { }, }); + if (emitSessionInfoUpdate) { + yield* agent.client.sessionUpdate({ + sessionId: requestedSessionId, + update: { + sessionUpdate: "session_info_update", + title: "Mock Grok session title", + }, + }); + } + yield* agent.client.sessionUpdate({ sessionId: requestedSessionId, update: { @@ -873,7 +938,17 @@ const program = Effect.gen(function* () { }, }); - return { stopReason: "end_turn" }; + // Live Grok stamps usage on prompt result `_meta` (not only usage_update). + return { + stopReason: "end_turn", + _meta: { + totalTokens: 12_345, + inputTokens: 10_000, + outputTokens: 2_000, + cachedReadTokens: 8_000, + reasoningTokens: 345, + }, + }; }), ); diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts index 80475a5c269..a033d650c49 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.ts @@ -57,6 +57,7 @@ import { makeAcpPlanUpdatedEvent, makeAcpRequestOpenedEvent, makeAcpRequestResolvedEvent, + makeAcpTokenUsageEvent, makeAcpToolCallEvent, } from "../acp/AcpCoreRuntimeEvents.ts"; import { @@ -863,6 +864,28 @@ export function makeCursorAdapter( turnId: ctx.activeTurnId, ...(event.itemId ? { itemId: event.itemId } : {}), text: event.text, + streamKind: event.streamKind, + rawPayload: event.rawPayload, + }), + ); + return; + case "UsageUpdated": + // Shared ACP consumer for usage_update (parser → runtime event). + // Same wiring pattern as ContentDelta/streamKind so the helper is live. + yield* logNative( + ctx.threadId, + "session/update", + event.rawPayload, + "acp.jsonrpc", + ); + yield* offerRuntimeEvent( + makeAcpTokenUsageEvent({ + stamp: yield* makeEventStamp(), + provider: PROVIDER, + threadId: ctx.threadId, + turnId: ctx.activeTurnId, + usedTokens: event.usage.used, + ...(event.usage.size > 0 ? { maxTokens: event.usage.size } : {}), rawPayload: event.rawPayload, }), ); diff --git a/apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts b/apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts index 7fe25699bbc..df5f292b4dc 100644 --- a/apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts +++ b/apps/server/src/provider/acp/AcpCoreRuntimeEvents.test.ts @@ -7,6 +7,7 @@ import { makeAcpPlanUpdatedEvent, makeAcpRequestOpenedEvent, makeAcpRequestResolvedEvent, + makeAcpTokenUsageEvent, makeAcpToolCallEvent, } from "./AcpCoreRuntimeEvents.ts"; @@ -124,16 +125,64 @@ describe("AcpCoreRuntimeEvents", () => { turnId, itemId: "assistant:session-1:segment:0", text: "hello", + streamKind: "reasoning_text", rawPayload: { sessionId: "session-1" }, }), ).toMatchObject({ type: "content.delta", itemId: "assistant:session-1:segment:0", payload: { + streamKind: "reasoning_text", delta: "hello", }, }); + const usageWithoutCompact = makeAcpTokenUsageEvent({ + stamp, + provider: ProviderDriverKind.make("cursor"), + threadId: "thread-1" as never, + turnId, + usedTokens: 1200, + maxTokens: 256_000, + rawPayload: { + sessionId: "session-1", + update: { sessionUpdate: "usage_update", used: 1200, size: 256_000 }, + }, + }); + expect(usageWithoutCompact).toMatchObject({ + type: "thread.token-usage.updated", + payload: { + usage: { + usedTokens: 1200, + lastUsedTokens: 1200, + maxTokens: 256_000, + }, + }, + }); + // Generic ACP must not hardcode auto-compact; omit when unknown. + expect( + (usageWithoutCompact as { payload: { usage: Record } }).payload.usage + .compactsAutomatically, + ).toBeUndefined(); + + const usageWithCompact = makeAcpTokenUsageEvent({ + stamp, + provider: ProviderDriverKind.make("cursor"), + threadId: "thread-1" as never, + turnId, + usedTokens: 1200, + maxTokens: 256_000, + compactsAutomatically: true, + rawPayload: { + sessionId: "session-1", + update: { sessionUpdate: "usage_update", used: 1200, size: 256_000 }, + }, + }); + expect( + (usageWithCompact as { payload: { usage: Record } }).payload.usage + .compactsAutomatically, + ).toBe(true); + expect( makeAcpAssistantItemEvent({ stamp, diff --git a/apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts b/apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts index c93e61dc37b..7f4b4f7fe70 100644 --- a/apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts +++ b/apps/server/src/provider/acp/AcpCoreRuntimeEvents.ts @@ -220,6 +220,7 @@ export function makeAcpContentDeltaEvent(input: { readonly turnId: TurnId | undefined; readonly itemId?: string; readonly text: string; + readonly streamKind?: "assistant_text" | "reasoning_text"; readonly rawPayload: unknown; }): ProviderRuntimeEvent { return { @@ -230,7 +231,7 @@ export function makeAcpContentDeltaEvent(input: { turnId: input.turnId, ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), payload: { - streamKind: "assistant_text", + streamKind: input.streamKind ?? "assistant_text", delta: input.text, }, raw: { @@ -240,3 +241,65 @@ export function makeAcpContentDeltaEvent(input: { }, }; } + +export function makeAcpTokenUsageEvent(input: { + readonly stamp: AcpEventStamp; + readonly provider: ProviderDriverKind; + readonly threadId: ThreadId; + readonly turnId: TurnId | undefined; + readonly usedTokens: number; + readonly maxTokens?: number; + readonly inputTokens?: number; + readonly outputTokens?: number; + readonly cachedInputTokens?: number; + readonly reasoningOutputTokens?: number; + /** + * Only set when the agent/provider is known to auto-compact. + * Generic ACP (e.g. Cursor) often does not; omit when unknown so UI defaults to false. + */ + readonly compactsAutomatically?: boolean; + readonly rawPayload: unknown; + readonly source?: AcpAdapterRawSource; + readonly method?: string; +}): ProviderRuntimeEvent { + return { + type: "thread.token-usage.updated", + ...input.stamp, + provider: input.provider, + threadId: input.threadId, + turnId: input.turnId, + payload: { + usage: { + usedTokens: input.usedTokens, + lastUsedTokens: input.usedTokens, + ...(input.maxTokens !== undefined && input.maxTokens > 0 + ? { maxTokens: input.maxTokens } + : {}), + ...(input.inputTokens !== undefined ? { inputTokens: input.inputTokens } : {}), + ...(input.outputTokens !== undefined ? { outputTokens: input.outputTokens } : {}), + ...(input.cachedInputTokens !== undefined + ? { cachedInputTokens: input.cachedInputTokens } + : {}), + ...(input.reasoningOutputTokens !== undefined + ? { reasoningOutputTokens: input.reasoningOutputTokens } + : {}), + ...(input.inputTokens !== undefined ? { lastInputTokens: input.inputTokens } : {}), + ...(input.outputTokens !== undefined ? { lastOutputTokens: input.outputTokens } : {}), + ...(input.cachedInputTokens !== undefined + ? { lastCachedInputTokens: input.cachedInputTokens } + : {}), + ...(input.reasoningOutputTokens !== undefined + ? { lastReasoningOutputTokens: input.reasoningOutputTokens } + : {}), + ...(input.compactsAutomatically !== undefined + ? { compactsAutomatically: input.compactsAutomatically } + : {}), + }, + }, + raw: { + source: input.source ?? "acp.jsonrpc", + method: input.method ?? "session/update", + payload: input.rawPayload, + }, + }; +} diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts index 7682c5f5f9c..81481c01a70 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.test.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.test.ts @@ -1,7 +1,9 @@ +import { ProviderDriverKind, TurnId } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import type * as EffectAcpSchema from "effect-acp/schema"; +import { makeAcpTokenUsageEvent } from "./AcpCoreRuntimeEvents.ts"; import { extractModelConfigId, mergeToolCallState, @@ -322,6 +324,7 @@ describe("AcpRuntimeModel", () => { { _tag: "ContentDelta", text: "hello from acp", + streamKind: "assistant_text", rawPayload: { sessionId: "session-1", update: { @@ -336,6 +339,122 @@ describe("AcpRuntimeModel", () => { ]); }); + it("parses thought, usage, commands, config, session info, and user chunks", () => { + const thought = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "agent_thought_chunk", + content: { type: "text", text: "thinking" }, + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(thought.events[0]).toMatchObject({ + _tag: "ContentDelta", + text: "thinking", + streamKind: "reasoning_text", + }); + + const usage = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "usage_update", + used: 1200, + size: 256000, + cost: { amount: 0.01, currency: "USD" }, + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(usage.events[0]).toMatchObject({ + _tag: "UsageUpdated", + usage: { used: 1200, size: 256000, costAmount: 0.01, costCurrency: "USD" }, + }); + // Parser → shared runtime event factory (CursorAdapter / GrokAdapter consumer path). + const usageEvent = usage.events[0]; + if (usageEvent?._tag !== "UsageUpdated") { + throw new Error("expected UsageUpdated"); + } + expect( + makeAcpTokenUsageEvent({ + stamp: { eventId: "event-usage" as never, createdAt: "2026-03-27T00:00:00.000Z" }, + provider: ProviderDriverKind.make("cursor"), + threadId: "thread-1" as never, + turnId: TurnId.make("turn-1"), + usedTokens: usageEvent.usage.used, + maxTokens: usageEvent.usage.size, + rawPayload: usageEvent.rawPayload, + }), + ).toMatchObject({ + type: "thread.token-usage.updated", + payload: { + usage: { + usedTokens: 1200, + maxTokens: 256000, + }, + }, + }); + + const commands = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "available_commands_update", + availableCommands: [ + { name: " review ", description: " Review code ", input: { hint: " path " } }, + { name: "", description: "skip" }, + ], + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(commands.events[0]).toMatchObject({ + _tag: "AvailableCommandsUpdated", + commands: [{ name: "review", description: "Review code", inputHint: "path" }], + }); + + const config = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "config_option_update", + configOptions: [ + { + id: "effort", + name: "Reasoning", + category: "thought_level", + type: "select", + currentValue: "high", + options: [{ value: "high", name: "High" }], + }, + ], + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(config.events[0]).toMatchObject({ + _tag: "ConfigOptionsUpdated", + configOptions: [ + { + id: "effort", + currentValue: "high", + }, + ], + }); + + const info = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "session_info_update", + title: "My session", + updatedAt: "2026-08-05T00:00:00Z", + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(info.events[0]).toMatchObject({ + _tag: "SessionInfoUpdated", + info: { title: "My session", updatedAt: "2026-08-05T00:00:00Z" }, + }); + + const userChunk = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { + sessionUpdate: "user_message_chunk", + content: { type: "text", text: "echo" }, + }, + } satisfies EffectAcpSchema.SessionNotification); + expect(userChunk.events[0]?._tag).toBe("UserMessageChunk"); + }); + it("keeps permission request parsing compatible with loose extension payloads", () => { const request = parsePermissionRequest({ sessionId: "session-1", diff --git a/apps/server/src/provider/acp/AcpRuntimeModel.ts b/apps/server/src/provider/acp/AcpRuntimeModel.ts index e6bfc127e6e..e7c0c98b11f 100644 --- a/apps/server/src/provider/acp/AcpRuntimeModel.ts +++ b/apps/server/src/provider/acp/AcpRuntimeModel.ts @@ -80,6 +80,26 @@ export interface AcpPermissionRequest { readonly toolCall?: AcpToolCallState; } +export type AcpContentStreamKind = "assistant_text" | "reasoning_text"; + +export interface AcpAvailableCommand { + readonly name: string; + readonly description?: string; + readonly inputHint?: string; +} + +export interface AcpUsageUpdate { + readonly used: number; + readonly size: number; + readonly costAmount?: number; + readonly costCurrency?: string; +} + +export interface AcpSessionInfoUpdate { + readonly title?: string | null; + readonly updatedAt?: string | null; +} + export type AcpParsedSessionEvent = | { readonly _tag: "ModeChanged"; @@ -107,6 +127,37 @@ export type AcpParsedSessionEvent = readonly _tag: "ContentDelta"; readonly itemId?: string; readonly text: string; + readonly streamKind: AcpContentStreamKind; + readonly rawPayload: unknown; + } + | { + readonly _tag: "AvailableCommandsUpdated"; + readonly commands: ReadonlyArray; + readonly rawPayload: unknown; + } + | { + readonly _tag: "UsageUpdated"; + readonly usage: AcpUsageUpdate; + readonly rawPayload: unknown; + } + | { + readonly _tag: "ConfigOptionsUpdated"; + readonly configOptions: ReadonlyArray; + readonly rawPayload: unknown; + } + | { + readonly _tag: "SessionInfoUpdated"; + readonly info: AcpSessionInfoUpdate; + readonly rawPayload: unknown; + } + | { + readonly _tag: "UserMessageChunk"; + readonly text: string; + readonly rawPayload: unknown; + } + | { + readonly _tag: "UnknownSessionUpdate"; + readonly sessionUpdate: string; readonly rawPayload: unknown; }; @@ -569,13 +620,111 @@ export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotificat events.push({ _tag: "ContentDelta", text: upd.content.text, + streamKind: "assistant_text", rawPayload: params, }); } break; } - default: + case "agent_thought_chunk": { + if (upd.content.type === "text" && upd.content.text.length > 0) { + events.push({ + _tag: "ContentDelta", + text: upd.content.text, + streamKind: "reasoning_text", + rawPayload: params, + }); + } + break; + } + case "user_message_chunk": { + if (upd.content.type === "text" && upd.content.text.length > 0) { + events.push({ + _tag: "UserMessageChunk", + text: upd.content.text, + rawPayload: params, + }); + } + break; + } + case "available_commands_update": { + const commands = upd.availableCommands.flatMap((command): AcpAvailableCommand[] => { + const name = command.name.trim(); + if (!name) { + return []; + } + const description = command.description.trim(); + const inputHint = + command.input && "hint" in command.input && typeof command.input.hint === "string" + ? command.input.hint.trim() + : undefined; + return [ + { + name, + ...(description.length > 0 ? { description } : {}), + ...(inputHint && inputHint.length > 0 ? { inputHint } : {}), + }, + ]; + }); + events.push({ + _tag: "AvailableCommandsUpdated", + commands, + rawPayload: params, + }); + break; + } + case "usage_update": { + const used = Number.isFinite(upd.used) && upd.used >= 0 ? Math.trunc(upd.used) : undefined; + if (used === undefined) { + break; + } + // Live Grok may omit size; consumers fall back to model totalContextTokens. + const size = Number.isFinite(upd.size) && upd.size >= 0 ? Math.trunc(upd.size) : 0; + const cost = upd.cost; + events.push({ + _tag: "UsageUpdated", + usage: { + used, + size, + ...(cost && typeof cost.amount === "number" && typeof cost.currency === "string" + ? { costAmount: cost.amount, costCurrency: cost.currency } + : {}), + }, + rawPayload: params, + }); + break; + } + case "config_option_update": { + events.push({ + _tag: "ConfigOptionsUpdated", + configOptions: upd.configOptions, + rawPayload: params, + }); + break; + } + case "session_info_update": { + events.push({ + _tag: "SessionInfoUpdated", + info: { + ...(upd.title !== undefined ? { title: upd.title } : {}), + ...(upd.updatedAt !== undefined ? { updatedAt: upd.updatedAt } : {}), + }, + rawPayload: params, + }); break; + } + default: { + // Exhaustive against current ACP schema; keep a fallback for forward compatibility. + const unknownUpdate = upd as { readonly sessionUpdate?: unknown }; + const sessionUpdate = + typeof unknownUpdate.sessionUpdate === "string" ? unknownUpdate.sessionUpdate : "unknown"; + events.push({ + _tag: "UnknownSessionUpdate", + sessionUpdate, + rawPayload: params, + }); + break; + } } return { ...(modeId !== undefined ? { modeId } : {}), events }; diff --git a/apps/server/src/provider/acp/AcpSessionRuntime.ts b/apps/server/src/provider/acp/AcpSessionRuntime.ts index 09fce6d56f9..c03fa38f71d 100644 --- a/apps/server/src/provider/acp/AcpSessionRuntime.ts +++ b/apps/server/src/provider/acp/AcpSessionRuntime.ts @@ -94,6 +94,8 @@ export interface AcpSessionRuntimeStartResult { | EffectAcpSchema.NewSessionResponse | EffectAcpSchema.ResumeSessionResponse; readonly modelConfigId: string | undefined; + /** Authenticate response payload when the agent returned one (process-observed). */ + readonly authenticateResult?: EffectAcpSchema.AuthenticateResponse; } export class AcpSessionRuntime extends Context.Service< @@ -222,10 +224,14 @@ export class AcpSessionRuntime extends Context.Service< readonly setModel: (model: string) => Effect.Effect; /** * Selects the active model through the unstable ACP `session/set_model` capability. + * Optional `_meta` is passed through for agent extensions (e.g. Grok `reasoningEffort`). * @see https://agentclientprotocol.com/protocol/schema#session/set_model */ readonly setSessionModel: ( modelId: string, + options?: { + readonly _meta?: { readonly [x: string]: unknown } | null; + }, ) => Effect.Effect; /** * Sends a generic ACP extension request and records it through the request logger. @@ -396,6 +402,7 @@ export const make = ( yield* handleSessionUpdate({ queue: eventQueue, modeStateRef, + configOptionsRef, toolCallsRef, assistantSegmentRef, assistantItemRuntimeId, @@ -545,7 +552,7 @@ export const make = ( methodId: options.authMethodId, } satisfies EffectAcpSchema.AuthenticateRequest; - yield* runLoggedRequest( + const authenticateResult = yield* runLoggedRequest( "authenticate", authenticatePayload, acp.agent.authenticate(authenticatePayload), @@ -652,6 +659,7 @@ export const make = ( initializeResult, sessionSetupResult, modelConfigId: extractModelConfigId(sessionSetupResult), + authenticateResult, } satisfies AcpStartedState; return nextState; }); @@ -789,12 +797,13 @@ export const make = ( Effect.flatMap((started) => setConfigOption(started.modelConfigId ?? "model", model)), Effect.asVoid, ), - setSessionModel: (modelId) => + setSessionModel: (modelId, options) => getStartedState.pipe( Effect.flatMap((started) => { const requestPayload = { sessionId: started.sessionId, modelId, + ...(options?._meta !== undefined ? { _meta: options._meta } : {}), } satisfies EffectAcpSchema.SetSessionModelRequest; return runLoggedRequest( "session/set_model", @@ -844,6 +853,7 @@ function configOptionCurrentValueMatches( const handleSessionUpdate = ({ queue, modeStateRef, + configOptionsRef, toolCallsRef, assistantSegmentRef, assistantItemRuntimeId, @@ -851,6 +861,7 @@ const handleSessionUpdate = ({ }: { readonly queue: Queue.Queue; readonly modeStateRef: Ref.Ref; + readonly configOptionsRef: Ref.Ref>; readonly toolCallsRef: Ref.Ref>; readonly assistantSegmentRef: Ref.Ref; readonly assistantItemRuntimeId: string; @@ -864,6 +875,14 @@ const handleSessionUpdate = ({ ); } for (const event of parsed.events) { + if (event._tag === "ConfigOptionsUpdated") { + // Keep authoritative session config in sync with live agent updates so + // getConfigOptions / validateConfigOptionValue / setConfigOption current-value + // checks do not stay pinned to the setup-time snapshot. + yield* Ref.set(configOptionsRef, event.configOptions); + yield* Queue.offer(queue, event); + continue; + } if (event._tag === "ToolCallUpdated") { yield* closeActiveAssistantSegment({ queue,