@rama_nigg/open-cursor 2.3.19 → 2.4.0

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.
package/src/plugin.ts CHANGED
@@ -10,7 +10,12 @@ import { startCursorOAuth } from "./auth";
10
10
  import { LineBuffer } from "./streaming/line-buffer.js";
11
11
  import { StreamToSseConverter, formatSseDone } from "./streaming/openai-sse.js";
12
12
  import { parseStreamJsonLine } from "./streaming/parser.js";
13
- import { extractText, extractThinking, isAssistantText, isThinking } from "./streaming/types.js";
13
+ import { extractText, extractThinking, isAssistantText, isResult, isThinking } from "./streaming/types.js";
14
+ import {
15
+ createChatCompletionUsageChunk,
16
+ extractOpenAiUsageFromResult,
17
+ type OpenAiUsage,
18
+ } from "./usage.js";
14
19
  import { createLogger } from "./utils/logger";
15
20
  import { RequestPerf } from "./utils/perf";
16
21
  import { parseAgentError, formatErrorForUser, stripAnsi } from "./utils/errors";
@@ -25,7 +30,7 @@ import { ToolRouter } from "./tools/router.js";
25
30
  import { SkillLoader } from "./tools/skills/loader.js";
26
31
  import { SkillResolver } from "./tools/skills/resolver.js";
27
32
  import { autoRefreshModels } from "./models/sync.js";
28
- import { readMcpConfigs } from "./mcp/config.js";
33
+ import { readMcpConfigs, readSubagentNames } from "./mcp/config.js";
29
34
  import { McpClientManager } from "./mcp/client-manager.js";
30
35
  import { buildMcpToolHookEntries, buildMcpToolDefinitions } from "./mcp/tool-bridge.js";
31
36
  import { createOpencodeClient } from "@opencode-ai/sdk";
@@ -52,6 +57,7 @@ import {
52
57
  parseToolLoopMaxRepeat,
53
58
  type ToolLoopGuard,
54
59
  } from "./provider/tool-loop-guard.js";
60
+ import { resolveCursorAgentBinary } from "./utils/binary.js";
55
61
 
56
62
  const log = createLogger("plugin");
57
63
 
@@ -92,6 +98,7 @@ export function buildAvailableToolsSystemMessage(
92
98
  lastToolMap: Array<{ id: string; name: string }>,
93
99
  mcpToolDefs: any[],
94
100
  mcpToolSummaries?: McpToolSummary[],
101
+ subagentNames: string[] = [],
95
102
  ): string | null {
96
103
  const parts: string[] = [];
97
104
 
@@ -132,6 +139,12 @@ export function buildAvailableToolsSystemMessage(
132
139
  parts.push(lines.join("\n"));
133
140
  }
134
141
 
142
+ if (subagentNames.length > 0) {
143
+ parts.push(
144
+ `When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
145
+ );
146
+ }
147
+
135
148
  return parts.length > 0 ? parts.join("\n\n") : null;
136
149
  }
137
150
 
@@ -184,7 +197,7 @@ function canonicalizePathForCompare(pathValue: string): string {
184
197
  normalizedPath = resolvedPath;
185
198
  }
186
199
 
187
- if (process.platform === "darwin") {
200
+ if (process.platform === "darwin" || process.platform === "win32") {
188
201
  return normalizedPath.toLowerCase();
189
202
  }
190
203
 
@@ -212,37 +225,76 @@ function isNonConfigPath(pathValue: string): boolean {
212
225
  return !isWithinPath(getOpenCodeConfigPrefix(), pathValue);
213
226
  }
214
227
 
215
- const SESSION_WORKSPACE_CACHE_LIMIT = 200;
216
-
217
- function resolveWorkspaceDirectory(worktree: string | undefined, directory: string | undefined): string {
218
- const envWorkspace = process.env.CURSOR_ACP_WORKSPACE?.trim();
219
- if (envWorkspace) {
220
- return resolve(envWorkspace);
228
+ // Filesystem roots are never a meaningful workspace: accepting "/" (or a bare
229
+ // Windows drive root like "C:\") makes every tool treat the whole machine as
230
+ // the project, which is both unsafe and a common symptom of a daemon that
231
+ // was launched without a real cwd (e.g. systemd unit without WorkingDirectory).
232
+ export function isRootPath(pathValue: string): boolean {
233
+ if (!pathValue) {
234
+ return false;
235
+ }
236
+ const resolved = resolve(pathValue);
237
+ if (resolved === "/") {
238
+ return true;
221
239
  }
240
+ return /^[A-Za-z]:[\\/]?$/.test(resolved);
241
+ }
222
242
 
223
- const envProjectDir = process.env.OPENCODE_CURSOR_PROJECT_DIR?.trim();
224
- if (envProjectDir) {
225
- return resolve(envProjectDir);
243
+ function isAcceptableWorkspace(pathValue: string, configPrefix: string): boolean {
244
+ if (!pathValue) {
245
+ return false;
246
+ }
247
+ if (isRootPath(pathValue)) {
248
+ return false;
226
249
  }
250
+ if (isWithinPath(configPrefix, pathValue)) {
251
+ return false;
252
+ }
253
+ return true;
254
+ }
227
255
 
256
+ const SESSION_WORKSPACE_CACHE_LIMIT = 200;
257
+
258
+ export function resolveWorkspaceDirectory(
259
+ worktree: string | undefined,
260
+ directory: string | undefined,
261
+ ): string {
228
262
  const configPrefix = getOpenCodeConfigPrefix();
229
263
 
264
+ const envWorkspace = resolveCandidate(process.env.CURSOR_ACP_WORKSPACE);
265
+ if (envWorkspace && !isRootPath(envWorkspace)) {
266
+ return envWorkspace;
267
+ }
268
+
269
+ const envProjectDir = resolveCandidate(process.env.OPENCODE_CURSOR_PROJECT_DIR);
270
+ if (envProjectDir && !isRootPath(envProjectDir)) {
271
+ return envProjectDir;
272
+ }
273
+
230
274
  const worktreeCandidate = resolveCandidate(worktree);
231
- if (worktreeCandidate && !isWithinPath(configPrefix, worktreeCandidate)) {
275
+ if (isAcceptableWorkspace(worktreeCandidate, configPrefix)) {
232
276
  return worktreeCandidate;
233
277
  }
234
278
 
235
279
  const dirCandidate = resolveCandidate(directory);
236
- if (dirCandidate && !isWithinPath(configPrefix, dirCandidate)) {
280
+ if (isAcceptableWorkspace(dirCandidate, configPrefix)) {
237
281
  return dirCandidate;
238
282
  }
239
283
 
240
284
  const cwd = resolve(process.cwd());
241
- if (cwd && !isWithinPath(configPrefix, cwd)) {
285
+ if (isAcceptableWorkspace(cwd, configPrefix)) {
242
286
  return cwd;
243
287
  }
244
288
 
245
- return dirCandidate || cwd || configPrefix;
289
+ // Fall back to the user's home directory rather than "/" when every other
290
+ // signal is unusable. $HOME is always writable for the current user and
291
+ // keeps tool scopes sane even when the daemon was spawned from root.
292
+ const home = resolveCandidate(homedir());
293
+ if (home && !isRootPath(home)) {
294
+ return home;
295
+ }
296
+
297
+ return configPrefix;
246
298
  }
247
299
 
248
300
  type ProxyRuntimeState = {
@@ -251,7 +303,11 @@ type ProxyRuntimeState = {
251
303
  };
252
304
 
253
305
  export function normalizeWorkspaceForCompare(pathValue: string): string {
254
- return resolve(pathValue);
306
+ const resolved = resolve(pathValue);
307
+ if (process.platform === "darwin" || process.platform === "win32") {
308
+ return resolved.toLowerCase();
309
+ }
310
+ return resolved;
255
311
  }
256
312
 
257
313
  export function isReusableProxyHealthPayload(payload: any, workspaceDirectory: string): boolean {
@@ -314,7 +370,12 @@ export function resolveChatParamTools(
314
370
  return PROVIDER_BOUNDARY.resolveChatParamTools(mode, existingTools, refreshedTools);
315
371
  }
316
372
 
317
- function createChatCompletionResponse(model: string, content: string, reasoningContent?: string) {
373
+ function createChatCompletionResponse(
374
+ model: string,
375
+ content: string,
376
+ reasoningContent?: string,
377
+ usage?: OpenAiUsage,
378
+ ) {
318
379
  const message: { role: "assistant"; content: string; reasoning_content?: string } = {
319
380
  role: "assistant",
320
381
  content,
@@ -324,7 +385,18 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
324
385
  message.reasoning_content = reasoningContent;
325
386
  }
326
387
 
327
- return {
388
+ const response: {
389
+ id: string;
390
+ object: string;
391
+ created: number;
392
+ model: string;
393
+ choices: Array<{
394
+ index: number;
395
+ message: typeof message;
396
+ finish_reason: string;
397
+ }>;
398
+ usage?: OpenAiUsage;
399
+ } = {
328
400
  id: `cursor-acp-${Date.now()}`,
329
401
  object: "chat.completion",
330
402
  created: Math.floor(Date.now() / 1000),
@@ -337,6 +409,12 @@ function createChatCompletionResponse(model: string, content: string, reasoningC
337
409
  },
338
410
  ],
339
411
  };
412
+
413
+ if (usage) {
414
+ response.usage = usage;
415
+ }
416
+
417
+ return response;
340
418
  }
341
419
 
342
420
  function createChatCompletionChunk(id: string, created: number, model: string, deltaContent: string, done = false) {
@@ -355,11 +433,17 @@ function createChatCompletionChunk(id: string, created: number, model: string, d
355
433
  };
356
434
  }
357
435
 
358
- function extractCompletionFromStream(output: string): { assistantText: string; reasoningText: string } {
436
+ export function extractCompletionFromStream(output: string): {
437
+ assistantText: string;
438
+ reasoningText: string;
439
+ usage?: OpenAiUsage;
440
+ } {
359
441
  const lines = output.split("\n");
360
442
  let assistantText = "";
361
443
  let reasoningText = "";
444
+ let usage: OpenAiUsage | undefined;
362
445
  let sawAssistantPartials = false;
446
+ let sawThinkingPartials = false;
363
447
 
364
448
  for (const line of lines) {
365
449
  const event = parseStreamJsonLine(line);
@@ -383,12 +467,22 @@ function extractCompletionFromStream(output: string): { assistantText: string; r
383
467
  if (isThinking(event)) {
384
468
  const thinking = extractThinking(event);
385
469
  if (thinking) {
386
- reasoningText += thinking;
470
+ const isPartial = typeof (event as any).timestamp_ms === "number";
471
+ if (isPartial) {
472
+ reasoningText += thinking;
473
+ sawThinkingPartials = true;
474
+ } else if (!sawThinkingPartials) {
475
+ reasoningText = thinking;
476
+ }
387
477
  }
388
478
  }
479
+
480
+ if (isResult(event)) {
481
+ usage = extractOpenAiUsageFromResult(event) ?? usage;
482
+ }
389
483
  }
390
484
 
391
- return { assistantText, reasoningText };
485
+ return { assistantText, reasoningText, usage };
392
486
  }
393
487
 
394
488
  function formatToolUpdateEvent(update: ToolUpdate): string {
@@ -563,7 +657,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
563
657
  if (url.pathname === "/v1/models" || url.pathname === "/models") {
564
658
  try {
565
659
  const bunAny = globalThis as any;
566
- const proc = bunAny.Bun.spawn(["cursor-agent", "models"], {
660
+ const proc = bunAny.Bun.spawn([resolveCursorAgentBinary(), "models"], {
567
661
  stdout: "pipe",
568
662
  stderr: "pipe",
569
663
  });
@@ -614,6 +708,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
614
708
  // DEBUG: Log raw request structure for tool-loop investigation
615
709
  debugLogToFile("raw_request_body", {
616
710
  model: body?.model,
711
+ cursorModel: body?.cursorModel,
617
712
  stream,
618
713
  toolCount: tools.length,
619
714
  toolNames: tools.map((t: any) => t?.function?.name ?? t?.name ?? "unknown"),
@@ -628,9 +723,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
628
723
  const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
629
724
  const boundaryContext = createBoundaryRuntimeContext("bun-handler");
630
725
 
631
- const prompt = buildPromptFromMessages(messages, tools);
632
- const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
633
- boundary.normalizeRuntimeModel(body?.model),
726
+ const subagentNames = readSubagentNames();
727
+ const prompt = buildPromptFromMessages(messages, tools, subagentNames);
728
+ const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
729
+ boundary.resolveRuntimeModel(body?.model, body?.cursorModel),
634
730
  );
635
731
  const msgSummaryBun = messages.map((m: any, i: number) => {
636
732
  const role = m?.role ?? "?";
@@ -656,7 +752,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
656
752
  }
657
753
 
658
754
  const cmd = [
659
- "cursor-agent",
755
+ resolveCursorAgentBinary(),
660
756
  "--print",
661
757
  "--output-format",
662
758
  "stream-json",
@@ -757,6 +853,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
757
853
  model,
758
854
  completion.assistantText || stdout || stderr,
759
855
  completion.reasoningText || undefined,
856
+ completion.usage,
760
857
  );
761
858
  return new Response(JSON.stringify(payload), {
762
859
  status: 200,
@@ -778,6 +875,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
778
875
  async start(controller) {
779
876
  let streamTerminated = false;
780
877
  let firstTokenReceived = false;
878
+ let usage: OpenAiUsage | undefined;
781
879
  try {
782
880
  const reader = (child.stdout as ReadableStream<Uint8Array>).getReader();
783
881
  const converter = new StreamToSseConverter(model, { id, created });
@@ -832,6 +930,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
832
930
  continue;
833
931
  }
834
932
 
933
+ if (isResult(event)) {
934
+ usage = extractOpenAiUsageFromResult(event) ?? usage;
935
+ }
936
+
835
937
  if (event.type === "tool_call") {
836
938
  perf.mark("tool-call");
837
939
  const result = await handleToolLoopEventWithFallback({
@@ -898,6 +1000,9 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
898
1000
  if (!event) {
899
1001
  continue;
900
1002
  }
1003
+ if (isResult(event)) {
1004
+ usage = extractOpenAiUsageFromResult(event) ?? usage;
1005
+ }
901
1006
  if (event.type === "tool_call") {
902
1007
  const result = await handleToolLoopEventWithFallback({
903
1008
  event: event as any,
@@ -986,6 +1091,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
986
1091
 
987
1092
  const doneChunk = createChatCompletionChunk(id, created, model, "", true);
988
1093
  controller.enqueue(encoder.encode(`data: ${JSON.stringify(doneChunk)}\n\n`));
1094
+ if (usage) {
1095
+ const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
1096
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(usageChunk)}\n\n`));
1097
+ }
989
1098
  controller.enqueue(encoder.encode(formatSseDone()));
990
1099
  } finally {
991
1100
  perf.mark("request:done");
@@ -1046,8 +1155,8 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1046
1155
  // Dynamic model discovery via cursor-agent models (Node.js handler)
1047
1156
  if (url.pathname === "/v1/models" || url.pathname === "/models") {
1048
1157
  try {
1049
- const { execSync } = await import("child_process");
1050
- const output = execSync("cursor-agent models", { encoding: "utf-8", timeout: 30000 });
1158
+ const { execFileSync } = await import("child_process");
1159
+ const output = execFileSync(resolveCursorAgentBinary(), ["models"], { encoding: "utf-8", timeout: 30000 });
1051
1160
  const clean = stripAnsi(output);
1052
1161
  const models: Array<{ id: string; object: string; created: number; owned_by: string }> = [];
1053
1162
  for (const line of clean.split("\n")) {
@@ -1092,9 +1201,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1092
1201
  const toolLoopGuard = createToolLoopGuard(messages, TOOL_LOOP_MAX_REPEAT);
1093
1202
  const boundaryContext = createBoundaryRuntimeContext("node-handler");
1094
1203
 
1095
- const prompt = buildPromptFromMessages(messages, tools);
1096
- const model = boundaryContext.run("normalizeRuntimeModel", (boundary) =>
1097
- boundary.normalizeRuntimeModel(bodyData?.model),
1204
+ const subagentNames = readSubagentNames();
1205
+ const prompt = buildPromptFromMessages(messages, tools, subagentNames);
1206
+ const model = boundaryContext.run("resolveRuntimeModel", (boundary) =>
1207
+ boundary.resolveRuntimeModel(bodyData?.model, bodyData?.cursorModel),
1098
1208
  );
1099
1209
  const msgSummary = messages.map((m: any, i: number) => {
1100
1210
  const role = m?.role ?? "?";
@@ -1114,7 +1224,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1114
1224
  });
1115
1225
 
1116
1226
  const cmd = [
1117
- "cursor-agent",
1227
+ resolveCursorAgentBinary(),
1118
1228
  "--print",
1119
1229
  "--output-format",
1120
1230
  "stream-json",
@@ -1128,7 +1238,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1128
1238
  cmd.push("--force");
1129
1239
  }
1130
1240
 
1131
- const child = spawn(cmd[0], cmd.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
1241
+ const child = spawn(cmd[0], cmd.slice(1), {
1242
+ stdio: ["pipe", "pipe", "pipe"],
1243
+ shell: process.platform === "win32",
1244
+ });
1132
1245
 
1133
1246
  // Write prompt to stdin to avoid E2BIG error
1134
1247
  child.stdin.write(prompt);
@@ -1216,6 +1329,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1216
1329
  model,
1217
1330
  completion.assistantText || stdout || stderr,
1218
1331
  completion.reasoningText || undefined,
1332
+ completion.usage,
1219
1333
  );
1220
1334
 
1221
1335
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -1242,6 +1356,7 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1242
1356
  const stderrChunks: Buffer[] = [];
1243
1357
  let streamTerminated = false;
1244
1358
  let firstTokenReceived = false;
1359
+ let usage: OpenAiUsage | undefined;
1245
1360
  child.stderr.on("data", (chunk) => {
1246
1361
  stderrChunks.push(Buffer.from(chunk));
1247
1362
  });
@@ -1314,6 +1429,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1314
1429
  continue;
1315
1430
  }
1316
1431
 
1432
+ if (isResult(event)) {
1433
+ usage = extractOpenAiUsageFromResult(event) ?? usage;
1434
+ }
1435
+
1317
1436
  if (event.type === "tool_call") {
1318
1437
  perf.mark("tool-call");
1319
1438
  const result = await handleToolLoopEventWithFallback({
@@ -1386,6 +1505,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1386
1505
  continue;
1387
1506
  }
1388
1507
 
1508
+ if (isResult(event)) {
1509
+ usage = extractOpenAiUsageFromResult(event) ?? usage;
1510
+ }
1511
+
1389
1512
  if (event.type === "tool_call") {
1390
1513
  const result = await handleToolLoopEventWithFallback({
1391
1514
  event: event as any,
@@ -1490,6 +1613,10 @@ async function ensureCursorProxyServer(workspaceDirectory: string, toolRouter?:
1490
1613
  ],
1491
1614
  };
1492
1615
  res.write(`data: ${JSON.stringify(doneChunk)}\n\n`);
1616
+ if (usage) {
1617
+ const usageChunk = createChatCompletionUsageChunk(id, created, model, usage);
1618
+ res.write(`data: ${JSON.stringify(usageChunk)}\n\n`);
1619
+ }
1493
1620
  res.write(formatSseDone());
1494
1621
  res.end();
1495
1622
  });
@@ -1585,7 +1712,7 @@ function jsonSchemaToZod(jsonSchema: any): any {
1585
1712
  }
1586
1713
  break;
1587
1714
  case "object":
1588
- zodType = z.record(z.any());
1715
+ zodType = z.record(z.string(), z.any());
1589
1716
  if (p.description) {
1590
1717
  zodType = zodType.describe(p.description);
1591
1718
  }
@@ -2058,7 +2185,11 @@ export const CursorPlugin: Plugin = async ({ $, directory, worktree, client, ser
2058
2185
 
2059
2186
  async "experimental.chat.system.transform"(input: any, output: { system: string[] }) {
2060
2187
  if (!toolsEnabled) return;
2061
- const systemMessage = buildAvailableToolsSystemMessage(lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries);
2188
+ const subagentNames = readSubagentNames();
2189
+ const systemMessage = buildAvailableToolsSystemMessage(
2190
+ lastToolNames, lastToolMap, mcpToolDefs, mcpToolSummaries,
2191
+ subagentNames,
2192
+ );
2062
2193
  if (!systemMessage) return;
2063
2194
  output.system = output.system || [];
2064
2195
  output.system.push(systemMessage);
@@ -36,6 +36,7 @@ export interface ProviderBoundary {
36
36
  ): ToolLoopFlags;
37
37
  matchesProvider(inputModel: any): boolean;
38
38
  normalizeRuntimeModel(model: unknown): string;
39
+ resolveRuntimeModel(model: unknown, cursorModel: unknown): string;
39
40
  applyChatParamDefaults(
40
41
  output: any,
41
42
  proxyBaseURL: string | undefined,
@@ -137,6 +138,15 @@ function createSharedBoundary(
137
138
  return raw;
138
139
  },
139
140
 
141
+ resolveRuntimeModel(model, cursorModel) {
142
+ const rawCursorModel = typeof cursorModel === "string" ? cursorModel.trim() : "";
143
+ if (rawCursorModel.length > 0) {
144
+ return this.normalizeRuntimeModel(rawCursorModel);
145
+ }
146
+
147
+ return this.normalizeRuntimeModel(model);
148
+ },
149
+
140
150
  applyChatParamDefaults(output, proxyBaseURL, defaultBaseURL, defaultApiKey) {
141
151
  output.options = output.options || {};
142
152
  output.options.baseURL = proxyBaseURL || defaultBaseURL;
@@ -44,6 +44,7 @@ const EXPLORATION_TOOLS = new Set([
44
44
  "bash",
45
45
  "shell",
46
46
  "webfetch",
47
+ "task",
47
48
  ]);
48
49
 
49
50
  export interface ToolLoopGuardDecision {
@@ -511,16 +512,20 @@ function evaluateWithFingerprints(
511
512
  };
512
513
  }
513
514
 
515
+ const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
516
+ const effectiveMaxRepeat = isExplorationTool
517
+ ? maxRepeat * EXPLORATION_LIMIT_MULTIPLIER
518
+ : maxRepeat;
519
+
514
520
  const strictRepeatCount = (strictCounts.get(strictFingerprint) ?? 0) + 1;
515
521
  strictCounts.set(strictFingerprint, strictRepeatCount);
516
- const strictTriggered = strictRepeatCount > maxRepeat;
522
+ const strictTriggered = strictRepeatCount > effectiveMaxRepeat;
517
523
 
518
- const isExplorationTool = EXPLORATION_TOOLS.has(toolName.toLowerCase());
519
524
  if (isExplorationTool) {
520
525
  return {
521
526
  fingerprint: strictFingerprint,
522
527
  repeatCount: strictRepeatCount,
523
- maxRepeat,
528
+ maxRepeat: effectiveMaxRepeat,
524
529
  errorClass,
525
530
  triggered: strictTriggered,
526
531
  tracked: true,
@@ -1,5 +1,22 @@
1
- export function createChatCompletionResponse(model: string, content: string) {
2
- return {
1
+ import type { OpenAiUsage } from "../usage.js";
2
+
3
+ export function createChatCompletionResponse(
4
+ model: string,
5
+ content: string,
6
+ usage?: OpenAiUsage,
7
+ ) {
8
+ const response: {
9
+ id: string;
10
+ object: string;
11
+ created: number;
12
+ model: string;
13
+ choices: Array<{
14
+ index: number;
15
+ message: { role: string; content: string };
16
+ finish_reason: string;
17
+ }>;
18
+ usage?: OpenAiUsage;
19
+ } = {
3
20
  id: `cursor-acp-${Date.now()}`,
4
21
  object: "chat.completion",
5
22
  created: Math.floor(Date.now() / 1000),
@@ -8,15 +25,16 @@ export function createChatCompletionResponse(model: string, content: string) {
8
25
  {
9
26
  index: 0,
10
27
  message: { role: "assistant", content },
11
- finish_reason: "stop"
28
+ finish_reason: "stop",
12
29
  }
13
30
  ],
14
- usage: {
15
- prompt_tokens: 0,
16
- completion_tokens: 0,
17
- total_tokens: 0
18
- }
19
31
  };
32
+
33
+ if (usage) {
34
+ response.usage = usage;
35
+ }
36
+
37
+ return response;
20
38
  }
21
39
 
22
40
  export function createChatCompletionChunk(
@@ -24,7 +42,7 @@ export function createChatCompletionChunk(
24
42
  created: number,
25
43
  model: string,
26
44
  deltaContent: string,
27
- done = false
45
+ done = false,
28
46
  ) {
29
47
  return {
30
48
  id,
@@ -35,8 +53,8 @@ export function createChatCompletionChunk(
35
53
  {
36
54
  index: 0,
37
55
  delta: deltaContent ? { content: deltaContent } : {},
38
- finish_reason: done ? "stop" : null
56
+ finish_reason: done ? "stop" : null,
39
57
  }
40
- ]
58
+ ],
41
59
  };
42
- }
60
+ }
@@ -36,7 +36,7 @@ function debugLogToFile(message: string, data: any): void {
36
36
  * Handles role:"tool" result messages and assistant tool_calls that
37
37
  * plain text flattening would silently drop.
38
38
  */
39
- export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>): string {
39
+ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>, subagentNames: string[] = []): string {
40
40
  // DEBUG: Log incoming message structure to file for root cause analysis
41
41
  const messageSummary = messages.map((m: any, i: number) => {
42
42
  const role = m?.role ?? "?";
@@ -98,6 +98,15 @@ export function buildPromptFromMessages(messages: Array<any>, tools: Array<any>)
98
98
  `SYSTEM: You have access to the following tools. When you need to use one, respond with a tool_call in the standard OpenAI format.\n` +
99
99
  `Tool guidance: prefer write/edit for file changes; use bash mainly to run commands/tests.\n\nAvailable tools:\n${toolDescs}`,
100
100
  );
101
+ const hasTaskTool = tools.some((t: any) => {
102
+ const name = (t?.function?.name ?? t?.name ?? "").toLowerCase();
103
+ return name === "task";
104
+ });
105
+ if (hasTaskTool && subagentNames.length > 0) {
106
+ lines.push(
107
+ `When calling the task tool, set subagent_type to one of: ${subagentNames.join(", ")}. Do not omit this parameter.`
108
+ );
109
+ }
101
110
  }
102
111
 
103
112
  for (const message of messages) {
@@ -75,8 +75,13 @@ export type StreamJsonResultEvent = {
75
75
  type: "result";
76
76
  subtype?: "success" | "error" | string;
77
77
  timestamp?: number;
78
+ duration_ms?: number;
79
+ duration_api_ms?: number;
78
80
  session_id?: string;
81
+ request_id?: string;
82
+ result?: string;
79
83
  is_error?: boolean;
84
+ usage?: Record<string, unknown>;
80
85
  error?: {
81
86
  message?: string;
82
87
  code?: number | string;