import type { AgentContextRuntime, AgentContextPrepareInput, AgentPreparedContext } from "./ContextRuntime.js";
import type { CanonicalMessage } from "../model/index.js";
export class NullContextRuntime implements AgentContextRuntime {
constructor(private readonly options: { maxMessages?: number } = {}) {}
async prepareForModel(input: AgentContextPrepareInput): Promise<AgentPreparedContext> {
const maxMessages = input.maxMessages ?? this.options.maxMessages;
if (maxMessages !== undefined && input.messages.length > maxMessages) {
return {
messages: toolPairSafeSlice(input.messages, maxMessages),
systemPromptParts: [],
tools: input.tools,
boundaries: [{ type: "compact", retainedMessages: maxMessages }],
diagnostics: [
{
code: "context_truncated",
severity: "warning",
message: `Context was truncated to the last ${maxMessages} messages.`,
},
],
};
}
return {
messages: input.messages,
systemPromptParts: [],
tools: input.tools,
boundaries: [],
diagnostics: [
{
code: "context_budget_not_enforced",
severity: "info",
message: "Token-level context budget is not enforced in the null context runtime.",
},
],
};
}
}
* Slice the last `max` messages, but push the cut point earlier if it would
* land between an assistant tool_calls message and its tool_result responses.
*/
function toolPairSafeSlice(messages: CanonicalMessage[], max: number): CanonicalMessage[] {
if (messages.length <= max) return messages;
let cut = messages.length - max;
while (cut < messages.length && isToolResultOnly(messages[cut])) {
cut--;
}
if (cut < 0) cut = 0;
return messages.slice(cut);
}
function isToolResultOnly(message: CanonicalMessage): boolean {
return (
message.role === "user" &&
message.content.length > 0 &&
message.content.every((block) => block.type === "tool_result")
);
}