Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions core/util/messageConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,21 +351,24 @@ export function convertFromUnifiedHistory(
}

/**
* Convert ChatHistoryItem array to ChatCompletionMessageParam array with injected system message
* Convert ChatHistoryItem array to ChatCompletionMessageParam array with injected system message.
* Supports both a plain string and an array of content blocks for the system message.
* When an array is provided, it is passed as the system message content directly,
* which allows Anthropic's prompt caching to cache each block independently.
* @param historyItems - The chat history items
* @param systemMessage - The system message to inject at the beginning
* @param systemMessage - The system message (string or array of {type:"text", text:string} blocks)
*/
export function convertFromUnifiedHistoryWithSystemMessage(
historyItems: ChatHistoryItem[],
systemMessage: string,
systemMessage: string | Array<{ type: "text"; text: string }>,
): ChatCompletionMessageParam[] {
const messages: ChatCompletionMessageParam[] = [];

// Inject system message at the beginning
messages.push({
role: "system",
content: systemMessage,
});
} as ChatCompletionMessageParam);

// Convert the rest of the history
const convertedMessages = convertFromUnifiedHistory(historyItems);
Expand Down
13 changes: 10 additions & 3 deletions extensions/cli/src/__mocks__/systemMessage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { vi } from "vitest";

export const constructSystemMessage = vi
export const constructSystemMessage = vi.fn().mockResolvedValue([
{
type: "text",
text: "You are an agent in the Continue CLI. Given the user's prompt, you should use the tools available to you to answer the user's question.",
},
]);

export const flattenSystemMessage = vi
.fn()
.mockResolvedValue(
"You are an agent in the Continue CLI. Given the user's prompt, you should use the tools available to you to answer the user's question.",
.mockImplementation((blocks: Array<{ type: string; text: string }>) =>
blocks.map((b) => b.text).join("\n\n"),
);
7 changes: 6 additions & 1 deletion extensions/cli/src/commands/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ describe("serve command", () => {
}));

vi.mock("../systemMessage.js", () => ({
constructSystemMessage: vi.fn(() => Promise.resolve("System message")),
constructSystemMessage: vi.fn(() =>
Promise.resolve([{ type: "text", text: "System message" }]),
),
flattenSystemMessage: vi.fn((blocks: Array<{ text: string }>) =>
blocks.map((b) => b.text).join("\n\n"),
),
}));

vi.mock("../telemetry/telemetryService.js", () => ({
Expand Down
14 changes: 10 additions & 4 deletions extensions/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import {
loadOrCreateSessionById,
} from "../session.js";
import { messageQueue } from "../stream/messageQueue.js";
import { constructSystemMessage } from "../systemMessage.js";
import {
constructSystemMessage,
flattenSystemMessage,
} from "../systemMessage.js";
import { telemetryService } from "../telemetry/telemetryService.js";
import { reportFailureTool } from "../tools/reportFailure.js";
import { gracefulExit, updateAgentMetadata } from "../util/exit.js";
Expand Down Expand Up @@ -153,17 +156,20 @@ export async function serve(prompt?: string, options: ServeOptions = {}) {
}

// Initialize session with system message
const systemMessage = await constructSystemMessage(
const systemMessageBlocks = await constructSystemMessage(
permissionsState.currentMode,
options.rule,
undefined,
true,
);

const initialHistory: ChatHistoryItem[] = [];
if (systemMessage) {
if (systemMessageBlocks.length > 0) {
initialHistory.push({
message: { role: "system" as const, content: systemMessage },
message: {
role: "system" as const,
content: flattenSystemMessage(systemMessageBlocks),
},
contextItems: [],
});
}
Expand Down
13 changes: 9 additions & 4 deletions extensions/cli/src/services/SystemMessageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ describe("SystemMessageService", () => {
headless: true,
};

constructSystemMessageMock.mockResolvedValue("Test system message");
const mockBlocks = [{ type: "text", text: "Test system message" }];
constructSystemMessageMock.mockResolvedValue(mockBlocks);

await service.initialize(config);
const message = await service.getSystemMessage("normal");
Expand All @@ -81,7 +82,7 @@ describe("SystemMessageService", () => {
"json",
true,
);
expect(message).toBe("Test system message");
expect(message).toEqual(mockBlocks);
});
});

Expand All @@ -96,7 +97,9 @@ describe("SystemMessageService", () => {
format: "json",
});

constructSystemMessageMock.mockResolvedValue("Updated message");
constructSystemMessageMock.mockResolvedValue([
{ type: "text", text: "Updated message" },
]);
await service.getSystemMessage("normal");

expect(constructSystemMessageMock).toHaveBeenCalledWith(
Expand All @@ -117,7 +120,9 @@ describe("SystemMessageService", () => {
additionalRules: ["rule2", "rule3"],
});

constructSystemMessageMock.mockResolvedValue("Updated message");
constructSystemMessageMock.mockResolvedValue([
{ type: "text", text: "Updated message" },
]);
await service.getSystemMessage("normal");

expect(constructSystemMessageMock).toHaveBeenCalledWith(
Expand Down
11 changes: 8 additions & 3 deletions extensions/cli/src/services/SystemMessageService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PermissionMode } from "../permissions/types.js";
import { constructSystemMessage } from "../systemMessage.js";
import {
constructSystemMessage,
SystemMessageBlock,
} from "../systemMessage.js";
import { logger } from "../util/logger.js";

import { BaseService } from "./BaseService.js";
Expand Down Expand Up @@ -46,7 +49,9 @@ export class SystemMessageService extends BaseService<SystemMessageServiceState>
/**
* Get a fresh system message with current mode and configuration
*/
public async getSystemMessage(currentMode: PermissionMode): Promise<string> {
public async getSystemMessage(
currentMode: PermissionMode,
): Promise<SystemMessageBlock[]> {
const { additionalRules, format, headless } = this.currentState;

const systemMessage = await constructSystemMessage(
Expand All @@ -58,7 +63,7 @@ export class SystemMessageService extends BaseService<SystemMessageServiceState>

logger.debug("Generated fresh system message", {
mode: currentMode,
messageLength: systemMessage.length,
blockCount: systemMessage.length,
});

return systemMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,22 @@ vi.mock("../util/logger.js", () => ({
vi.mock("../services/index.js", () => ({
services: {
systemMessage: {
getSystemMessage: vi.fn(() => Promise.resolve("System message")),
getSystemMessage: vi.fn(() =>
Promise.resolve([{ type: "text", text: "System message" }]),
),
},
toolPermissions: {
getState: vi.fn(() => ({ currentMode: "enabled" })),
},
},
}));

vi.mock("../systemMessage.js", () => ({
flattenSystemMessage: vi.fn((blocks: Array<{ text: string }>) =>
blocks.map((b) => b.text).join("\n\n"),
),
}));

vi.mock("os", async (importOriginal) => {
const actual = (await importOriginal()) as object;
return {
Expand Down
21 changes: 11 additions & 10 deletions extensions/cli/src/stream/streamChatResponse.autoCompaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,17 @@ export async function handleAutoCompaction(
try {
// Get system message to calculate its token count for compaction pruning
// Use provided message if available, otherwise fetch it (for backward compatibility)
const systemMessage =
providedSystemMessage ??
(async () => {
const { services } = await import("../services/index.js");
return services.systemMessage.getSystemMessage(
services.toolPermissions.getState().currentMode,
);
})();
const resolvedSystemMessage =
typeof systemMessage === "string" ? systemMessage : await systemMessage;
let resolvedSystemMessage: string;
if (providedSystemMessage === undefined) {
const { services } = await import("../services/index.js");
const { flattenSystemMessage } = await import("../systemMessage.js");
const blocks = await services.systemMessage.getSystemMessage(
services.toolPermissions.getState().currentMode,
);
resolvedSystemMessage = flattenSystemMessage(blocks);
} else {
resolvedSystemMessage = providedSystemMessage;
}

const { countChatHistoryItemTokens } = await import("../util/tokenizer.js");
const systemMessageTokens = countChatHistoryItemTokens(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ vi.mock("../util/logger.js", () => ({
vi.mock("../services/index.js", () => ({
services: {
systemMessage: {
getSystemMessage: vi.fn(() => Promise.resolve("System message")),
getSystemMessage: vi.fn(() =>
Promise.resolve([{ type: "text", text: "System message" }]),
),
},
toolPermissions: {
getState: vi.fn(() => ({ currentMode: "enabled" })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ChatHistoryItem } from "core/index.js";
import type { ChatCompletionTool } from "openai/resources/chat/completions.mjs";

import { services } from "../services/index.js";
import { flattenSystemMessage, SystemMessageBlock } from "../systemMessage.js";
import { ToolCall } from "../tools/index.js";
import { logger } from "../util/logger.js";
import { validateContextLength } from "../util/tokenizer.js";
Expand All @@ -17,7 +18,7 @@ export interface CompactionHelperOptions {
isCompacting: boolean;
isHeadless: boolean;
callbacks?: StreamCallbacks;
systemMessage: string;
systemMessage: SystemMessageBlock[];
tools?: ChatCompletionTool[];
}

Expand All @@ -42,14 +43,16 @@ export async function handlePreApiCompaction(
return { chatHistory, wasCompacted: false };
}

const systemMessageString = flattenSystemMessage(systemMessage);

const { wasCompacted, chatHistory: preCompactHistory } =
await handleAutoCompaction(chatHistory, model, llmApi, {
isHeadless,
callbacks: {
onSystemMessage: callbacks?.onSystemMessage,
onContent: callbacks?.onContent,
},
systemMessage,
systemMessage: systemMessageString,
tools,
});

Expand Down Expand Up @@ -84,6 +87,8 @@ export async function handlePostToolValidation(
return { chatHistory, wasCompacted: false };
}

const systemMessageString = flattenSystemMessage(systemMessage);

// Get updated history after tool execution
const chatHistorySvc = services.chatHistory;
if (
Expand All @@ -98,7 +103,7 @@ export async function handlePostToolValidation(
chatHistory,
model,
safetyBuffer: SAFETY_BUFFER,
systemMessage,
systemMessage: systemMessageString,
tools,
});

Expand All @@ -117,7 +122,7 @@ export async function handlePostToolValidation(
onSystemMessage: callbacks?.onSystemMessage,
onContent: callbacks?.onContent,
},
systemMessage,
systemMessage: systemMessageString,
tools,
});

Expand All @@ -136,7 +141,7 @@ export async function handlePostToolValidation(
chatHistory,
model,
safetyBuffer: SAFETY_BUFFER,
systemMessage,
systemMessage: systemMessageString,
tools,
});

Expand Down Expand Up @@ -185,6 +190,8 @@ export async function handleNormalAutoCompaction(
return { chatHistory, wasCompacted: false };
}

const systemMessageString = flattenSystemMessage(systemMessage);

const chatHistorySvc = services.chatHistory;
if (
typeof chatHistorySvc?.isReady === "function" &&
Expand All @@ -200,7 +207,7 @@ export async function handleNormalAutoCompaction(
onSystemMessage: callbacks?.onSystemMessage,
onContent: callbacks?.onContent,
},
systemMessage,
systemMessage: systemMessageString,
tools,
});

Expand Down
19 changes: 19 additions & 0 deletions extensions/cli/src/stream/streamChatResponse.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,14 +367,33 @@ export function recordStreamTelemetry(options: {
});

// Mirror core metrics to PostHog for product analytics
const cacheReadTokens =
fullUsage?.prompt_tokens_details?.cache_read_tokens ?? 0;
const cacheWriteTokens =
fullUsage?.prompt_tokens_details?.cache_write_tokens ?? 0;

try {
posthogService.capture("apiRequest", {
model: model.model,
durationMs: totalDuration,
inputTokens: actualInputTokens,
outputTokens: actualOutputTokens,
costUsd: cost,
cacheReadTokens,
cacheWriteTokens,
});

// Emit prompt_cache_metrics for the Prompt Cache Performance dashboard
if (actualInputTokens > 0) {
posthogService.capture("prompt_cache_metrics", {
model: model.model,
cache_read_tokens: cacheReadTokens,
cache_write_tokens: cacheWriteTokens,
total_prompt_tokens: actualInputTokens,
cache_hit_rate: cacheReadTokens / actualInputTokens,
tool_count: tools?.length ?? 0,
});
}
} catch {}

return cost;
Expand Down
Loading
Loading