Skip to content

Commit 166e928

Browse files
committed
More comprehensive prompt cache debugging logs
1 parent 1070287 commit 166e928

File tree

7 files changed

+755
-98
lines changed

7 files changed

+755
-98
lines changed

common/src/types/contracts/llm.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,13 @@ export type PromptAiSdkStreamFn = (
4040
agentId?: string
4141
maxRetries?: number
4242
onCostCalculated?: (credits: number) => Promise<void>
43+
onCacheDebugProviderRequestBuilt?: (params: {
44+
provider: string
45+
rawBody: unknown
46+
normalizedBody?: unknown
47+
}) => void
4348
includeCacheControl?: boolean
49+
cacheDebugCorrelation?: string
4450
agentProviderOptions?: OpenRouterProviderRoutingOptions
4551
/** List of agents that can be spawned - used to transform agent tool calls */
4652
spawnableAgents?: string[]
@@ -68,7 +74,13 @@ export type PromptAiSdkFn = (
6874
chargeUser?: boolean
6975
agentId?: string
7076
onCostCalculated?: (credits: number) => Promise<void>
77+
onCacheDebugProviderRequestBuilt?: (params: {
78+
provider: string
79+
rawBody: unknown
80+
normalizedBody?: unknown
81+
}) => void
7182
includeCacheControl?: boolean
83+
cacheDebugCorrelation?: string
7284
agentProviderOptions?: OpenRouterProviderRoutingOptions
7385
maxRetries?: number
7486
/** Cost mode - 'free' mode means 0 credits charged for all agents */
@@ -97,7 +109,13 @@ export type PromptAiSdkStructuredInput<T> = {
97109
chargeUser?: boolean
98110
agentId?: string
99111
onCostCalculated?: (credits: number) => Promise<void>
112+
onCacheDebugProviderRequestBuilt?: (params: {
113+
provider: string
114+
rawBody: unknown
115+
normalizedBody?: unknown
116+
}) => void
100117
includeCacheControl?: boolean
118+
cacheDebugCorrelation?: string
101119
agentProviderOptions?: OpenRouterProviderRoutingOptions
102120
maxRetries?: number
103121
sendAction: SendActionFn

common/src/util/cache-debug.ts

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import type { JSONValue } from '../types/json'
2+
3+
type SerializableValue = JSONValue
4+
5+
type SerializableRecord = Record<string, SerializableValue>
6+
7+
export type CacheDebugCorrelation = {
8+
projectRoot: string
9+
filename: string
10+
snapshotId: string
11+
}
12+
13+
function normalizeForJson(value: unknown): SerializableValue {
14+
if (
15+
value === null ||
16+
typeof value === 'string' ||
17+
typeof value === 'number' ||
18+
typeof value === 'boolean'
19+
) {
20+
return value
21+
}
22+
23+
if (value instanceof URL) {
24+
return value.toString()
25+
}
26+
27+
if (value instanceof Uint8Array) {
28+
return {
29+
type: 'Uint8Array',
30+
byteLength: value.byteLength,
31+
}
32+
}
33+
34+
if (Array.isArray(value)) {
35+
return value.map((item) => normalizeForJson(item))
36+
}
37+
38+
if (typeof value === 'object') {
39+
return Object.fromEntries(
40+
Object.entries(value as Record<string, unknown>).map(([key, entryValue]) => [
41+
key,
42+
normalizeForJson(entryValue),
43+
]),
44+
)
45+
}
46+
47+
return String(value)
48+
}
49+
50+
function summarizeDataUrl(value: string): SerializableValue {
51+
const firstComma = value.indexOf(',')
52+
const header = firstComma >= 0 ? value.slice(0, firstComma) : value
53+
const payload = firstComma >= 0 ? value.slice(firstComma + 1) : ''
54+
return {
55+
type: 'data-url',
56+
mediaType: header.slice(5).split(';')[0] || 'unknown',
57+
payloadLength: payload.length,
58+
preview: payload.slice(0, 32),
59+
}
60+
}
61+
62+
function summarizeLargeValue(value: SerializableValue): SerializableValue {
63+
if (Array.isArray(value)) {
64+
return value.map((item) => summarizeLargeValue(item))
65+
}
66+
67+
if (!value || typeof value !== 'object') {
68+
if (typeof value === 'string' && value.startsWith('data:')) {
69+
return summarizeDataUrl(value)
70+
}
71+
return value
72+
}
73+
74+
if ('url' in value && typeof value.url === 'string' && value.url.startsWith('data:')) {
75+
return {
76+
...value,
77+
url: summarizeDataUrl(value.url),
78+
}
79+
}
80+
81+
return Object.fromEntries(
82+
Object.entries(value).map(([key, entryValue]) => {
83+
if (key === 'file_data' && typeof entryValue === 'string' && entryValue.startsWith('data:')) {
84+
return [key, summarizeDataUrl(entryValue)]
85+
}
86+
if (key === 'arguments' && typeof entryValue === 'string') {
87+
return [key, entryValue]
88+
}
89+
return [key, summarizeLargeValue(entryValue)]
90+
}),
91+
)
92+
}
93+
94+
function parseRequestBody(body: unknown): unknown {
95+
if (typeof body !== 'string') {
96+
return body
97+
}
98+
99+
try {
100+
return JSON.parse(body)
101+
} catch {
102+
return body
103+
}
104+
}
105+
106+
export function serializeCacheDebugCorrelation(
107+
correlation: CacheDebugCorrelation,
108+
): string {
109+
return JSON.stringify(correlation)
110+
}
111+
112+
export function parseCacheDebugCorrelation(
113+
value: unknown,
114+
): CacheDebugCorrelation | undefined {
115+
if (typeof value !== 'string') {
116+
return undefined
117+
}
118+
119+
try {
120+
const parsed = JSON.parse(value) as Partial<CacheDebugCorrelation>
121+
if (
122+
typeof parsed.projectRoot === 'string' &&
123+
typeof parsed.filename === 'string' &&
124+
typeof parsed.snapshotId === 'string'
125+
) {
126+
return {
127+
projectRoot: parsed.projectRoot,
128+
filename: parsed.filename,
129+
snapshotId: parsed.snapshotId,
130+
}
131+
}
132+
} catch {
133+
return undefined
134+
}
135+
136+
return undefined
137+
}
138+
139+
export function normalizeProviderRequestBodyForCacheDebug(params: {
140+
provider: string
141+
body: unknown
142+
}): SerializableValue {
143+
const parsed = parseRequestBody(params.body)
144+
const body = normalizeForJson(parsed)
145+
146+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
147+
return body
148+
}
149+
150+
const record = body as SerializableRecord
151+
const normalized: SerializableRecord = {}
152+
153+
for (const key of ['model', 'messages', 'tools', 'tool_choice', 'response_format', 'reasoning', 'reasoning_effort', 'verbosity', 'provider']) {
154+
if (key in record) {
155+
normalized[key] = summarizeLargeValue(record[key])
156+
}
157+
}
158+
159+
if (params.provider === 'openrouter') {
160+
for (const key of ['models', 'plugins', 'web_search_options', 'include_reasoning']) {
161+
if (key in record) {
162+
normalized[key] = summarizeLargeValue(record[key])
163+
}
164+
}
165+
}
166+
167+
return normalized
168+
}

packages/agent-runtime/src/prompt-agent-stream.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ export const getAgentStreamFromTemplate = (params: {
2626
tools: ToolSet
2727
userId: string | undefined
2828
userInputId: string
29+
cacheDebugCorrelation?: string
30+
onCacheDebugProviderRequestBuilt?: (params: {
31+
provider: string
32+
rawBody: unknown
33+
normalizedBody?: unknown
34+
}) => void
2935

3036
onCostCalculated?: (credits: number) => Promise<void>
3137
promptAiSdkStream: PromptAiSdkStreamFn
@@ -47,6 +53,8 @@ export const getAgentStreamFromTemplate = (params: {
4753
tools,
4854
userId,
4955
userInputId,
56+
cacheDebugCorrelation,
57+
onCacheDebugProviderRequestBuilt,
5058

5159
sendAction,
5260
onCostCalculated,
@@ -80,6 +88,8 @@ export const getAgentStreamFromTemplate = (params: {
8088
tools,
8189
userId,
8290
userInputId,
91+
cacheDebugCorrelation,
92+
onCacheDebugProviderRequestBuilt,
8393

8494
onCostCalculated,
8595
sendAction,

packages/agent-runtime/src/run-agent-step.ts

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { createHash } from 'crypto'
2-
31
import { AnalyticsEvent } from '@codebuff/common/constants/analytics-events'
42
import { supportsCacheControl } from '@codebuff/common/old-constants'
53
import { TOOLS_WHICH_WONT_FORCE_NEXT_STEP } from '@codebuff/common/tools/constants'
64
import { buildArray } from '@codebuff/common/util/array'
75
import { AbortError, getErrorObject, isAbortError } from '@codebuff/common/util/error'
6+
import { serializeCacheDebugCorrelation } from '@codebuff/common/util/cache-debug'
87
import { systemMessage, userMessage } from '@codebuff/common/util/messages'
98
import { APICallError, type ToolSet } from 'ai'
109
import { cloneDeep, mapValues } from 'lodash'
@@ -21,7 +20,10 @@ import { getAgentPrompt } from './templates/strings'
2120
import { getToolSet } from './tools/prompts'
2221
import { processStream } from './tools/stream-parser'
2322
import { getAgentOutput } from './util/agent-output'
24-
import { writeCacheDebugSnapshot } from './util/cache-debug'
23+
import {
24+
createCacheDebugSnapshot,
25+
enrichCacheDebugSnapshotWithProviderRequest,
26+
} from './util/cache-debug'
2527
import {
2628
withSystemInstructionTags,
2729
withSystemTags as withSystemTags,
@@ -259,6 +261,52 @@ export const runAgentStep = async (
259261
const iterationNum = agentState.messageHistory.length
260262
const systemTokens = countTokensJson(system)
261263

264+
const cacheDebugCorrelation = CACHE_DEBUG_FULL_LOGGING
265+
? createCacheDebugSnapshot({
266+
agentType: String(agentType),
267+
system,
268+
toolDefinitions: params.tools
269+
? Object.fromEntries(
270+
Object.entries(params.tools).map(([name, tool]) => [
271+
name,
272+
{
273+
description: tool.description,
274+
inputSchema: tool.inputSchema as {},
275+
},
276+
]),
277+
)
278+
: {},
279+
messages: [systemMessage(system), ...agentState.messageHistory],
280+
logger,
281+
projectRoot: fileContext.projectRoot,
282+
runId: agentState.runId,
283+
userInputId,
284+
agentStepId,
285+
model,
286+
})
287+
: undefined
288+
289+
const onCacheDebugProviderRequestBuilt =
290+
cacheDebugCorrelation
291+
? ({
292+
provider,
293+
rawBody,
294+
normalizedBody,
295+
}: {
296+
provider: string
297+
rawBody: unknown
298+
normalizedBody?: unknown
299+
}) => {
300+
enrichCacheDebugSnapshotWithProviderRequest({
301+
correlation: cacheDebugCorrelation,
302+
provider,
303+
rawBody,
304+
normalized: normalizedBody ?? rawBody,
305+
logger,
306+
})
307+
}
308+
: undefined
309+
262310
logger.debug(
263311
{
264312
iteration: iterationNum,
@@ -286,6 +334,10 @@ export const runAgentStep = async (
286334
model,
287335
n: params.n,
288336
onCostCalculated,
337+
cacheDebugCorrelation: cacheDebugCorrelation
338+
? serializeCacheDebugCorrelation(cacheDebugCorrelation)
339+
: undefined,
340+
onCacheDebugProviderRequestBuilt,
289341
})
290342

291343
if (result.aborted) {
@@ -336,8 +388,12 @@ export const runAgentStep = async (
336388
...params,
337389
agentId: agentState.parentId ? agentState.agentId : undefined,
338390
costMode: params.costMode,
391+
cacheDebugCorrelation: cacheDebugCorrelation
392+
? serializeCacheDebugCorrelation(cacheDebugCorrelation)
393+
: undefined,
339394
includeCacheControl: supportsCacheControl(agentTemplate.model),
340395
messages: [systemMessage(system), ...agentState.messageHistory],
396+
onCacheDebugProviderRequestBuilt,
341397
template: agentTemplate,
342398
onCostCalculated,
343399
})
@@ -715,36 +771,6 @@ export async function loopAgentSteps(
715771
inputSchema: tool.inputSchema as {},
716772
}))
717773

718-
if (CACHE_DEBUG_FULL_LOGGING) {
719-
// Debug: hash the system prompt and tool definitions to detect prompt cache invalidation
720-
const systemHash = createHash('sha256').update(system).digest('hex').slice(0, 8)
721-
const sortedToolDefs = Object.keys(toolDefinitions).sort().reduce((acc, key) => {
722-
acc[key] = toolDefinitions[key]
723-
return acc
724-
}, {} as Record<string, unknown>)
725-
const toolsHash = createHash('sha256').update(JSON.stringify(sortedToolDefs)).digest('hex').slice(0, 8)
726-
logger.debug(
727-
{
728-
systemHash,
729-
toolsHash,
730-
systemLength: system.length,
731-
toolCount: Object.keys(toolDefinitions).length,
732-
toolNames: Object.keys(toolDefinitions).sort(),
733-
agentType,
734-
},
735-
`[Cache Debug] System prompt hash: ${systemHash}, Tools hash: ${toolsHash}`,
736-
)
737-
738-
writeCacheDebugSnapshot({
739-
agentType: String(agentType),
740-
system,
741-
toolDefinitions: sortedToolDefs,
742-
messages: initialMessages,
743-
logger,
744-
projectRoot: fileContext.projectRoot,
745-
})
746-
}
747-
748774
const additionalToolDefinitionsWithCache = async () => {
749775
if (!cachedAdditionalToolDefinitions) {
750776
cachedAdditionalToolDefinitions = await additionalToolDefinitions({

0 commit comments

Comments
 (0)