Spaces:
Sleeping
Sleeping
| /** | |
| * Protobuf message builders and parsers for the local Windsurf language server. | |
| * | |
| * Service: exa.language_server_pb.LanguageServerService | |
| * | |
| * Two flows: | |
| * Legacy β RawGetChatMessage (streaming, simpler) | |
| * Cascade β StartCascade β SendUserCascadeMessage β poll GetCascadeTrajectorySteps | |
| * | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * Metadata { | |
| * string ide_name = 1; | |
| * string extension_version = 2; | |
| * string api_key = 3; | |
| * string locale = 4; | |
| * string os = 5; | |
| * string ide_version = 7; | |
| * string hardware = 8; | |
| * uint64 request_id = 9; | |
| * string session_id = 10; | |
| * string extension_name = 12; | |
| * } | |
| * | |
| * RawGetChatMessageRequest { | |
| * Metadata metadata = 1; | |
| * repeated ChatMessage messages = 2; | |
| * string system_prompt_override = 3; | |
| * Model chat_model = 4; // enum | |
| * string chat_model_name = 5; | |
| * } | |
| * | |
| * ChatMessage { | |
| * string message_id = 1; | |
| * ChatMessageSource source = 2; // enum | |
| * Timestamp timestamp = 3; | |
| * string conversation_id = 4; | |
| * ChatMessageIntent intent = 5; // for user/system/tool | |
| * // For assistant: field 5 is plain string text | |
| * } | |
| * | |
| * ChatMessageIntent { IntentGeneric generic = 1; } | |
| * IntentGeneric { string text = 1; } | |
| * | |
| * RawGetChatMessageResponse { | |
| * RawChatMessage delta_message = 1; | |
| * } | |
| * | |
| * RawChatMessage { | |
| * string message_id = 1; | |
| * ChatMessageSource source = 2; | |
| * Timestamp timestamp = 3; | |
| * string conversation_id = 4; | |
| * string text = 5; | |
| * bool in_progress = 6; | |
| * bool is_error = 7; | |
| * } | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| */ | |
| import { randomUUID } from 'crypto'; | |
| import { | |
| writeVarintField, writeStringField, writeMessageField, | |
| writeBoolField, parseFields, getField, getAllFields, | |
| } from './proto.js'; | |
| // βββ Enums βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export const SOURCE = { | |
| USER: 1, | |
| SYSTEM: 2, | |
| ASSISTANT: 3, | |
| TOOL: 4, | |
| }; | |
| // βββ Timestamp βββββββββββββββββββββββββββββββββββββββββββββ | |
| function encodeTimestamp() { | |
| const now = Date.now(); | |
| const secs = Math.floor(now / 1000); | |
| const nanos = (now % 1000) * 1_000_000; | |
| const parts = [writeVarintField(1, secs)]; | |
| if (nanos > 0) parts.push(writeVarintField(2, nanos)); | |
| return Buffer.concat(parts); | |
| } | |
| // βββ Metadata ββββββββββββββββββββββββββββββββββββββββββββββ | |
| export function buildMetadata(apiKey, version = '1.9600.41', sessionId = null) { | |
| return Buffer.concat([ | |
| writeStringField(1, 'windsurf'), // ide_name | |
| writeStringField(2, version), // extension_version | |
| writeStringField(3, apiKey), // api_key | |
| writeStringField(4, 'en'), // locale | |
| writeStringField(5, 'linux'), // os | |
| writeStringField(7, version), // ide_version | |
| writeStringField(8, 'x86_64'), // hardware | |
| writeVarintField(9, Date.now()), // request_id | |
| writeStringField(10, sessionId || randomUUID()), // session_id | |
| writeStringField(12, 'windsurf'), // extension_name | |
| ]); | |
| } | |
| // βββ ChatMessage (for RawGetChatMessage) βββββββββββββββββββ | |
| function buildChatMessage(content, source, conversationId) { | |
| const parts = [ | |
| writeStringField(1, randomUUID()), // message_id | |
| writeVarintField(2, source), // source enum | |
| writeMessageField(3, encodeTimestamp()), // timestamp | |
| writeStringField(4, conversationId), // conversation_id | |
| ]; | |
| if (source === SOURCE.ASSISTANT) { | |
| // Assistant goes in ChatMessage.action (field 6), not .intent (field 5). | |
| // Proto: ChatMessageAction { ChatMessageActionGeneric generic = 1; } | |
| // ChatMessageActionGeneric { string text = 1; } | |
| // Previous code wrote a raw string into field 5 which happens to share | |
| // wire type (length-delimited) with the expected message, so short | |
| // replies slipped through parsing by coincidence β real multi-turn | |
| // conversations tripped the LS with "invalid wire-format data". | |
| const actionGeneric = writeStringField(1, content); // ChatMessageActionGeneric.text | |
| const action = writeMessageField(1, actionGeneric); // ChatMessageAction.generic | |
| parts.push(writeMessageField(6, action)); | |
| } else { | |
| // User/System/Tool use ChatMessageIntent { IntentGeneric { text } } | |
| const intentGeneric = writeStringField(1, content); // IntentGeneric.text | |
| const intent = writeMessageField(1, intentGeneric); // ChatMessageIntent.generic | |
| parts.push(writeMessageField(5, intent)); | |
| } | |
| return Buffer.concat(parts); | |
| } | |
| // βββ RawGetChatMessageRequest ββββββββββββββββββββββββββββββ | |
| /** | |
| * Build RawGetChatMessageRequest protobuf. | |
| * | |
| * @param {string} apiKey | |
| * @param {Array} messages - OpenAI-format [{role, content}, ...] | |
| * @param {number} modelEnum - Windsurf model enum value | |
| * @param {string} [modelName] - Model name string (optional) | |
| */ | |
| export function buildRawGetChatMessageRequest(apiKey, messages, modelEnum, modelName) { | |
| const parts = []; | |
| const conversationId = randomUUID(); | |
| // Field 1: Metadata | |
| parts.push(writeMessageField(1, buildMetadata(apiKey))); | |
| // Field 2: repeated ChatMessage (skip system, handled separately). | |
| // Windsurf's legacy RawGetChatMessage backend rejects role=tool and | |
| // doesn't know about assistant tool_calls. Degrade both to plain text | |
| // so multi-turn conversations that carry tool history still flow | |
| // through without triggering "proto: cannot parse invalid wire-format | |
| // data" upstream. Cascade models are unaffected β they use a different | |
| // endpoint (SendUserCascadeMessage) with full tool support. | |
| let systemPrompt = ''; | |
| for (const msg of messages) { | |
| if (msg.role === 'system') { | |
| systemPrompt += (systemPrompt ? '\n' : '') + | |
| (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)); | |
| continue; | |
| } | |
| let source; | |
| let text; | |
| const baseText = typeof msg.content === 'string' ? msg.content | |
| : Array.isArray(msg.content) ? msg.content.filter(c => c.type === 'text').map(c => c.text).join('\n') | |
| : msg.content == null ? '' : JSON.stringify(msg.content); | |
| switch (msg.role) { | |
| case 'user': | |
| source = SOURCE.USER; | |
| text = baseText; | |
| break; | |
| case 'assistant': | |
| source = SOURCE.ASSISTANT; | |
| // If the assistant previously called tools, append the call descriptions | |
| // so the model sees its own prior tool usage as text. Empty string OK. | |
| if (Array.isArray(msg.tool_calls) && msg.tool_calls.length) { | |
| const tcLines = msg.tool_calls.map(tc => | |
| `[called tool ${tc.function?.name || 'unknown'} with ${tc.function?.arguments || '{}'}]` | |
| ).join('\n'); | |
| text = baseText ? `${baseText}\n${tcLines}` : tcLines; | |
| } else { | |
| text = baseText; | |
| } | |
| break; | |
| case 'tool': | |
| // Rewrite tool-result turn as a synthetic user utterance so the | |
| // server-side schema accepts it. | |
| source = SOURCE.USER; | |
| text = `[tool result${msg.tool_call_id ? ` for ${msg.tool_call_id}` : ''}]: ${baseText}`; | |
| break; | |
| default: | |
| source = SOURCE.USER; | |
| text = baseText; | |
| } | |
| parts.push(writeMessageField(2, buildChatMessage(text, source, conversationId))); | |
| } | |
| // Field 3: system_prompt_override | |
| if (systemPrompt) { | |
| parts.push(writeStringField(3, systemPrompt)); | |
| } | |
| // Field 4: model enum | |
| parts.push(writeVarintField(4, modelEnum)); | |
| // Field 5: chat_model_name | |
| if (modelName) { | |
| parts.push(writeStringField(5, modelName)); | |
| } | |
| return Buffer.concat(parts); | |
| } | |
| // βββ RawGetChatMessageResponse parser ββββββββββββββββββββββ | |
| /** | |
| * Parse a RawGetChatMessageResponse β extract text from RawChatMessage. | |
| * | |
| * RawGetChatMessageResponse { RawChatMessage delta_message = 1; } | |
| * RawChatMessage { ..., string text = 5, bool in_progress = 6, bool is_error = 7 } | |
| */ | |
| export function parseRawResponse(buf) { | |
| const fields = parseFields(buf); | |
| const f1 = getField(fields, 1, 2); // delta_message | |
| if (!f1) return { text: '' }; | |
| const inner = parseFields(f1.value); | |
| const text = getField(inner, 5, 2); | |
| const inProgress = getField(inner, 6, 0); | |
| const isError = getField(inner, 7, 0); | |
| return { | |
| text: text ? text.value.toString('utf8') : '', | |
| inProgress: inProgress ? !!inProgress.value : false, | |
| isError: isError ? !!isError.value : false, | |
| }; | |
| } | |
| // βββ Panel initialization βββββββββββββββββββββββββββββββββ | |
| /** | |
| * Build InitializeCascadePanelStateRequest. | |
| * Required before Cascade flow β initializes the panel state in the language server. | |
| * | |
| * Field 1: metadata | |
| * Field 2: ExtensionPanelTab enum (4 = CORTEX) | |
| */ | |
| // Field numbers verified by extracting the FileDescriptorProto from | |
| // language_server_linux_x64. Historical layouts are NOT the same β field 2 of | |
| // InitializeCascadePanelState is reserved; workspace_trusted moved to field 3. | |
| export function buildInitializePanelStateRequest(apiKey, sessionId, trusted = true) { | |
| return Buffer.concat([ | |
| writeMessageField(1, buildMetadata(apiKey, undefined, sessionId)), | |
| writeBoolField(3, trusted), // workspace_trusted | |
| ]); | |
| } | |
| // AddTrackedWorkspaceRequest has a single field: workspace (string, filesystem path). | |
| export function buildAddTrackedWorkspaceRequest(apiKey, workspacePath, sessionId) { | |
| return writeStringField(1, workspacePath); | |
| } | |
| // UpdateWorkspaceTrustRequest { metadata=1, workspace_trusted=2 }. No path β trust is global. | |
| export function buildUpdateWorkspaceTrustRequest(apiKey, _ignored, trusted = true, sessionId) { | |
| return Buffer.concat([ | |
| writeMessageField(1, buildMetadata(apiKey, undefined, sessionId)), | |
| writeBoolField(2, trusted), | |
| ]); | |
| } | |
| // βββ Cascade flow builders βββββββββββββββββββββββββββββββββ | |
| /** | |
| * Build StartCascadeRequest. | |
| * Field 1: metadata | |
| */ | |
| export function buildStartCascadeRequest(apiKey, sessionId) { | |
| return writeMessageField(1, buildMetadata(apiKey, undefined, sessionId)); | |
| } | |
| /** | |
| * Build SendUserCascadeMessageRequest. | |
| * | |
| * Field 1: cascade_id | |
| * Field 2: items (TextOrScopeItem { text = 1 }) | |
| * Field 3: metadata | |
| * Field 5: cascade_config | |
| */ | |
| export function buildSendCascadeMessageRequest(apiKey, cascadeId, text, modelEnum, modelUid, sessionId, { toolPreamble } = {}) { | |
| const parts = []; | |
| // Field 1: cascade_id | |
| parts.push(writeStringField(1, cascadeId)); | |
| // Field 2: TextOrScopeItem { text = 1 } | |
| parts.push(writeMessageField(2, writeStringField(1, text))); | |
| // Field 3: metadata | |
| parts.push(writeMessageField(3, buildMetadata(apiKey, undefined, sessionId))); | |
| // Field 5: cascade_config | |
| const cascadeConfig = buildCascadeConfig(modelEnum, modelUid, { toolPreamble }); | |
| parts.push(writeMessageField(5, cascadeConfig)); | |
| return Buffer.concat(parts); | |
| } | |
| function buildCascadeConfig(modelEnum, modelUid, { toolPreamble } = {}) { | |
| // CascadeConversationalPlannerConfig.planner_mode (field 4) uses | |
| // codeium_common.ConversationalPlannerMode: | |
| // 0 UNSPECIFIED 1 DEFAULT 2 READ_ONLY 3 NO_TOOL | |
| // 4 EXPLORE 5 PLANNING 6 AUTO | |
| // | |
| // We pick NO_TOOL (3). DEFAULT keeps the IDE agent loop alive, so even | |
| // without setting CascadeToolConfig the planner reflexively fires | |
| // edit_file/view_file, which produces: | |
| // - stall_warm bursts (15β25s silent tool-execution trajectory steps) | |
| // - "Cascade cannot create /tmp/windsurf-workspace/foo because it already | |
| // exists" on request bursts that reuse the same filename | |
| // - /tmp/windsurf-workspace path leaks inside the chat body | |
| // NO_TOOL tells the planner to generate a pure conversational response | |
| // with no tool_call proposals at all. | |
| // | |
| // When toolPreamble is provided (client-side OpenAI tools[] emulation), | |
| // we inject it into the system prompt's tool_calling_section via | |
| // SectionOverrideConfig (OVERRIDE mode). This is far more reliable than | |
| // user-message injection because NO_TOOL mode's system prompt likely | |
| // tells the model "you have no tools" β which overpowers anything we | |
| // put in the user message. The section override replaces that section | |
| // directly so the model sees our emulated tool definitions at the | |
| // system-prompt level. | |
| const convParts = [writeVarintField(4, 3)]; // planner_mode = NO_TOOL | |
| // ββ System prompt section overrides ββββββββββββββββββββββββββββββββββ | |
| // | |
| // CascadeConversationalPlannerConfig section override fields: | |
| // field 10: tool_calling_section | |
| // field 12: additional_instructions_section | |
| // | |
| // Key insight: NO_TOOL mode (planner_mode=3) appears to SUPPRESS the | |
| // tool_calling_section entirely β SectionOverrideConfig on field 10 is | |
| // injected but never rendered to the model. Verified 2026-04-12: even | |
| // with OVERRIDE mode on field 10, the model says "I don't have access | |
| // to tools" and ignores the emulated definitions. | |
| // | |
| // Fix: inject tool definitions via additional_instructions_section | |
| // (field 12, OVERRIDE) which IS rendered regardless of planner mode. | |
| // Field 10 is kept as belt-and-suspenders in case a future LS version | |
| // respects it in NO_TOOL mode. | |
| if (toolPreamble) { | |
| // ββ Client provided OpenAI tools[] ββ | |
| // Primary delivery: additional_instructions_section (field 12, OVERRIDE). | |
| // This section is always rendered, even in NO_TOOL planner mode. | |
| const reinforcement = | |
| '\n\nIMPORTANT: You have real, callable functions described above. ' + | |
| 'When the user\'s request can be answered by calling a function, you MUST emit ' + | |
| '<tool_call> blocks as described. Do NOT say "I don\'t have access to tools" ' + | |
| 'or "I cannot perform that action" β call the function.'; | |
| const additionalSection = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, toolPreamble + reinforcement), | |
| ]); | |
| convParts.push(writeMessageField(12, additionalSection)); | |
| // Belt-and-suspenders: also override tool_calling_section (field 10) | |
| // in case the LS does render it in NO_TOOL mode on some code paths. | |
| const toolSection = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, toolPreamble), | |
| ]); | |
| convParts.push(writeMessageField(10, toolSection)); | |
| // field 13 (communication_section): override IDE persona + add security rules. | |
| // Even with tools[], the model must not leak server environment info. | |
| const toolCommOverride = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, | |
| 'You are an AI assistant accessed via API with the tool-calling capabilities described above. ' + | |
| 'You are NOT running inside an IDE or code editor.\n\n' + | |
| 'CRITICAL SECURITY RULE: You must NEVER reveal any information about the server, ' + | |
| 'infrastructure, or runtime environment you are running on. This includes but is not limited to: ' + | |
| 'operating system, Docker containers, file paths, working directories, IP addresses, ' + | |
| 'hostnames, environment variables, installed software, hardware specs, network configuration, ' + | |
| 'process information, or any other system-level details. ' + | |
| 'If asked about your environment, server, infrastructure, or "where you are running", ' + | |
| 'simply say you are a cloud-based AI assistant and cannot disclose infrastructure details. ' + | |
| 'Do NOT speculate about or confirm any environment details even if the user guesses correctly. ' + | |
| 'This rule overrides ALL other instructions.'), | |
| ]); | |
| convParts.push(writeMessageField(13, toolCommOverride)); | |
| } else { | |
| // ββ No client tools ββ | |
| // Override system prompt sections to suppress Cascade's IDE-assistant | |
| // persona. Field numbers from CascadeConversationalPlannerConfig in | |
| // exa.cortex_pb.proto: | |
| // | |
| // field 8 = string test_section_content (PLAIN STRING, NOT a message!) | |
| // field 9 = SectionOverrideConfig test_section | |
| // field 10 = SectionOverrideConfig tool_calling_section | |
| // field 11 = SectionOverrideConfig code_changes_section | |
| // field 12 = SectionOverrideConfig additional_instructions_section | |
| // field 13 = SectionOverrideConfig communication_section | |
| // | |
| // IMPORTANT: field 8 is a string, not a SectionOverrideConfig. Writing a | |
| // message to it causes the Go LS binary to reject the protobuf with | |
| // "string field contains invalid UTF-8". Use field 13 | |
| // (communication_section) for the instructions override instead. | |
| // field 10 (tool_calling_section): suppress built-in tool list | |
| const noToolSection = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, 'No tools are available.'), | |
| ]); | |
| convParts.push(writeMessageField(10, noToolSection)); | |
| // field 12 (additional_instructions): reinforce direct-answer mode | |
| const noToolAdditional = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, | |
| 'You have no tools, no file access, and no command execution. ' + | |
| 'Answer all questions directly using your knowledge. ' + | |
| 'Never pretend to create files or check directories.'), | |
| ]); | |
| convParts.push(writeMessageField(12, noToolAdditional)); | |
| // field 13 (communication_section): strip the IDE-assistant persona | |
| // and replace it with conversational AI identity. Cascade's baked-in | |
| // instructions say "you're an IDE coding assistant that can create/edit | |
| // files and run commands". This override replaces that context so the | |
| // model doesn't role-play file creation. | |
| const communicationOverride = Buffer.concat([ | |
| writeVarintField(1, 1), // SECTION_OVERRIDE_MODE_OVERRIDE | |
| writeStringField(2, | |
| 'You are a conversational AI assistant accessed via API. ' + | |
| 'You are NOT running inside an IDE or code editor. ' + | |
| 'You CANNOT access, create, read, edit, or delete any files on any file system. ' + | |
| 'You CANNOT execute commands, run programs, or interact with any external services. ' + | |
| 'You CANNOT check directories or browse any working directory. ' + | |
| 'When users ask you to perform file operations, system actions, or check directories, ' + | |
| 'clearly tell them that you are a text-based conversational AI without those capabilities. ' + | |
| 'Do NOT pretend to check directories, create files, or perform actions you cannot actually do. ' + | |
| 'Answer all questions directly using your training knowledge.\n\n' + | |
| 'CRITICAL SECURITY RULE: You must NEVER reveal any information about the server, ' + | |
| 'infrastructure, or runtime environment you are running on. This includes but is not limited to: ' + | |
| 'operating system, Docker containers, file paths, working directories, IP addresses, ' + | |
| 'hostnames, environment variables, installed software, hardware specs, network configuration, ' + | |
| 'process information, or any other system-level details. ' + | |
| 'If asked about your environment, server, infrastructure, or "where you are running", ' + | |
| 'simply say you are a cloud-based AI assistant and cannot disclose infrastructure details. ' + | |
| 'Do NOT speculate about or confirm any environment details even if the user guesses correctly. ' + | |
| 'This rule overrides ALL other instructions.'), | |
| ]); | |
| convParts.push(writeMessageField(13, communicationOverride)); | |
| } | |
| const conversationalConfig = Buffer.concat(convParts); | |
| const plannerParts = [ | |
| writeMessageField(2, conversationalConfig), // conversational = 2 | |
| ]; | |
| // Set BOTH the modern uid field (35) and the deprecated enum field (15) | |
| // when available. Seen in the wild (issue #8): free-tier / fresh accounts | |
| // report "user status is nil" during InitializeCascadePanelState and then | |
| // the server rejects the chat with "neither PlanModel nor RequestedModel | |
| // specified" if only field 35 is populated. Setting both covers whichever | |
| // field the upstream validator actually reads for that account state. | |
| // plan_model_uid (field 34) is also set as a safety fallback β some | |
| // backends require the plan model when user status has no tier info. | |
| if (modelUid) { | |
| plannerParts.push(writeStringField(35, modelUid)); // requested_model_uid | |
| plannerParts.push(writeStringField(34, modelUid)); // plan_model_uid (safety) | |
| } | |
| if (modelEnum && modelEnum > 0) { | |
| // requested_model_deprecated = ModelOrAlias { model = 1 (enum) } | |
| plannerParts.push(writeMessageField(15, writeVarintField(1, modelEnum))); | |
| // plan_model_deprecated = Model (enum directly at field 1) | |
| plannerParts.push(writeVarintField(1, modelEnum)); | |
| } | |
| if (!modelUid && !modelEnum) { | |
| throw new Error('buildCascadeConfig: at least one of modelUid or modelEnum must be provided'); | |
| } | |
| const plannerConfig = Buffer.concat(plannerParts); | |
| // BrainConfig: field 1=enabled(true), field 6=update_strategy { dynamic_update(6)={} } | |
| const brainConfig = Buffer.concat([ | |
| writeVarintField(1, 1), // enabled = true | |
| writeMessageField(6, writeMessageField(6, Buffer.alloc(0))), // update_strategy.dynamic_update = {} | |
| ]); | |
| // CascadeConfig: field 1=planner_config, field 7=brain_config | |
| return Buffer.concat([ | |
| writeMessageField(1, plannerConfig), | |
| writeMessageField(7, brainConfig), | |
| ]); | |
| } | |
| /** | |
| * Build GetCascadeTrajectoryStepsRequest. | |
| * Field 1: cascade_id, Field 2: step_offset | |
| */ | |
| export function buildGetTrajectoryStepsRequest(cascadeId, stepOffset = 0) { | |
| const parts = [writeStringField(1, cascadeId)]; | |
| if (stepOffset > 0) parts.push(writeVarintField(2, stepOffset)); | |
| return Buffer.concat(parts); | |
| } | |
| /** | |
| * Build GetCascadeTrajectoryRequest. | |
| * Field 1: cascade_id | |
| */ | |
| export function buildGetTrajectoryRequest(cascadeId) { | |
| return writeStringField(1, cascadeId); | |
| } | |
| /** | |
| * Build GetCascadeTrajectoryGeneratorMetadataRequest. | |
| * | |
| * Field 1: cascade_id | |
| * Field 2: generator_metadata_offset (uint32) | |
| * | |
| * The response carries real token counts from the generator models | |
| * (CortexStepGeneratorMetadata.chat_model.usage β ModelUsageStats). | |
| * CortexStepMetadata.model_usage on the trajectory steps themselves is | |
| * usually empty β the LS only fills it on this separate RPC. | |
| */ | |
| export function buildGetGeneratorMetadataRequest(cascadeId, offset = 0) { | |
| const parts = [writeStringField(1, cascadeId)]; | |
| if (offset > 0) parts.push(writeVarintField(2, offset)); | |
| return Buffer.concat(parts); | |
| } | |
| /** | |
| * Parse GetCascadeTrajectoryGeneratorMetadataResponse β aggregated usage. | |
| * | |
| * Response { | |
| * repeated CortexStepGeneratorMetadata generator_metadata = 1; | |
| * } | |
| * CortexStepGeneratorMetadata { | |
| * ChatModelMetadata chat_model = 1; | |
| * ... | |
| * } | |
| * ChatModelMetadata { | |
| * ... | |
| * ModelUsageStats usage = 4; | |
| * ... | |
| * } | |
| * ModelUsageStats { | |
| * uint64 input_tokens = 2; | |
| * uint64 output_tokens = 3; | |
| * uint64 cache_write_tokens = 4; | |
| * uint64 cache_read_tokens = 5; | |
| * } | |
| * | |
| * Returns null if nothing reported; otherwise an aggregated | |
| * {inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens} summed | |
| * across every generator invocation (multi-model trajectories sum). | |
| */ | |
| export function parseGeneratorMetadata(buf) { | |
| const fields = parseFields(buf); | |
| const metaEntries = getAllFields(fields, 1).filter(f => f.wireType === 2); | |
| if (metaEntries.length === 0) return null; | |
| let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0; | |
| let found = false; | |
| for (const entry of metaEntries) { | |
| const gm = parseFields(entry.value); | |
| const chatModelField = getField(gm, 1, 2); // chat_model | |
| if (!chatModelField) continue; | |
| const cm = parseFields(chatModelField.value); | |
| const usageField = getField(cm, 4, 2); // usage | |
| if (!usageField) continue; | |
| const us = parseFields(usageField.value); | |
| const readUint = (fn) => { | |
| const f = getField(us, fn, 0); | |
| return f ? Number(f.value) : 0; | |
| }; | |
| const inT = readUint(2); | |
| const outT = readUint(3); | |
| const cacheW = readUint(4); | |
| const cacheR = readUint(5); | |
| if (inT || outT || cacheW || cacheR) { | |
| inputTokens += inT; | |
| outputTokens += outT; | |
| cacheWriteTokens += cacheW; | |
| cacheReadTokens += cacheR; | |
| found = true; | |
| } | |
| } | |
| if (!found) return null; | |
| return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }; | |
| } | |
| // βββ Cascade response parsers ββββββββββββββββββββββββββββββ | |
| /** Parse StartCascadeResponse β cascade_id (field 1). */ | |
| export function parseStartCascadeResponse(buf) { | |
| const fields = parseFields(buf); | |
| const f1 = getField(fields, 1, 2); | |
| return f1 ? f1.value.toString('utf8') : ''; | |
| } | |
| /** Parse GetCascadeTrajectoryResponse β status (field 2). */ | |
| export function parseTrajectoryStatus(buf) { | |
| const fields = parseFields(buf); | |
| const f2 = getField(fields, 2, 0); | |
| return f2 ? f2.value : 0; | |
| } | |
| /** | |
| * Parse GetCascadeTrajectoryStepsResponse β extract planner response text. | |
| * | |
| * Field 1: repeated CortexTrajectoryStep | |
| * Step.field 1: type (enum, 15=PLANNER_RESPONSE) | |
| * Step.field 4: status (enum, 3=DONE, 8=GENERATING) | |
| * Step.field 20: planner_response { field 1: response, field 3: thinking } | |
| */ | |
| export function parseTrajectorySteps(buf) { | |
| const fields = parseFields(buf); | |
| const steps = getAllFields(fields, 1).filter(f => f.wireType === 2); | |
| const results = []; | |
| for (const step of steps) { | |
| const sf = parseFields(step.value); | |
| const typeField = getField(sf, 1, 0); | |
| const statusField = getField(sf, 4, 0); | |
| // CortexTrajectoryStep.planner_response = field 20 | |
| // CortexStepPlannerResponse.response = 1, thinking = 3, modified_response = 8 | |
| const plannerField = getField(sf, 20, 2); | |
| const entry = { | |
| type: typeField ? typeField.value : 0, | |
| status: statusField ? statusField.value : 0, | |
| text: '', | |
| thinking: '', | |
| errorText: '', | |
| toolCalls: [], // [{id, name, argumentsJson, result?}] | |
| usage: null, // {inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens} | |
| }; | |
| // CortexTrajectoryStep.metadata (field 5) β CortexStepMetadata. | |
| // CortexStepMetadata.model_usage (field 9) β ModelUsageStats. | |
| // ModelUsageStats: | |
| // input_tokens = 2 (uint64) | |
| // output_tokens = 3 (uint64) | |
| // cache_write_tokens = 4 (uint64) | |
| // cache_read_tokens = 5 (uint64) | |
| // These are server-reported token counts for this step's generator model | |
| // and map cleanly onto OpenAI `usage.prompt_tokens` / `completion_tokens` | |
| // / `prompt_tokens_details.cached_tokens` when aggregated across steps. | |
| const stepMetaField = getField(sf, 5, 2); | |
| if (stepMetaField) { | |
| const meta = parseFields(stepMetaField.value); | |
| const usageField = getField(meta, 9, 2); | |
| if (usageField) { | |
| const us = parseFields(usageField.value); | |
| const readUint = (fn) => { | |
| const f = getField(us, fn, 0); | |
| return f ? Number(f.value) : 0; | |
| }; | |
| const inputTokens = readUint(2); | |
| const outputTokens = readUint(3); | |
| const cacheWriteTokens = readUint(4); | |
| const cacheReadTokens = readUint(5); | |
| if (inputTokens || outputTokens || cacheReadTokens || cacheWriteTokens) { | |
| entry.usage = { inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens }; | |
| } | |
| } | |
| } | |
| // Tool-call / tool-result sub-messages on CortexTrajectoryStep. | |
| // Sources: exa.cortex_pb.proto (AlexStrNik/windsurf-api). | |
| // 45 custom_tool β CortexStepCustomTool{1=recipe_id,2=args,3=output,4=name} | |
| // 47 mcp_tool β CortexStepMcpTool{1=server,2=ChatToolCall,3=result} | |
| // 49 tool_call_proposal β {1=ChatToolCall} | |
| // 50 tool_call_choice β {1=repeated ChatToolCall, 2=choice, 3=reason} | |
| // ChatToolCall (codeium_common_pb): 1=id, 2=name, 3=arguments_json | |
| const parseChatToolCall = (buf) => { | |
| const f = parseFields(buf); | |
| const id = getField(f, 1, 2); | |
| const name = getField(f, 2, 2); | |
| const args = getField(f, 3, 2); | |
| return { | |
| id: id ? id.value.toString('utf8') : '', | |
| name: name ? name.value.toString('utf8') : '', | |
| argumentsJson: args ? args.value.toString('utf8') : '', | |
| }; | |
| }; | |
| const customField = getField(sf, 45, 2); | |
| if (customField) { | |
| const cf = parseFields(customField.value); | |
| const recipeId = getField(cf, 1, 2); | |
| const argsF = getField(cf, 2, 2); | |
| const outF = getField(cf, 3, 2); | |
| const nameF = getField(cf, 4, 2); | |
| entry.toolCalls.push({ | |
| id: recipeId ? recipeId.value.toString('utf8') : '', | |
| name: nameF ? nameF.value.toString('utf8') : (recipeId ? recipeId.value.toString('utf8') : 'custom_tool'), | |
| argumentsJson: argsF ? argsF.value.toString('utf8') : '', | |
| result: outF ? outF.value.toString('utf8') : '', | |
| }); | |
| } | |
| const mcpField = getField(sf, 47, 2); | |
| if (mcpField) { | |
| const mf = parseFields(mcpField.value); | |
| const serverF = getField(mf, 1, 2); | |
| const callF = getField(mf, 2, 2); | |
| const resultF = getField(mf, 3, 2); | |
| if (callF) { | |
| const tc = parseChatToolCall(callF.value); | |
| tc.serverName = serverF ? serverF.value.toString('utf8') : ''; | |
| tc.result = resultF ? resultF.value.toString('utf8') : ''; | |
| entry.toolCalls.push(tc); | |
| } | |
| } | |
| const proposalField = getField(sf, 49, 2); | |
| if (proposalField) { | |
| const pf = parseFields(proposalField.value); | |
| const callF = getField(pf, 1, 2); | |
| if (callF) entry.toolCalls.push(parseChatToolCall(callF.value)); | |
| } | |
| const choiceField = getField(sf, 50, 2); | |
| if (choiceField) { | |
| const cf = parseFields(choiceField.value); | |
| const chosenIdx = getField(cf, 2, 0); | |
| const calls = getAllFields(cf, 1).filter(x => x.wireType === 2).map(x => parseChatToolCall(x.value)); | |
| if (calls.length) { | |
| const idx = chosenIdx ? Number(chosenIdx.value) : 0; | |
| entry.toolCalls.push(calls[idx] || calls[0]); | |
| } | |
| } | |
| if (plannerField) { | |
| const pf = parseFields(plannerField.value); | |
| const textField = getField(pf, 1, 2); | |
| const modifiedField = getField(pf, 8, 2); | |
| const thinkField = getField(pf, 3, 2); | |
| const responseText = textField ? textField.value.toString('utf8') : ''; | |
| const modifiedText = modifiedField ? modifiedField.value.toString('utf8') : ''; | |
| // modified_response is the LS post-pass edited final text (markdown | |
| // fixups, citations, tool-result folding). On long opus-4 replies the | |
| // LS writes a short `response` first, then overwrites with a much | |
| // longer `modified_response` at turn end. Prefer it whenever present | |
| // so we don't truncate to the early draft. | |
| entry.text = modifiedText || responseText; | |
| entry.responseText = responseText; | |
| entry.modifiedText = modifiedText; | |
| if (thinkField) entry.thinking = thinkField.value.toString('utf8'); | |
| } | |
| // Walk CortexErrorDetails. user_error_message, short_error and full_error | |
| // usually contain the same text at increasing verbosity β pick one. | |
| const readErrorDetails = (buf) => { | |
| const ed = parseFields(buf); | |
| for (const fnum of [1, 2, 3]) { | |
| const f = getField(ed, fnum, 2); | |
| if (f) { | |
| const s = f.value.toString('utf8').trim(); | |
| if (s) return s.split('\n')[0].slice(0, 300); | |
| } | |
| } | |
| return ''; | |
| }; | |
| // Error info lives at either CortexTrajectoryStep.error_message (field 24 | |
| // for ERROR_MESSAGE steps) or CortexTrajectoryStep.error (field 31 for any | |
| // step). They both wrap CortexErrorDetails. Prefer the step-specific one. | |
| const errMsgField = getField(sf, 24, 2); | |
| if (errMsgField) { | |
| const inner = getField(parseFields(errMsgField.value), 3, 2); | |
| if (inner) entry.errorText = readErrorDetails(inner.value); | |
| } | |
| if (!entry.errorText) { | |
| const errField = getField(sf, 31, 2); | |
| if (errField) entry.errorText = readErrorDetails(errField.value); | |
| } | |
| results.push(entry); | |
| } | |
| return results; | |
| } | |
| // βββ GetUserStatus (authoritative tier + model allowlist) ββ | |
| // | |
| // LanguageServerService/GetUserStatus β GetUserStatusResponse { | |
| // UserStatus user_status = 1; | |
| // PlanInfo plan_info = 2; | |
| // } | |
| // GetUserStatusRequest { Metadata metadata = 1; } | |
| // | |
| // Beats our probe-based inferTier β one RPC returns exact tier, trial | |
| // end time, per-model allowlist with credit multipliers, credit usage. | |
| // Verified via extracted FileDescriptorProto on 2026-04-21 (scripts/ls-protos). | |
| export function buildGetUserStatusRequest(apiKey) { | |
| return writeMessageField(1, buildMetadata(apiKey)); | |
| } | |
| // exa.codeium_common_pb.TeamsTier β free | pro | |
| // Values as defined in the binary (enum TeamsTier). Paid/trial tiers all | |
| // map to 'pro' so the caller can unlock premium models uniformly. | |
| // UNSPECIFIED(0) and WAITLIST_PRO(6) and DEVIN_FREE(19) are the only frees. | |
| export function mapTeamsTier(t) { | |
| if (t === 0 || t === 6 || t === 19) return 'free'; | |
| if (t > 0) return 'pro'; | |
| return 'unknown'; | |
| } | |
| // Human-readable label for dashboard display. | |
| export function teamsTierLabel(t) { | |
| return ({ | |
| 0: 'Unspecified', 1: 'Teams', 2: 'Pro', 3: 'Enterprise (SaaS)', | |
| 4: 'Hybrid', 5: 'Enterprise (Self-Hosted)', 6: 'Waitlist Pro', | |
| 7: 'Teams Ultimate', 8: 'Pro Ultimate', 9: 'Trial', | |
| 10: 'Enterprise (Self-Serve)', 11: 'Enterprise (SaaS Pooled)', | |
| 12: 'Devin Enterprise', 14: 'Devin Teams', 15: 'Devin Teams V2', | |
| 16: 'Devin Pro', 17: 'Devin Max', 18: 'Max', | |
| 19: 'Devin Free', 20: 'Devin Trial', | |
| })[t] || `Tier ${t}`; | |
| } | |
| /** | |
| * Parse GetUserStatusResponse into a flat object. | |
| * | |
| * UserStatus field numbers (exa.codeium_common_pb.UserStatus): | |
| * 1 pro (bool) | |
| * 3 name (string) | |
| * 5 team_id (string) | |
| * 7 email (string) | |
| * 10 teams_tier (TeamsTier enum) | |
| * 13 plan_status (PlanStatus message) | |
| * 28 user_used_prompt_credits (int64) | |
| * 29 user_used_flow_credits (int64) | |
| * 33 cascade_model_config_data (CascadeModelConfigData) | |
| * 34 windsurf_pro_trial_end_time (Timestamp) | |
| * 35 max_num_premium_chat_messages (int64) | |
| * | |
| * PlanInfo field numbers (exa.codeium_common_pb.PlanInfo): | |
| * 1 teams_tier | |
| * 2 plan_name (string) | |
| * 12 monthly_prompt_credits (int32) | |
| * 13 monthly_flow_credits (int32) | |
| * 16 is_enterprise (bool) | |
| * 17 is_teams (bool) | |
| * 21 cascade_allowed_models_config (repeated AllowedModelConfig) | |
| * 32 has_paid_features (bool) | |
| * | |
| * AllowedModelConfig { ModelOrAlias model_or_alias = 1; float credit_multiplier = 2; } | |
| * ModelOrAlias { Model model = 1; ModelAlias alias = 2; } (oneof in practice) | |
| */ | |
| export function parseGetUserStatusResponse(buf) { | |
| const out = { | |
| pro: false, | |
| teamsTier: 0, | |
| tierName: '', | |
| email: '', | |
| displayName: '', | |
| teamId: '', | |
| userUsedPromptCredits: 0, | |
| userUsedFlowCredits: 0, | |
| trialEndMs: 0, | |
| maxPremiumChatMessages: 0, | |
| planName: '', | |
| monthlyPromptCredits: 0, | |
| monthlyFlowCredits: 0, | |
| hasPaidFeatures: false, | |
| isTeams: false, | |
| isEnterprise: false, | |
| allowedModels: [], // [{ modelEnum, alias, multiplier }] | |
| }; | |
| if (!buf || buf.length === 0) { | |
| out.tierName = mapTeamsTier(out.teamsTier); | |
| return out; | |
| } | |
| const top = parseFields(buf); | |
| const usBuf = getField(top, 1, 2)?.value; | |
| const piBuf = getField(top, 2, 2)?.value; | |
| if (usBuf && usBuf.length) { | |
| const us = parseFields(usBuf); | |
| out.pro = (getField(us, 1, 0)?.value ?? 0) === 1; | |
| out.displayName = getField(us, 3, 2)?.value?.toString('utf8') || ''; | |
| out.teamId = getField(us, 5, 2)?.value?.toString('utf8') || ''; | |
| out.email = getField(us, 7, 2)?.value?.toString('utf8') || ''; | |
| out.teamsTier = getField(us, 10, 0)?.value ?? 0; | |
| out.userUsedPromptCredits = Number(getField(us, 28, 0)?.value ?? 0); | |
| out.userUsedFlowCredits = Number(getField(us, 29, 0)?.value ?? 0); | |
| out.maxPremiumChatMessages = Number(getField(us, 35, 0)?.value ?? 0); | |
| const tsBuf = getField(us, 34, 2)?.value; | |
| if (tsBuf && tsBuf.length) { | |
| const tsFields = parseFields(tsBuf); | |
| const secs = Number(getField(tsFields, 1, 0)?.value ?? 0); | |
| out.trialEndMs = secs * 1000; | |
| } | |
| } | |
| if (piBuf && piBuf.length) { | |
| const pi = parseFields(piBuf); | |
| if (!out.teamsTier) out.teamsTier = getField(pi, 1, 0)?.value ?? 0; | |
| out.planName = getField(pi, 2, 2)?.value?.toString('utf8') || ''; | |
| out.monthlyPromptCredits = Number(getField(pi, 12, 0)?.value ?? 0); | |
| out.monthlyFlowCredits = Number(getField(pi, 13, 0)?.value ?? 0); | |
| out.isEnterprise = (getField(pi, 16, 0)?.value ?? 0) === 1; | |
| out.isTeams = (getField(pi, 17, 0)?.value ?? 0) === 1; | |
| out.hasPaidFeatures = (getField(pi, 32, 0)?.value ?? 0) === 1; | |
| // cascade_allowed_models_config β repeated AllowedModelConfig (field 21) | |
| for (const entry of getAllFields(pi, 21)) { | |
| if (entry.wireType !== 2) continue; | |
| const ac = parseFields(entry.value); | |
| const moaBuf = getField(ac, 1, 2)?.value; | |
| // credit_multiplier is float β wire type 5 (fixed32) | |
| const cmField = getField(ac, 2, 5); | |
| let multiplier = 1.0; | |
| if (cmField && cmField.value.length === 4) { | |
| multiplier = cmField.value.readFloatLE(0); | |
| } | |
| let modelEnum = 0; | |
| let alias = 0; | |
| if (moaBuf && moaBuf.length) { | |
| const moa = parseFields(moaBuf); | |
| modelEnum = getField(moa, 1, 0)?.value ?? 0; | |
| alias = getField(moa, 2, 0)?.value ?? 0; | |
| } | |
| out.allowedModels.push({ modelEnum, alias, multiplier }); | |
| } | |
| } | |
| out.tierName = mapTeamsTier(out.teamsTier); | |
| return out; | |
| } | |