opencode-cursor-proxy 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +139 -0
  3. package/README.zh-CN.md +136 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/api/agent-service.d.ts +136 -0
  8. package/dist/lib/api/agent-service.js +938 -0
  9. package/dist/lib/api/agent-service.js.map +1 -0
  10. package/dist/lib/api/ai-service.d.ts +26 -0
  11. package/dist/lib/api/ai-service.js +38 -0
  12. package/dist/lib/api/ai-service.js.map +1 -0
  13. package/dist/lib/api/cursor-client.d.ts +119 -0
  14. package/dist/lib/api/cursor-client.js +511 -0
  15. package/dist/lib/api/cursor-client.js.map +1 -0
  16. package/dist/lib/api/cursor-models.d.ts +13 -0
  17. package/dist/lib/api/cursor-models.js +34 -0
  18. package/dist/lib/api/cursor-models.js.map +1 -0
  19. package/dist/lib/api/openai-compat.d.ts +10 -0
  20. package/dist/lib/api/openai-compat.js +262 -0
  21. package/dist/lib/api/openai-compat.js.map +1 -0
  22. package/dist/lib/api/proto/agent-messages.d.ts +25 -0
  23. package/dist/lib/api/proto/agent-messages.js +132 -0
  24. package/dist/lib/api/proto/agent-messages.js.map +1 -0
  25. package/dist/lib/api/proto/bidi.d.ts +17 -0
  26. package/dist/lib/api/proto/bidi.js +24 -0
  27. package/dist/lib/api/proto/bidi.js.map +1 -0
  28. package/dist/lib/api/proto/decoding.d.ts +19 -0
  29. package/dist/lib/api/proto/decoding.js +118 -0
  30. package/dist/lib/api/proto/decoding.js.map +1 -0
  31. package/dist/lib/api/proto/encoding.d.ts +64 -0
  32. package/dist/lib/api/proto/encoding.js +180 -0
  33. package/dist/lib/api/proto/encoding.js.map +1 -0
  34. package/dist/lib/api/proto/exec.d.ts +12 -0
  35. package/dist/lib/api/proto/exec.js +383 -0
  36. package/dist/lib/api/proto/exec.js.map +1 -0
  37. package/dist/lib/api/proto/index.d.ts +13 -0
  38. package/dist/lib/api/proto/index.js +10 -0
  39. package/dist/lib/api/proto/index.js.map +1 -0
  40. package/dist/lib/api/proto/interaction.d.ts +15 -0
  41. package/dist/lib/api/proto/interaction.js +99 -0
  42. package/dist/lib/api/proto/interaction.js.map +1 -0
  43. package/dist/lib/api/proto/kv.d.ts +52 -0
  44. package/dist/lib/api/proto/kv.js +156 -0
  45. package/dist/lib/api/proto/kv.js.map +1 -0
  46. package/dist/lib/api/proto/tool-calls.d.ts +9 -0
  47. package/dist/lib/api/proto/tool-calls.js +144 -0
  48. package/dist/lib/api/proto/tool-calls.js.map +1 -0
  49. package/dist/lib/api/proto/types.d.ts +201 -0
  50. package/dist/lib/api/proto/types.js +10 -0
  51. package/dist/lib/api/proto/types.js.map +1 -0
  52. package/dist/lib/auth/helpers.d.ts +40 -0
  53. package/dist/lib/auth/helpers.js +103 -0
  54. package/dist/lib/auth/helpers.js.map +1 -0
  55. package/dist/lib/auth/index.d.ts +7 -0
  56. package/dist/lib/auth/index.js +10 -0
  57. package/dist/lib/auth/index.js.map +1 -0
  58. package/dist/lib/auth/login.d.ts +55 -0
  59. package/dist/lib/auth/login.js +184 -0
  60. package/dist/lib/auth/login.js.map +1 -0
  61. package/dist/lib/config.d.ts +153 -0
  62. package/dist/lib/config.js +182 -0
  63. package/dist/lib/config.js.map +1 -0
  64. package/dist/lib/openai-compat/handler.d.ts +40 -0
  65. package/dist/lib/openai-compat/handler.js +808 -0
  66. package/dist/lib/openai-compat/handler.js.map +1 -0
  67. package/dist/lib/openai-compat/index.d.ts +9 -0
  68. package/dist/lib/openai-compat/index.js +13 -0
  69. package/dist/lib/openai-compat/index.js.map +1 -0
  70. package/dist/lib/openai-compat/types.d.ts +127 -0
  71. package/dist/lib/openai-compat/types.js +6 -0
  72. package/dist/lib/openai-compat/types.js.map +1 -0
  73. package/dist/lib/openai-compat/utils.d.ts +143 -0
  74. package/dist/lib/openai-compat/utils.js +348 -0
  75. package/dist/lib/openai-compat/utils.js.map +1 -0
  76. package/dist/lib/session-reuse.d.ts +88 -0
  77. package/dist/lib/session-reuse.js +198 -0
  78. package/dist/lib/session-reuse.js.map +1 -0
  79. package/dist/lib/storage.d.ts +55 -0
  80. package/dist/lib/storage.js +159 -0
  81. package/dist/lib/storage.js.map +1 -0
  82. package/dist/lib/utils/cache.d.ts +131 -0
  83. package/dist/lib/utils/cache.js +297 -0
  84. package/dist/lib/utils/cache.js.map +1 -0
  85. package/dist/lib/utils/fetch.d.ts +84 -0
  86. package/dist/lib/utils/fetch.js +261 -0
  87. package/dist/lib/utils/fetch.js.map +1 -0
  88. package/dist/lib/utils/index.d.ts +13 -0
  89. package/dist/lib/utils/index.js +22 -0
  90. package/dist/lib/utils/index.js.map +1 -0
  91. package/dist/lib/utils/jwt.d.ts +40 -0
  92. package/dist/lib/utils/jwt.js +102 -0
  93. package/dist/lib/utils/jwt.js.map +1 -0
  94. package/dist/lib/utils/logger.d.ts +107 -0
  95. package/dist/lib/utils/logger.js +227 -0
  96. package/dist/lib/utils/logger.js.map +1 -0
  97. package/dist/lib/utils/model-resolver.d.ts +49 -0
  98. package/dist/lib/utils/model-resolver.js +503 -0
  99. package/dist/lib/utils/model-resolver.js.map +1 -0
  100. package/dist/lib/utils/request-pool.d.ts +38 -0
  101. package/dist/lib/utils/request-pool.js +105 -0
  102. package/dist/lib/utils/request-pool.js.map +1 -0
  103. package/dist/lib/utils/request-transformer.d.ts +87 -0
  104. package/dist/lib/utils/request-transformer.js +154 -0
  105. package/dist/lib/utils/request-transformer.js.map +1 -0
  106. package/dist/lib/utils/tokenizer.d.ts +14 -0
  107. package/dist/lib/utils/tokenizer.js +76 -0
  108. package/dist/lib/utils/tokenizer.js.map +1 -0
  109. package/dist/plugin/index.d.ts +8 -0
  110. package/dist/plugin/index.js +9 -0
  111. package/dist/plugin/index.js.map +1 -0
  112. package/dist/plugin/plugin.d.ts +21 -0
  113. package/dist/plugin/plugin.js +309 -0
  114. package/dist/plugin/plugin.js.map +1 -0
  115. package/dist/plugin/types.d.ts +120 -0
  116. package/dist/plugin/types.js +7 -0
  117. package/dist/plugin/types.js.map +1 -0
  118. package/dist/server.d.ts +15 -0
  119. package/dist/server.js +95 -0
  120. package/dist/server.js.map +1 -0
  121. package/package.json +79 -0
@@ -0,0 +1,938 @@
1
+ /**
2
+ * Cursor Agent Service Client
3
+ *
4
+ * Implements the AgentService API for chat functionality.
5
+ * Uses the BidiSse pattern:
6
+ * - RunSSE (server-streaming) to receive responses
7
+ * - BidiAppend (unary) to send client messages
8
+ *
9
+ * Proto structure:
10
+ * AgentClientMessage:
11
+ * field 1: run_request (AgentRunRequest)
12
+ * field 2: exec_client_message (ExecClientMessage)
13
+ * field 3: kv_client_message (KvClientMessage)
14
+ * field 4: conversation_action (ConversationAction)
15
+ * field 5: exec_client_control_message
16
+ * field 6: interaction_response
17
+ *
18
+ * AgentServerMessage:
19
+ * field 1: interaction_update (InteractionUpdate)
20
+ * field 2: exec_server_message (ExecServerMessage)
21
+ * field 3: conversation_checkpoint_update (completion signal)
22
+ * field 4: kv_server_message (KvServerMessage)
23
+ * field 5: exec_server_control_message
24
+ * field 7: interaction_query
25
+ *
26
+ * InteractionUpdate.message:
27
+ * field 1: text_delta
28
+ * field 4: thinking_delta
29
+ * field 8: token_delta
30
+ * field 13: heartbeat
31
+ * field 14: turn_ended
32
+ */
33
+ import { randomUUID } from "node:crypto";
34
+ import { readdirSync } from "node:fs";
35
+ import { homedir } from "node:os";
36
+ import { join } from "node:path";
37
+ import { generateChecksum, addConnectEnvelope } from "./cursor-client";
38
+ import { encodeMessageField, parseProtoFields, parseExecServerMessage, buildExecClientMessageWithMcpResult, buildExecClientMessageWithShellResult, buildExecClientMessageWithLsResult, buildExecClientMessageWithRequestContextResult, buildExecClientMessageWithReadResult, buildExecClientMessageWithGrepResult, buildExecClientMessageWithWriteResult, buildAgentClientMessageWithExec, buildExecClientControlMessage, buildAgentClientMessageWithExecControl, parseKvServerMessage, buildKvClientMessage, buildAgentClientMessageWithKv, AgentMode, encodeBidiRequestId, encodeBidiAppendRequest, buildRequestContext, encodeUserMessage, encodeUserMessageAction, encodeConversationAction, encodeConversationActionWithResume, encodeAgentClientMessageWithConversationAction, encodeModelDetails, encodeAgentRunRequest, encodeAgentClientMessage, parseInteractionUpdate, analyzeBlobData, extractAssistantContent, } from "./proto";
39
+ // Re-export types that external code may need
40
+ export { AgentMode };
41
+ import { config, isDebugEnabled, isTimingEnabled } from "../config";
42
+ import { apiLogger, createTimer } from "../utils/logger";
43
+ // Create specialized loggers
44
+ const debugLog = isDebugEnabled() ? (msg) => apiLogger.debug(msg) : () => { };
45
+ const timingLog = isTimingEnabled() ? (msg) => apiLogger.info(msg) : () => { };
46
+ function createTimingMetrics() {
47
+ return {
48
+ requestStart: Date.now(),
49
+ chunkCount: 0,
50
+ textChunks: 0,
51
+ toolCalls: 0,
52
+ execRequests: 0,
53
+ kvMessages: 0,
54
+ heartbeats: 0,
55
+ };
56
+ }
57
+ function logTimingMetrics(metrics) {
58
+ const total = Date.now() - metrics.requestStart;
59
+ metrics.totalMs = total;
60
+ timingLog("[TIMING] ═══════════════════════════════════════════════════════");
61
+ timingLog("[TIMING] Request Performance Summary");
62
+ timingLog("[TIMING] ───────────────────────────────────────────────────────");
63
+ timingLog(`[TIMING] Message build: ${metrics.messageBuildMs ?? "-"}ms`);
64
+ timingLog(`[TIMING] SSE connection: ${metrics.sseConnectionMs ?? "-"}ms`);
65
+ timingLog(`[TIMING] First BidiAppend: ${metrics.firstBidiAppendMs ?? "-"}ms`);
66
+ timingLog(`[TIMING] First chunk: ${metrics.firstChunkMs ?? "-"}ms`);
67
+ timingLog(`[TIMING] First text: ${metrics.firstTextMs ?? "-"}ms`);
68
+ timingLog(`[TIMING] First tool call: ${metrics.firstToolCallMs ?? "-"}ms`);
69
+ timingLog(`[TIMING] Turn ended: ${metrics.turnEndedMs ?? "-"}ms`);
70
+ timingLog(`[TIMING] Total: ${total}ms`);
71
+ timingLog("[TIMING] ───────────────────────────────────────────────────────");
72
+ timingLog(`[TIMING] Chunks: ${metrics.chunkCount} (text: ${metrics.textChunks}, tools: ${metrics.toolCalls})`);
73
+ timingLog(`[TIMING] Exec requests: ${metrics.execRequests}, KV messages: ${metrics.kvMessages}`);
74
+ timingLog(`[TIMING] Heartbeats: ${metrics.heartbeats}`);
75
+ timingLog("[TIMING] ═══════════════════════════════════════════════════════");
76
+ }
77
+ // Cursor API URLs from config
78
+ export const CURSOR_API_URL = config.api.baseUrl;
79
+ export const AGENT_PRIVACY_URL = config.api.agentPrivacyUrl;
80
+ export const AGENT_NON_PRIVACY_URL = config.api.agentNonPrivacyUrl;
81
+ function detectLatestInstalledAgentVersion() {
82
+ try {
83
+ const versionsDir = join(homedir(), ".local", "share", "cursor-agent", "versions");
84
+ const entries = readdirSync(versionsDir, { withFileTypes: true })
85
+ .filter((d) => d.isDirectory())
86
+ .map((d) => d.name)
87
+ .sort();
88
+ const latest = entries.at(-1);
89
+ return latest ? `cli-${latest}` : undefined;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ function resolveClientVersionHeader() {
96
+ return config.api.clientVersion || detectLatestInstalledAgentVersion() || "cli-unknown";
97
+ }
98
+ function parseTrailerMetadata(trailer) {
99
+ const meta = {};
100
+ for (const rawLine of trailer.split(/\r?\n/)) {
101
+ const line = rawLine.trim();
102
+ if (!line)
103
+ continue;
104
+ const idx = line.indexOf(":");
105
+ if (idx === -1)
106
+ continue;
107
+ const key = line.slice(0, idx).trim().toLowerCase();
108
+ const value = line.slice(idx + 1).trim();
109
+ if (!key)
110
+ continue;
111
+ meta[key] = value;
112
+ }
113
+ return meta;
114
+ }
115
+ function decodeGrpcStatusDetailsBin(detailsB64) {
116
+ try {
117
+ const decoded = Buffer.from(detailsB64.trim(), "base64");
118
+ const statusFields = parseProtoFields(decoded);
119
+ let statusMessage;
120
+ const extracted = [];
121
+ const collectStrings = (bytes, depth) => {
122
+ if (depth > 5)
123
+ return;
124
+ if (bytes.length === 0 || bytes.length > 20000)
125
+ return;
126
+ for (const pf of parseProtoFields(bytes)) {
127
+ if (pf.wireType === 2 && pf.value instanceof Uint8Array) {
128
+ const maybeText = new TextDecoder().decode(pf.value).trim();
129
+ if (maybeText.length >= 6 &&
130
+ /[A-Za-z]/.test(maybeText) &&
131
+ !maybeText.includes("\u0000")) {
132
+ extracted.push(maybeText);
133
+ }
134
+ collectStrings(pf.value, depth + 1);
135
+ }
136
+ }
137
+ };
138
+ for (const field of statusFields) {
139
+ // google.rpc.Status.message = field 2
140
+ if (field.fieldNumber === 2 && field.wireType === 2 && field.value instanceof Uint8Array) {
141
+ const text = new TextDecoder().decode(field.value).trim();
142
+ if (text)
143
+ statusMessage = text;
144
+ }
145
+ // google.rpc.Status.details (repeated Any) = field 3
146
+ if (field.fieldNumber === 3 && field.wireType === 2 && field.value instanceof Uint8Array) {
147
+ const anyFields = parseProtoFields(field.value);
148
+ const valueField = anyFields.find((f) => f.fieldNumber === 2 && f.wireType === 2);
149
+ if (!valueField || !(valueField.value instanceof Uint8Array))
150
+ continue;
151
+ // Try to extract human-readable strings from the Any.value payload
152
+ collectStrings(valueField.value, 0);
153
+ }
154
+ }
155
+ const unique = Array.from(new Set(extracted));
156
+ const details = unique.length > 0 ? unique.join(" | ") : undefined;
157
+ if (details && statusMessage)
158
+ return `${statusMessage} | ${details}`;
159
+ return details ?? statusMessage;
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ }
165
+ // --- Types are now imported from ./proto ---
166
+ // ExecRequest types, ToolCallInfo, AgentStreamChunk, AgentServiceOptions, AgentChatRequest
167
+ // are all imported from ./proto/types via the barrel export
168
+ // Local aliases for the buildExecClientMessageWithMcpResult function which needs a slightly different signature
169
+ function buildExecClientMessage(id, execId, result) {
170
+ return buildExecClientMessageWithMcpResult(id, execId, result);
171
+ }
172
+ export class AgentServiceClient {
173
+ baseUrl;
174
+ accessToken;
175
+ workspacePath;
176
+ blobStore;
177
+ privacyMode = true;
178
+ clientVersionHeader = "cli-unknown";
179
+ baseUrlAttempts = null;
180
+ // For tool result submission during streaming
181
+ currentRequestId = null;
182
+ currentAppendSeqno = 0n;
183
+ // For session reuse - track assistant responses stored in KV blobs
184
+ // When Cursor stores model responses in blobs instead of streaming, we need to extract them
185
+ pendingAssistantBlobs = [];
186
+ constructor(accessToken, options = {}) {
187
+ this.accessToken = accessToken;
188
+ this.privacyMode = options.privacyMode ?? config.api.privacyMode;
189
+ this.clientVersionHeader = resolveClientVersionHeader();
190
+ // Default to api2, but allow fallback to agent backends if needed
191
+ this.baseUrl = options.baseUrl ?? CURSOR_API_URL;
192
+ this.workspacePath = options.workspacePath ?? process.cwd();
193
+ this.blobStore = new Map();
194
+ debugLog(`[DEBUG] AgentServiceClient using baseUrl: ${this.baseUrl}, privacyMode=${this.privacyMode}, clientVersion=${this.clientVersionHeader}`);
195
+ }
196
+ getHeaders(requestId) {
197
+ const checksum = generateChecksum(this.accessToken);
198
+ const headers = {
199
+ "authorization": `Bearer ${this.accessToken}`,
200
+ "content-type": "application/grpc-web+proto",
201
+ "user-agent": "connect-es/1.4.0",
202
+ "x-cursor-checksum": checksum,
203
+ "x-cursor-client-version": this.clientVersionHeader,
204
+ "x-cursor-client-type": "cli",
205
+ "x-cursor-timezone": Intl.DateTimeFormat().resolvedOptions().timeZone,
206
+ "x-ghost-mode": this.privacyMode ? "true" : "false",
207
+ // Signal to backend that we can receive SSE text/event-stream responses
208
+ // Without this, server may store responses in KV blobs instead of streaming
209
+ "x-cursor-streaming": "true",
210
+ };
211
+ if (requestId) {
212
+ headers["x-request-id"] = requestId;
213
+ }
214
+ return headers;
215
+ }
216
+ getBaseUrlAttempts() {
217
+ if (this.baseUrlAttempts)
218
+ return this.baseUrlAttempts;
219
+ const orderedCandidates = [this.baseUrl];
220
+ // Cursor can route AgentService/BidiService to agent.api5.cursor.sh, but those
221
+ // backends often require HTTP/2. Bun's fetch currently struggles with that,
222
+ // so only attempt api5 fallbacks when explicitly enabled.
223
+ const allowApi5Fallback = config.api.tryApi5Fallback;
224
+ const isBunRuntime = typeof globalThis.Bun !== "undefined";
225
+ if (allowApi5Fallback && !isBunRuntime) {
226
+ if (this.baseUrl === CURSOR_API_URL) {
227
+ orderedCandidates.push(this.privacyMode ? AGENT_PRIVACY_URL : AGENT_NON_PRIVACY_URL);
228
+ orderedCandidates.push(this.privacyMode ? AGENT_NON_PRIVACY_URL : AGENT_PRIVACY_URL);
229
+ }
230
+ else if (this.baseUrl === AGENT_PRIVACY_URL ||
231
+ this.baseUrl === AGENT_NON_PRIVACY_URL) {
232
+ orderedCandidates.push(CURSOR_API_URL);
233
+ }
234
+ }
235
+ this.baseUrlAttempts = Array.from(new Set(orderedCandidates));
236
+ return this.baseUrlAttempts;
237
+ }
238
+ blobIdToKey(blobId) {
239
+ return Buffer.from(blobId).toString('hex');
240
+ }
241
+ /**
242
+ * Build the AgentClientMessage for a chat request
243
+ */
244
+ buildChatMessage(request) {
245
+ const messageId = randomUUID();
246
+ const conversationId = request.conversationId ?? randomUUID();
247
+ const model = request.model ?? "gpt-4o";
248
+ const mode = request.mode ?? AgentMode.AGENT;
249
+ // Build RequestContext (REQUIRED for agent to work)
250
+ // Include tools in RequestContext.tools (field 7) - CRITICAL for tool calling!
251
+ const requestContext = buildRequestContext(this.workspacePath, request.tools);
252
+ // Build the message hierarchy
253
+ const userMessage = encodeUserMessage(request.message, messageId, mode);
254
+ const userMessageAction = encodeUserMessageAction(userMessage, requestContext);
255
+ const conversationAction = encodeConversationAction(userMessageAction);
256
+ const modelDetails = encodeModelDetails(model);
257
+ // Pass tools to AgentRunRequest (field 4: mcp_tools) and workspace path (for field 6: mcp_file_system_options)
258
+ const agentRunRequest = encodeAgentRunRequest(conversationAction, modelDetails, conversationId, request.tools, this.workspacePath);
259
+ const agentClientMessage = encodeAgentClientMessage(agentRunRequest);
260
+ return agentClientMessage;
261
+ }
262
+ /**
263
+ * Call BidiAppend to send a client message
264
+ */
265
+ async bidiAppend(requestId, appendSeqno, data) {
266
+ const startTime = Date.now();
267
+ const hexData = Buffer.from(data).toString("hex");
268
+ const appendRequest = encodeBidiAppendRequest(hexData, requestId, appendSeqno);
269
+ const envelope = addConnectEnvelope(appendRequest);
270
+ debugLog(`[TIMING] bidiAppend: data=${data.length}bytes, hex=${hexData.length}chars, envelope=${envelope.length}bytes, encode=${Date.now() - startTime}ms`);
271
+ const url = `${this.baseUrl}/aiserver.v1.BidiService/BidiAppend`;
272
+ const fetchStart = Date.now();
273
+ const response = await fetch(url, {
274
+ method: "POST",
275
+ headers: this.getHeaders(requestId),
276
+ body: Buffer.from(envelope),
277
+ });
278
+ debugLog(`[TIMING] bidiAppend fetch took ${Date.now() - fetchStart}ms, status=${response.status}`);
279
+ if (!response.ok) {
280
+ const errorText = await response.text();
281
+ throw new Error(`BidiAppend failed: ${response.status} - ${errorText}`);
282
+ }
283
+ // Read the response body to see if there's any useful information
284
+ const responseBody = await response.arrayBuffer();
285
+ if (responseBody.byteLength > 0) {
286
+ debugLog(`[DEBUG] BidiAppend response: ${responseBody.byteLength} bytes`);
287
+ const bytes = new Uint8Array(responseBody);
288
+ // Parse as gRPC-Web envelope
289
+ if (bytes.length >= 5) {
290
+ const flags = bytes[0] ?? 0;
291
+ const b1 = bytes[1] ?? 0;
292
+ const b2 = bytes[2] ?? 0;
293
+ const b3 = bytes[3] ?? 0;
294
+ const b4 = bytes[4] ?? 0;
295
+ const length = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
296
+ debugLog(`[DEBUG] BidiAppend response: flags=${flags}, length=${length}, totalBytes=${bytes.length}`);
297
+ if (length > 0 && bytes.length >= 5 + length) {
298
+ const payload = bytes.slice(5, 5 + length);
299
+ debugLog(`[DEBUG] BidiAppend payload hex: ${Buffer.from(payload).toString('hex')}`);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ async handleKvMessage(kvMsg, requestId, appendSeqno) {
305
+ if (kvMsg.messageType === 'get_blob_args' && kvMsg.blobId) {
306
+ const key = this.blobIdToKey(kvMsg.blobId);
307
+ const data = this.blobStore.get(key);
308
+ const result = data ? encodeMessageField(1, data) : new Uint8Array(0);
309
+ const kvClientMsg = buildKvClientMessage(kvMsg.id, 'get_blob_result', result);
310
+ const responseMsg = buildAgentClientMessageWithKv(kvClientMsg);
311
+ await this.bidiAppend(requestId, appendSeqno, responseMsg);
312
+ return appendSeqno + 1n;
313
+ }
314
+ if (kvMsg.messageType === 'set_blob_args' && kvMsg.blobId && kvMsg.blobData) {
315
+ const key = this.blobIdToKey(kvMsg.blobId);
316
+ this.blobStore.set(key, kvMsg.blobData);
317
+ const blobAnalysis = analyzeBlobData(kvMsg.blobData);
318
+ debugLog(`[KV-BLOB] SET id=${kvMsg.id}, key=${key.slice(0, 16)}..., size=${kvMsg.blobData.length}b, type=${blobAnalysis.type}`);
319
+ if (blobAnalysis.type === 'json' && blobAnalysis.json) {
320
+ const json = blobAnalysis.json;
321
+ debugLog(`[KV-BLOB] JSON keys: ${Object.keys(json).join(', ')}`);
322
+ if (json.role)
323
+ debugLog(`[KV-BLOB] JSON role: ${json.role}`);
324
+ if (json.content)
325
+ debugLog(`[KV-BLOB] JSON content type: ${typeof json.content}, isArray: ${Array.isArray(json.content)}`);
326
+ }
327
+ const extractedContent = extractAssistantContent(blobAnalysis, key);
328
+ for (const item of extractedContent) {
329
+ debugLog(`[KV-BLOB] ✓ Assistant content found: ${item.content.slice(0, 100)}...`);
330
+ this.pendingAssistantBlobs.push(item);
331
+ }
332
+ const result = new Uint8Array(0);
333
+ const kvClientMsg = buildKvClientMessage(kvMsg.id, 'set_blob_result', result);
334
+ const responseMsg = buildAgentClientMessageWithKv(kvClientMsg);
335
+ await this.bidiAppend(requestId, appendSeqno, responseMsg);
336
+ return appendSeqno + 1n;
337
+ }
338
+ return appendSeqno;
339
+ }
340
+ /**
341
+ * Send a tool result back to the server (for MCP tools only)
342
+ * This must be called during an active chat stream when an exec_request chunk is received
343
+ */
344
+ async sendToolResult(execRequest, result) {
345
+ if (!this.currentRequestId) {
346
+ throw new Error("No active chat stream - cannot send tool result");
347
+ }
348
+ debugLog(`[DEBUG] Sending tool result for exec id: ${execRequest.id}, result: ${result.success ? "success" : "error"}`);
349
+ // Build ExecClientMessage with mcp_result
350
+ const execClientMsg = buildExecClientMessage(execRequest.id, execRequest.execId, result);
351
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
352
+ // Send the result
353
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
354
+ this.currentAppendSeqno++;
355
+ debugLog(`[DEBUG] Tool result sent, new seqno: ${this.currentAppendSeqno}`);
356
+ // Send stream close control message
357
+ const controlMsg = buildExecClientControlMessage(execRequest.id);
358
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
359
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
360
+ this.currentAppendSeqno++;
361
+ debugLog(`[DEBUG] Stream close sent for exec id: ${execRequest.id}`);
362
+ }
363
+ /**
364
+ * Send a shell execution result back to the server
365
+ */
366
+ async sendShellResult(id, execId, command, cwd, stdout, stderr, exitCode, executionTimeMs) {
367
+ if (!this.currentRequestId) {
368
+ throw new Error("No active chat stream - cannot send shell result");
369
+ }
370
+ debugLog(`[DEBUG] Sending shell result for id: ${id}, exitCode: ${exitCode}`);
371
+ const execClientMsg = buildExecClientMessageWithShellResult(id, execId, command, cwd, stdout, stderr, exitCode, executionTimeMs);
372
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
373
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
374
+ this.currentAppendSeqno++;
375
+ // Send stream close control message
376
+ const controlMsg = buildExecClientControlMessage(id);
377
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
378
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
379
+ this.currentAppendSeqno++;
380
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
381
+ }
382
+ /**
383
+ * Send an LS result back to the server
384
+ */
385
+ async sendLsResult(id, execId, filesString) {
386
+ if (!this.currentRequestId) {
387
+ throw new Error("No active chat stream - cannot send ls result");
388
+ }
389
+ debugLog(`[DEBUG] Sending ls result for id: ${id}`);
390
+ const execClientMsg = buildExecClientMessageWithLsResult(id, execId, filesString);
391
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
392
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
393
+ this.currentAppendSeqno++;
394
+ // Send stream close control message
395
+ const controlMsg = buildExecClientControlMessage(id);
396
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
397
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
398
+ this.currentAppendSeqno++;
399
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
400
+ }
401
+ /**
402
+ * Send a request context result back to the server
403
+ */
404
+ async sendRequestContextResult(id, execId) {
405
+ if (!this.currentRequestId) {
406
+ throw new Error("No active chat stream - cannot send request context result");
407
+ }
408
+ debugLog(`[DEBUG] Sending request context result for id: ${id}`);
409
+ const execClientMsg = buildExecClientMessageWithRequestContextResult(id, execId);
410
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
411
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
412
+ this.currentAppendSeqno++;
413
+ // Send stream close control message
414
+ const controlMsg = buildExecClientControlMessage(id);
415
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
416
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
417
+ this.currentAppendSeqno++;
418
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
419
+ }
420
+ /**
421
+ * Send a file read result back to the server
422
+ */
423
+ async sendReadResult(id, execId, content, path, totalLines, fileSize, truncated) {
424
+ if (!this.currentRequestId) {
425
+ throw new Error("No active chat stream - cannot send read result");
426
+ }
427
+ debugLog(`[DEBUG] Sending read result for id: ${id}, path: ${path}, contentLength: ${content.length}`);
428
+ const execClientMsg = buildExecClientMessageWithReadResult(id, execId, content, path, totalLines, fileSize, truncated);
429
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
430
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
431
+ this.currentAppendSeqno++;
432
+ // Send stream close control message
433
+ const controlMsg = buildExecClientControlMessage(id);
434
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
435
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
436
+ this.currentAppendSeqno++;
437
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
438
+ }
439
+ /**
440
+ * Send a grep/glob result back to the server
441
+ */
442
+ async sendGrepResult(id, execId, pattern, path, files) {
443
+ if (!this.currentRequestId) {
444
+ throw new Error("No active chat stream - cannot send grep result");
445
+ }
446
+ debugLog(`[DEBUG] Sending grep result for id: ${id}, pattern: ${pattern}, files: ${files.length}`);
447
+ const execClientMsg = buildExecClientMessageWithGrepResult(id, execId, pattern, path, files);
448
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
449
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
450
+ this.currentAppendSeqno++;
451
+ // Send stream close control message
452
+ const controlMsg = buildExecClientControlMessage(id);
453
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
454
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
455
+ this.currentAppendSeqno++;
456
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
457
+ }
458
+ /**
459
+ * Send a file write result back to the server
460
+ */
461
+ async sendWriteResult(id, execId, result) {
462
+ if (!this.currentRequestId) {
463
+ throw new Error("No active chat stream - cannot send write result");
464
+ }
465
+ debugLog(`[DEBUG] Sending write result for id: ${id}, result: ${result.success ? "success" : "error"}`);
466
+ const execClientMsg = buildExecClientMessageWithWriteResult(id, execId, result);
467
+ const responseMsg = buildAgentClientMessageWithExec(execClientMsg);
468
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, responseMsg);
469
+ this.currentAppendSeqno++;
470
+ // Send stream close control message
471
+ const controlMsg = buildExecClientControlMessage(id);
472
+ const controlResponseMsg = buildAgentClientMessageWithExecControl(controlMsg);
473
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, controlResponseMsg);
474
+ this.currentAppendSeqno++;
475
+ debugLog(`[DEBUG] Stream close sent for exec id: ${id}`);
476
+ }
477
+ /**
478
+ * Send a ResumeAction to signal the server to continue after tool results
479
+ * Based on Cursor CLI analysis: after sending tool results, send ConversationAction with resume_action
480
+ * to tell the server to resume streaming instead of storing responses in KV blobs
481
+ */
482
+ async sendResumeAction() {
483
+ if (!this.currentRequestId) {
484
+ throw new Error("No active chat stream - cannot send resume action");
485
+ }
486
+ debugLog(`[DEBUG] sendResumeAction called: requestId=${this.currentRequestId}, currentSeqno=${String(this.currentAppendSeqno)}`);
487
+ const conversationAction = encodeConversationActionWithResume();
488
+ const agentClientMessage = encodeAgentClientMessageWithConversationAction(conversationAction);
489
+ debugLog("[DEBUG] Sending ResumeAction with bidiAppend...");
490
+ await this.bidiAppend(this.currentRequestId, this.currentAppendSeqno, agentClientMessage);
491
+ this.currentAppendSeqno++;
492
+ debugLog(`[DEBUG] ResumeAction sent successfully, new seqno: ${String(this.currentAppendSeqno)}`);
493
+ }
494
+ /**
495
+ * Send a streaming chat request.
496
+ *
497
+ * Cursor periodically migrates AgentService/BidiService to different backends.
498
+ * If a request fails before any meaningful output, retry against known agent
499
+ * backends before surfacing the error.
500
+ */
501
+ async *chatStream(request) {
502
+ const baseUrlAttempts = this.getBaseUrlAttempts();
503
+ let lastError = null;
504
+ for (let attemptIndex = 0; attemptIndex < baseUrlAttempts.length; attemptIndex++) {
505
+ const baseUrl = baseUrlAttempts[attemptIndex] ?? this.baseUrl;
506
+ this.baseUrl = baseUrl;
507
+ let sawMeaningfulOutput = false;
508
+ let shouldRetry = false;
509
+ for await (const chunk of this.chatStreamOnce(request)) {
510
+ if (chunk.type === "error") {
511
+ lastError = chunk;
512
+ if (!sawMeaningfulOutput && attemptIndex < baseUrlAttempts.length - 1) {
513
+ debugLog(`[DEBUG] chatStream retrying with next baseUrl after error: ${chunk.error}`);
514
+ shouldRetry = true;
515
+ break;
516
+ }
517
+ yield chunk;
518
+ return;
519
+ }
520
+ if (chunk.type === "text" ||
521
+ chunk.type === "exec_request" ||
522
+ chunk.type === "tool_call_started" ||
523
+ chunk.type === "tool_call_completed" ||
524
+ chunk.type === "partial_tool_call" ||
525
+ chunk.type === "interaction_query" ||
526
+ chunk.type === "kv_blob_assistant") {
527
+ sawMeaningfulOutput = true;
528
+ }
529
+ yield chunk;
530
+ }
531
+ if (!shouldRetry)
532
+ return;
533
+ }
534
+ if (lastError) {
535
+ yield lastError;
536
+ }
537
+ else {
538
+ yield { type: "error", error: "Unknown error" };
539
+ }
540
+ }
541
+ async *chatStreamOnce(request) {
542
+ const metrics = createTimingMetrics();
543
+ const requestId = randomUUID();
544
+ const messageBody = this.buildChatMessage(request);
545
+ metrics.messageBuildMs = Date.now() - metrics.requestStart;
546
+ let appendSeqno = 0n;
547
+ // Heartbeats are frequent; be generous to avoid premature turn cuts
548
+ const HEARTBEAT_IDLE_MS_PROGRESS = config.heartbeat.idleAfterProgressMs;
549
+ const HEARTBEAT_MAX_PROGRESS = config.heartbeat.maxAfterProgress;
550
+ const HEARTBEAT_IDLE_MS_NOPROGRESS = config.heartbeat.idleBeforeProgressMs;
551
+ const HEARTBEAT_MAX_NOPROGRESS = config.heartbeat.maxBeforeProgress;
552
+ let lastProgressAt = Date.now();
553
+ let heartbeatSinceProgress = 0;
554
+ let hasProgress = false;
555
+ const markProgress = () => {
556
+ heartbeatSinceProgress = 0;
557
+ lastProgressAt = Date.now();
558
+ hasProgress = true;
559
+ };
560
+ // Store for tool result submission
561
+ this.currentRequestId = requestId;
562
+ this.currentAppendSeqno = 0n;
563
+ // Build BidiRequestId message for RunSSE
564
+ const bidiRequestId = encodeBidiRequestId(requestId);
565
+ const envelope = addConnectEnvelope(bidiRequestId);
566
+ // Start the SSE stream
567
+ const sseUrl = `${this.baseUrl}/agent.v1.AgentService/RunSSE`;
568
+ const controller = new AbortController();
569
+ const timeout = setTimeout(() => controller.abort(), config.network.requestTimeoutMs);
570
+ try {
571
+ const ssePromise = fetch(sseUrl, {
572
+ method: "POST",
573
+ headers: this.getHeaders(requestId),
574
+ body: Buffer.from(envelope),
575
+ signal: controller.signal,
576
+ });
577
+ // Send initial message
578
+ await this.bidiAppend(requestId, appendSeqno++, messageBody);
579
+ metrics.firstBidiAppendMs = Date.now() - metrics.requestStart;
580
+ this.currentAppendSeqno = appendSeqno;
581
+ const sseResponse = await ssePromise;
582
+ metrics.sseConnectionMs = Date.now() - metrics.requestStart;
583
+ debugLog(`[TIMING] Request sent: build=${metrics.messageBuildMs}ms, append=${metrics.firstBidiAppendMs}ms, response=${metrics.sseConnectionMs}ms`);
584
+ if (!sseResponse.ok) {
585
+ clearTimeout(timeout);
586
+ const errorText = await sseResponse.text();
587
+ yield { type: "error", error: `SSE stream failed: ${sseResponse.status} - ${errorText}` };
588
+ return;
589
+ }
590
+ if (!sseResponse.body) {
591
+ clearTimeout(timeout);
592
+ yield { type: "error", error: "No response body from SSE stream" };
593
+ return;
594
+ }
595
+ const reader = sseResponse.body.getReader();
596
+ let buffer = new Uint8Array(0);
597
+ let turnEnded = false;
598
+ let firstContentLogged = false;
599
+ let hasStreamedText = false; // Track if we received any text via streaming
600
+ // Clear any pending assistant blobs from previous requests
601
+ this.pendingAssistantBlobs = [];
602
+ try {
603
+ while (!turnEnded) {
604
+ const { done, value } = await reader.read();
605
+ if (done) {
606
+ yield { type: "done" };
607
+ break;
608
+ }
609
+ if (!firstContentLogged) {
610
+ metrics.firstChunkMs = Date.now() - metrics.requestStart;
611
+ debugLog(`[TIMING] First chunk received in ${metrics.firstChunkMs}ms`);
612
+ firstContentLogged = true;
613
+ }
614
+ // Append to buffer
615
+ const newBuffer = new Uint8Array(buffer.length + value.length);
616
+ newBuffer.set(buffer);
617
+ newBuffer.set(value, buffer.length);
618
+ buffer = newBuffer;
619
+ // Parse frames
620
+ let offset = 0;
621
+ while (offset + 5 <= buffer.length) {
622
+ const flags = buffer[offset] ?? 0;
623
+ const b1 = buffer[offset + 1] ?? 0;
624
+ const b2 = buffer[offset + 2] ?? 0;
625
+ const b3 = buffer[offset + 3] ?? 0;
626
+ const b4 = buffer[offset + 4] ?? 0;
627
+ const length = (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
628
+ if (offset + 5 + length > buffer.length)
629
+ break;
630
+ const frameData = buffer.slice(offset + 5, offset + 5 + length);
631
+ offset += 5 + length;
632
+ // Check for trailer frame
633
+ if ((flags ?? 0) & 0x80) {
634
+ const trailer = new TextDecoder().decode(frameData);
635
+ debugLog(`Received trailer frame: ${trailer.slice(0, 200)}`);
636
+ const meta = parseTrailerMetadata(trailer);
637
+ const grpcStatus = Number(meta["grpc-status"] ?? "0");
638
+ if (grpcStatus !== 0) {
639
+ const grpcMessage = meta["grpc-message"]
640
+ ? decodeURIComponent(meta["grpc-message"])
641
+ : "Unknown gRPC error";
642
+ const detailsBin = meta["grpc-status-details-bin"];
643
+ const decodedDetails = detailsBin
644
+ ? decodeGrpcStatusDetailsBin(detailsBin)
645
+ : undefined;
646
+ const fullError = decodedDetails
647
+ ? `${grpcMessage} (grpc-status ${grpcStatus}): ${decodedDetails}`
648
+ : `${grpcMessage} (grpc-status ${grpcStatus})`;
649
+ console.error("gRPC error:", fullError);
650
+ yield { type: "error", error: fullError };
651
+ }
652
+ continue;
653
+ }
654
+ // Parse AgentServerMessage
655
+ metrics.chunkCount++;
656
+ const serverMsgFields = parseProtoFields(frameData);
657
+ debugLog(`[DEBUG] Server message fields: ${serverMsgFields.map(f => `field${f.fieldNumber}:${f.wireType}`).join(", ")}`);
658
+ for (const field of serverMsgFields) {
659
+ try {
660
+ // field 1 = interaction_update
661
+ if (field.fieldNumber === 1 && field.wireType === 2 && field.value instanceof Uint8Array) {
662
+ debugLog(`[DEBUG] Received interaction_update, length: ${field.value.length}`);
663
+ const parsed = parseInteractionUpdate(field.value);
664
+ // Yield text content
665
+ if (parsed.text) {
666
+ if (metrics.firstTextMs === undefined) {
667
+ metrics.firstTextMs = Date.now() - metrics.requestStart;
668
+ }
669
+ metrics.textChunks++;
670
+ yield { type: "text", content: parsed.text };
671
+ hasStreamedText = true;
672
+ markProgress();
673
+ }
674
+ // Yield thinking content (for reasoning/thinking models)
675
+ if (parsed.thinking) {
676
+ metrics.textChunks++;
677
+ yield { type: "thinking", content: parsed.thinking };
678
+ markProgress();
679
+ }
680
+ // Yield tool call started
681
+ if (parsed.toolCallStarted) {
682
+ if (metrics.firstToolCallMs === undefined) {
683
+ metrics.firstToolCallMs = Date.now() - metrics.requestStart;
684
+ }
685
+ metrics.toolCalls++;
686
+ yield {
687
+ type: "tool_call_started",
688
+ toolCall: {
689
+ callId: parsed.toolCallStarted.callId,
690
+ modelCallId: parsed.toolCallStarted.modelCallId,
691
+ toolType: parsed.toolCallStarted.toolType,
692
+ name: parsed.toolCallStarted.name,
693
+ arguments: parsed.toolCallStarted.arguments,
694
+ },
695
+ };
696
+ markProgress();
697
+ }
698
+ // Yield tool call completed
699
+ if (parsed.toolCallCompleted) {
700
+ yield {
701
+ type: "tool_call_completed",
702
+ toolCall: {
703
+ callId: parsed.toolCallCompleted.callId,
704
+ modelCallId: parsed.toolCallCompleted.modelCallId,
705
+ toolType: parsed.toolCallCompleted.toolType,
706
+ name: parsed.toolCallCompleted.name,
707
+ arguments: parsed.toolCallCompleted.arguments,
708
+ },
709
+ };
710
+ markProgress();
711
+ }
712
+ // Yield partial tool call updates
713
+ if (parsed.partialToolCall) {
714
+ yield {
715
+ type: "partial_tool_call",
716
+ toolCall: {
717
+ callId: parsed.partialToolCall.callId,
718
+ modelCallId: undefined,
719
+ toolType: "partial",
720
+ name: "partial",
721
+ arguments: "",
722
+ },
723
+ partialArgs: parsed.partialToolCall.argsTextDelta,
724
+ };
725
+ markProgress();
726
+ }
727
+ if (parsed.isComplete) {
728
+ metrics.turnEndedMs = Date.now() - metrics.requestStart;
729
+ turnEnded = true;
730
+ }
731
+ // Yield heartbeat events for the server to track
732
+ if (parsed.isHeartbeat) {
733
+ metrics.heartbeats++;
734
+ heartbeatSinceProgress++;
735
+ const idleMs = Date.now() - lastProgressAt;
736
+ const idleLimit = hasProgress ? HEARTBEAT_IDLE_MS_PROGRESS : HEARTBEAT_IDLE_MS_NOPROGRESS;
737
+ const beatLimit = hasProgress ? HEARTBEAT_MAX_PROGRESS : HEARTBEAT_MAX_NOPROGRESS;
738
+ if (heartbeatSinceProgress >= beatLimit || idleMs >= idleLimit) {
739
+ console.warn(`[DEBUG] Heartbeat idle for ${idleMs}ms (${heartbeatSinceProgress} beats) - closing stream`);
740
+ turnEnded = true;
741
+ }
742
+ else {
743
+ yield { type: "heartbeat" };
744
+ }
745
+ }
746
+ }
747
+ // field 3 = conversation_checkpoint_update (completion signal)
748
+ // NOTE: Checkpoint does NOT mean we're done! exec_server_message can come AFTER checkpoint.
749
+ // Only end on turn_ended (field 14 in interaction_update) or stream close.
750
+ if (field.fieldNumber === 3 && field.wireType === 2 && field.value instanceof Uint8Array) {
751
+ debugLog(`[DEBUG] Received checkpoint, data length: ${field.value.length}`);
752
+ // Try to parse checkpoint to see what it contains
753
+ const checkpointFields = parseProtoFields(field.value);
754
+ debugLog(`[DEBUG] Checkpoint fields: ${checkpointFields.map(f => `field${f.fieldNumber}:${f.wireType}`).join(", ")}`);
755
+ for (const cf of checkpointFields) {
756
+ if (cf.wireType === 2 && cf.value instanceof Uint8Array) {
757
+ try {
758
+ const text = new TextDecoder().decode(cf.value);
759
+ if (text.length < 200) {
760
+ debugLog(`[DEBUG] Checkpoint field ${cf.fieldNumber}: ${text}`);
761
+ }
762
+ }
763
+ catch { }
764
+ }
765
+ }
766
+ yield { type: "checkpoint" };
767
+ markProgress();
768
+ // DO NOT set turnEnded here - exec messages may follow!
769
+ }
770
+ // field 2 = exec_server_message (tool execution request)
771
+ if (field.fieldNumber === 2 && field.wireType === 2 && field.value instanceof Uint8Array) {
772
+ debugLog(`[DEBUG] Received exec_server_message (field 2), length: ${field.value.length}`);
773
+ // Parse the ExecServerMessage
774
+ const execRequest = parseExecServerMessage(field.value);
775
+ if (execRequest) {
776
+ // Log based on type
777
+ if (execRequest.type === 'mcp') {
778
+ debugLog(`[DEBUG] Parsed MCP exec request: id=${execRequest.id}, name=${execRequest.name}, toolName=${execRequest.toolName}, providerIdentifier=${execRequest.providerIdentifier}, toolCallId=${execRequest.toolCallId}`);
779
+ }
780
+ else {
781
+ debugLog(`[DEBUG] Parsed ${execRequest.type} exec request: id=${execRequest.id}`);
782
+ }
783
+ // Yield exec_request chunk for the server to handle
784
+ metrics.execRequests++;
785
+ yield {
786
+ type: "exec_request",
787
+ execRequest,
788
+ };
789
+ markProgress();
790
+ }
791
+ else {
792
+ // Log other exec types we don't handle yet
793
+ const execFields = parseProtoFields(field.value);
794
+ debugLog(`[DEBUG] exec_server_message fields (unhandled): ${execFields.map(f => `field${f.fieldNumber}`).join(", ")}`);
795
+ }
796
+ }
797
+ // field 4 = kv_server_message
798
+ if (field.fieldNumber === 4 && field.wireType === 2 && field.value instanceof Uint8Array) {
799
+ metrics.kvMessages++;
800
+ const kvMsg = parseKvServerMessage(field.value);
801
+ debugLog(`[DEBUG] KV message: id=${kvMsg.id}, type=${kvMsg.messageType}, blobId=${kvMsg.blobId ? Buffer.from(kvMsg.blobId).toString('hex').slice(0, 20) : 'none'}...`);
802
+ appendSeqno = await this.handleKvMessage(kvMsg, requestId, appendSeqno);
803
+ this.currentAppendSeqno = appendSeqno;
804
+ }
805
+ // field 5 = exec_server_control_message (abort signal from server)
806
+ if (field.fieldNumber === 5 && field.wireType === 2 && field.value instanceof Uint8Array) {
807
+ debugLog("[DEBUG] Received exec_server_control_message (field 5)!");
808
+ const controlFields = parseProtoFields(field.value);
809
+ debugLog(`[DEBUG] exec_server_control_message fields: ${controlFields.map(f => `field${f.fieldNumber}:${f.wireType}`).join(", ")}`);
810
+ // ExecServerControlMessage has field 1 = abort (ExecServerAbort)
811
+ for (const cf of controlFields) {
812
+ if (cf.fieldNumber === 1 && cf.wireType === 2 && cf.value instanceof Uint8Array) {
813
+ debugLog("[DEBUG] Server sent abort signal!");
814
+ // Parse ExecServerAbort - it has field 1 = id (string)
815
+ const abortFields = parseProtoFields(cf.value);
816
+ for (const af of abortFields) {
817
+ if (af.fieldNumber === 1 && af.wireType === 2 && af.value instanceof Uint8Array) {
818
+ const abortId = new TextDecoder().decode(af.value);
819
+ debugLog(`[DEBUG] Abort id: ${abortId}`);
820
+ }
821
+ }
822
+ yield { type: "exec_server_abort" };
823
+ }
824
+ }
825
+ markProgress();
826
+ }
827
+ // field 7 = interaction_query (server asking for user approval/input)
828
+ if (field.fieldNumber === 7 && field.wireType === 2 && field.value instanceof Uint8Array) {
829
+ debugLog("[DEBUG] Received interaction_query (field 7)!");
830
+ const queryFields = parseProtoFields(field.value);
831
+ debugLog(`[DEBUG] interaction_query fields: ${queryFields.map(f => `field${f.fieldNumber}:${f.wireType}`).join(", ")}`);
832
+ // InteractionQuery structure:
833
+ // field 1 = id (uint32)
834
+ // field 2 = web_search_request_query (oneof)
835
+ // field 3 = ask_question_interaction_query (oneof)
836
+ // field 4 = switch_mode_request_query (oneof)
837
+ // field 5 = exa_search_request_query (oneof)
838
+ // field 6 = exa_fetch_request_query (oneof)
839
+ let queryId = 0;
840
+ let queryType = 'unknown';
841
+ for (const qf of queryFields) {
842
+ if (qf.fieldNumber === 1 && qf.wireType === 0) {
843
+ queryId = Number(qf.value);
844
+ }
845
+ else if (qf.fieldNumber === 2 && qf.wireType === 2) {
846
+ queryType = 'web_search';
847
+ }
848
+ else if (qf.fieldNumber === 3 && qf.wireType === 2) {
849
+ queryType = 'ask_question';
850
+ }
851
+ else if (qf.fieldNumber === 4 && qf.wireType === 2) {
852
+ queryType = 'switch_mode';
853
+ }
854
+ else if (qf.fieldNumber === 5 && qf.wireType === 2) {
855
+ queryType = 'exa_search';
856
+ }
857
+ else if (qf.fieldNumber === 6 && qf.wireType === 2) {
858
+ queryType = 'exa_fetch';
859
+ }
860
+ }
861
+ debugLog(`[DEBUG] InteractionQuery: id=${queryId}, type=${queryType}`);
862
+ // Yield the interaction query for the server to handle
863
+ yield {
864
+ type: "interaction_query",
865
+ queryId,
866
+ queryType,
867
+ };
868
+ markProgress();
869
+ }
870
+ }
871
+ catch (parseErr) {
872
+ const error = parseErr instanceof Error ? parseErr : new Error(String(parseErr));
873
+ console.error("Error parsing field:", field.fieldNumber, error);
874
+ yield { type: "error", error: `Parse error in field ${field.fieldNumber}: ${error.message}` };
875
+ }
876
+ }
877
+ if (turnEnded) {
878
+ break;
879
+ }
880
+ }
881
+ buffer = buffer.slice(offset);
882
+ }
883
+ // Clean exit - check for KV blob assistant responses if no text was streamed
884
+ if (turnEnded) {
885
+ controller.abort(); // Clean up the connection
886
+ logTimingMetrics(metrics);
887
+ // Session reuse: If no text was streamed but we have pending assistant blobs,
888
+ // emit them as kv_blob_assistant chunks so the server can use the content
889
+ if (!hasStreamedText && this.pendingAssistantBlobs.length > 0) {
890
+ debugLog(`[DEBUG] No streamed text but found ${this.pendingAssistantBlobs.length} assistant blob(s) - emitting`);
891
+ for (const blob of this.pendingAssistantBlobs) {
892
+ yield { type: "kv_blob_assistant", blobContent: blob.content };
893
+ }
894
+ }
895
+ yield { type: "done" };
896
+ }
897
+ }
898
+ finally {
899
+ reader.releaseLock();
900
+ clearTimeout(timeout);
901
+ this.currentRequestId = null;
902
+ }
903
+ }
904
+ catch (err) {
905
+ clearTimeout(timeout);
906
+ this.currentRequestId = null;
907
+ const error = err;
908
+ if (error.name === 'AbortError') {
909
+ // Normal termination after turn ended
910
+ return;
911
+ }
912
+ console.error("Agent stream error:", error.name, error.message, err.stack);
913
+ yield { type: "error", error: error.message || String(err) };
914
+ }
915
+ }
916
+ /**
917
+ * Send a non-streaming chat request (collects all chunks)
918
+ */
919
+ async chat(request) {
920
+ let result = "";
921
+ for await (const chunk of this.chatStream(request)) {
922
+ if (chunk.type === "error") {
923
+ throw new Error(chunk.error ?? "Unknown error");
924
+ }
925
+ if (chunk.type === "text" && chunk.content) {
926
+ result += chunk.content;
927
+ }
928
+ }
929
+ return result;
930
+ }
931
+ }
932
+ /**
933
+ * Create an Agent Service client
934
+ */
935
+ export function createAgentServiceClient(accessToken, options) {
936
+ return new AgentServiceClient(accessToken, options);
937
+ }
938
+ //# sourceMappingURL=agent-service.js.map