( messages: Message[], action: TranscriptEntry, tools: Tools, context: ToolPermissionContext, signal: AbortSignal, )
| 1010 | * @param signal - Abort signal |
| 1011 | */ |
| 1012 | export async function classifyYoloAction( |
| 1013 | messages: Message[], |
| 1014 | action: TranscriptEntry, |
| 1015 | tools: Tools, |
| 1016 | context: ToolPermissionContext, |
| 1017 | signal: AbortSignal, |
| 1018 | ): Promise<YoloClassifierResult> { |
| 1019 | const lookup = buildToolLookup(tools) |
| 1020 | const actionCompact = toCompact(action, lookup) |
| 1021 | // '' = "no security relevance" (Tool.toAutoClassifierInput contract). Without |
| 1022 | // this guard the empty action block + cache_control below hits an API 400. |
| 1023 | if (actionCompact === '') { |
| 1024 | return { |
| 1025 | shouldBlock: false, |
| 1026 | reason: 'Tool declares no classifier-relevant input', |
| 1027 | model: getClassifierModel(), |
| 1028 | } |
| 1029 | } |
| 1030 | |
| 1031 | const systemPrompt = await buildYoloSystemPrompt(context) |
| 1032 | const transcriptEntries = buildTranscriptEntries(messages) |
| 1033 | const claudeMdMessage = buildClaudeMdMessage() |
| 1034 | const prefixMessages: Anthropic.MessageParam[] = claudeMdMessage |
| 1035 | ? [claudeMdMessage] |
| 1036 | : [] |
| 1037 | |
| 1038 | let toolCallsLength = actionCompact.length |
| 1039 | let userPromptsLength = 0 |
| 1040 | const userContentBlocks: Anthropic.TextBlockParam[] = [] |
| 1041 | for (const entry of transcriptEntries) { |
| 1042 | for (const block of entry.content) { |
| 1043 | const serialized = toCompactBlock(block, entry.role, lookup) |
| 1044 | if (serialized === '') continue |
| 1045 | switch (entry.role) { |
| 1046 | case 'user': |
| 1047 | userPromptsLength += serialized.length |
| 1048 | break |
| 1049 | case 'assistant': |
| 1050 | toolCallsLength += serialized.length |
| 1051 | break |
| 1052 | default: { |
| 1053 | const _exhaustive: never = entry.role |
| 1054 | void _exhaustive |
| 1055 | } |
| 1056 | } |
| 1057 | userContentBlocks.push({ type: 'text' as const, text: serialized }) |
| 1058 | } |
| 1059 | } |
| 1060 | |
| 1061 | const userPrompt = userContentBlocks.map(b => b.text).join('') + actionCompact |
| 1062 | const promptLengths = { |
| 1063 | systemPrompt: systemPrompt.length, |
| 1064 | toolCalls: toolCallsLength, |
| 1065 | userPrompts: userPromptsLength, |
| 1066 | } |
| 1067 | |
| 1068 | // Compare main-loop context vs classifier transcript to track projection |
| 1069 | // divergence. tokenCountWithEstimation is cheap (walks back to last API |
no test coverage detected