diff --git a/src/modules/aix/client/ContentReassembler.ts b/src/modules/aix/client/ContentReassembler.ts index c7f895a29..2217280c2 100644 --- a/src/modules/aix/client/ContentReassembler.ts +++ b/src/modules/aix/client/ContentReassembler.ts @@ -238,11 +238,14 @@ export class ContentReassembler { case 'ii': await this.onAppendInlineImage(op); break; + case 'svs': + this.onSetVendorState(op); + break; case 'urlc': this.onAddUrlCitation(op); break; case 'vp': - this.onVoidPlaceholder(op); + this.onAppendVoidPlaceholder(op); break; default: // noinspection JSUnusedLocalSymbols @@ -579,7 +582,7 @@ export class ContentReassembler { // This ensures we don't interrupt the text flow } - private onVoidPlaceholder(vp: Extract): void { + private onAppendVoidPlaceholder(vp: Extract): void { const { text, mot } = vp; // update the model op @@ -603,6 +606,22 @@ export class ContentReassembler { // Placeholders don't affect text fragment indexing } + private onSetVendorState(vs: Extract): void { + // apply vendor state to the last created fragment + const lastFragment = this.accumulator.fragments[this.accumulator.fragments.length - 1]; + if (!lastFragment) { + console.warn('[ContentReassembler] Vendor state particle without preceding content fragment'); + return; + } + + // attach vendor state + const { vendor, state } = vs; + lastFragment.vendorState = { + ...lastFragment.vendorState, + [vendor]: state, + } + } + // Helper to remove placeholder when real content arrives private removePlaceholderIfAtIndex0(): void { if (this.accumulator.fragments.length > 0) { diff --git a/src/modules/aix/server/api/aix.wiretypes.ts b/src/modules/aix/server/api/aix.wiretypes.ts index 1e49595d2..bfaf29c20 100644 --- a/src/modules/aix/server/api/aix.wiretypes.ts +++ b/src/modules/aix/server/api/aix.wiretypes.ts @@ -213,6 +213,7 @@ export namespace AixWire_Parts { // Model Auxiliary Part (for thinking blocks) + // NOTE: not a _BasePart_schema for now, may become if we put the vndAnt attributes there export const ModelAuxPart_schema = z.object({ pt: z.literal('ma'), aType: z.literal('reasoning'), @@ -669,6 +670,7 @@ export namespace AixWire_Particles { | { p: 'ia', mimeType: string, a_b64: string, label?: string, generator?: string, durationMs?: number } // inline audio, complete | { p: 'ii', mimeType: string, i_b64: string, label?: string, generator?: string, prompt?: string } // inline image, complete | { p: 'urlc', title: string, url: string, num?: number, from?: number, to?: number, text?: string, pubTs?: number } // url citation - pubTs: publication timestamp + | { p: 'svs', vendor: string, state: Record } // set vendor state - applies to the last emitted part (opaque protocol state) | { p: 'vp', text: string, mot: 'search-web' | 'gen-image' | 'code-exec' }; // void placeholder - temporary status text that gets wiped when real content arrives } diff --git a/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts b/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts index 1a02920ae..39c75464e 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/ChatGenerateTransmitter.ts @@ -442,6 +442,19 @@ export class ChatGenerateTransmitter implements IParticleTransmitter { } satisfies Extract); } + /** + * Sends vendor-specific state modifier for the last emitted part. + * This attaches opaque protocol state (e.g., Gemini thoughtSignature) without polluting core part schemas. + */ + sendSetVendorState(vendor: string, state: Record) { + // queue vendor state particle immediately after the content part has been queued (and if text, it will be emitted sooner anyway) + this.transmissionQueue.push({ + p: 'svs', + vendor, + state, + } satisfies Extract); + } + /** Communicates the model name to the client */ setModelName(modelName: string) { this.transmissionQueue.push({ diff --git a/src/modules/aix/server/dispatch/chatGenerate/parsers/IParticleTransmitter.ts b/src/modules/aix/server/dispatch/chatGenerate/parsers/IParticleTransmitter.ts index fa463fdb7..41a1c4f0b 100644 --- a/src/modules/aix/server/dispatch/chatGenerate/parsers/IParticleTransmitter.ts +++ b/src/modules/aix/server/dispatch/chatGenerate/parsers/IParticleTransmitter.ts @@ -69,6 +69,12 @@ export interface IParticleTransmitter { /** Sends a void placeholder particle - temporary status that gets wiped when real content arrives */ sendVoidPlaceholder(mot: 'search-web' | 'gen-image' | 'code-exec', text: string): void; + /** + * Sends vendor-specific state modifier for the last emitted part. + * Used to attach opaque protocol state (e.g., Gemini thoughtSignature) without polluting core part schemas. + */ + sendSetVendorState(vendor: string, state: unknown): void; + // Non-parts data // /** Communicates the model name to the client */