blob: 2a255d333ac692264604bdf16c7aff0b849bf757 [file]
import type { ModelMessage, NormalizedUsage, ToolCallPart } from './model-protocol.js';
export interface CompletedProviderStep {
toolCalls?: readonly ToolCallPart[];
usage?: NormalizedUsage;
}
export interface RequestProjectionContext {
completedSteps: readonly CompletedProviderStep[];
stepNumber: number;
model: unknown;
messages: ModelMessage[];
activeTools?: readonly string[];
}
export interface RequestProjection {
activeTools?: string[];
messages?: ModelMessage[];
}
export type RequestProjectionStage = (
context: RequestProjectionContext,
) => RequestProjection | undefined | PromiseLike<RequestProjection | undefined>;
/**
* Deterministic request-projection pipeline over ONE provider-visible request.
* Order is a contract: mid-turn capacity compaction runs first among the
* message-shaping hooks so every later mechanism operates on (and re-converges
* onto) its projection — active tool-result pruning re-archives large tool
* results in the rebuilt tail, and semantic/active-full compaction sees the
* already-compacted messages. On a step where the capacity hook replaced the
* request, semantic/active-full compaction yields (see AiSdkBackend.send) so
* two summarizers never run for one step.
*
* Every hook here only SHAPES the projection. The pass/terminate capacity
* verdict is issued once, after the whole pipeline, by the final-request
* estimate owner (buildMidTurnFinalRequestVerdict) over the actual outgoing
* (messages, tools) payload — never by an individual hook over an intermediate
* projection that a later hook could still rescue.
*/
export function composeRequestProjection(
toolAvailability: RequestProjectionStage | undefined,
midTurnCapacityCompact: RequestProjectionStage | undefined,
activeToolResultPrune: RequestProjectionStage | undefined,
activeFullCompact?: RequestProjectionStage | undefined,
): RequestProjectionStage | undefined {
const hooks = [
toolAvailability,
midTurnCapacityCompact,
activeToolResultPrune,
activeFullCompact,
].filter(Boolean) as RequestProjectionStage[];
if (hooks.length === 0) return undefined;
return async (context: RequestProjectionContext): Promise<RequestProjection | undefined> => {
let result: RequestProjection | undefined;
let messages = context.messages;
for (const hook of hooks) {
const hookOptions = {
...context,
messages,
...(result?.activeTools ? { activeTools: result.activeTools } : {}),
} as RequestProjectionContext;
const hookResult = await Promise.resolve(hook(hookOptions));
if (!hookResult) continue;
result = {
...(result ?? {}),
...hookResult,
activeTools: hookResult.activeTools ?? result?.activeTools,
};
if (hookResult.messages) messages = hookResult.messages;
}
return result;
};
}