blob: dab3003b92b8df37b90e8cb358afbe096faf3cbf [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { ModelMessage, NormalizedUsage, ToolCallPart } from './model-protocol.js';
export interface CompletedProviderStep {
toolCalls?: readonly ToolCallPart[];
usage?: NormalizedUsage;
}
/** The system prompt and active tool subset one request actually dispatches. */
export interface DispatchRequestShape {
systemPromptChars: number;
activeTools: string[];
}
export interface RequestProjectionContext {
completedSteps: readonly CompletedProviderStep[];
stepNumber: number;
model: unknown;
messages: ModelMessage[];
activeTools?: readonly string[];
/**
* Resolve what this step will really send, from a stage's projected active
* tool set. Dispatch appends step-specific system prompt fragments and can
* clear the tool set entirely on a finalization step, so a stage that
* MEASURES the request must price this shape, not the pre-dispatch inputs —
* otherwise a payload measure gets paired with a different request's tokens.
*/
resolveDispatch: (activeTools: readonly string[] | undefined) => DispatchRequestShape;
}
export interface RequestProjection {
activeTools?: string[];
messages?: ModelMessage[];
}
export type RequestProjectionStage = (
context: RequestProjectionContext,
) => RequestProjection | undefined | PromiseLike<RequestProjection | undefined>;
export function composeRequestProjection(
...stages: Array<RequestProjectionStage | undefined>
): RequestProjectionStage | undefined {
const hooks = stages.filter((stage): stage is RequestProjectionStage => stage !== undefined);
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;
};
}