AIX: transfer/reassemble per-fragment opaque vendor data

This commit is contained in:
Enrico Ros
2025-11-20 19:05:12 -08:00
parent f37e65a91e
commit 7f4553225b
4 changed files with 42 additions and 2 deletions
+21 -2
View File
@@ -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<AixWire_Particles.PartParticleOp, { p: 'vp' }>): void {
private onAppendVoidPlaceholder(vp: Extract<AixWire_Particles.PartParticleOp, { p: 'vp' }>): 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<AixWire_Particles.PartParticleOp, { p: 'svs' }>): 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) {
@@ -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<string, unknown> } // 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
}
@@ -442,6 +442,19 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
} satisfies Extract<AixWire_Particles.PartParticleOp, { p: 'vp' }>);
}
/**
* 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<string, unknown>) {
// 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<AixWire_Particles.PartParticleOp, { p: 'svs' }>);
}
/** Communicates the model name to the client */
setModelName(modelName: string) {
this.transmissionQueue.push({
@@ -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 */