@bitkyc08/opencodex 2.6.17 → 2.6.18
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/README.md +9 -0
- package/bin/ocx.mjs +70 -5
- package/gui/dist/assets/index-DDcEW0Cm.css +1 -0
- package/gui/dist/assets/index-DbTEyo46.js +9 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +9 -2
- package/src/adapters/base.ts +6 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +88 -0
- package/src/adapters/cursor/cursor-errors.ts +85 -0
- package/src/adapters/cursor/discovery.ts +144 -0
- package/src/adapters/cursor/effort-map.ts +74 -0
- package/src/adapters/cursor/exec-policy.ts +44 -0
- package/src/adapters/cursor/framing.ts +136 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +25 -0
- package/src/adapters/cursor/live-models.ts +93 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +758 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +236 -0
- package/src/adapters/cursor/message-mapper.ts +46 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +177 -0
- package/src/adapters/cursor/native-exec-fs.ts +284 -0
- package/src/adapters/cursor/native-exec-mcp.ts +151 -0
- package/src/adapters/cursor/native-exec-network.ts +32 -0
- package/src/adapters/cursor/native-exec-shell.ts +191 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +177 -0
- package/src/adapters/cursor/protobuf-events.ts +309 -0
- package/src/adapters/cursor/protobuf-request.ts +347 -0
- package/src/adapters/cursor/request-builder.ts +98 -0
- package/src/adapters/cursor/tool-definitions.ts +301 -0
- package/src/adapters/cursor/transport-retry.ts +116 -0
- package/src/adapters/cursor/transport.ts +47 -0
- package/src/adapters/cursor/types.ts +36 -0
- package/src/adapters/cursor.ts +99 -0
- package/src/adapters/google.ts +7 -1
- package/src/adapters/kiro.ts +15 -0
- package/src/adapters/openai-chat.ts +7 -2
- package/src/adapters/run-turn-queue.ts +58 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/bridge.ts +7 -1
- package/src/cli-help.ts +9 -2
- package/src/cli-status.ts +7 -5
- package/src/cli.ts +122 -79
- package/src/codex-catalog.ts +213 -71
- package/src/codex-history-provider.ts +31 -14
- package/src/codex-inject.ts +17 -9
- package/src/codex-paths.ts +2 -1
- package/src/codex-shim.ts +30 -7
- package/src/codex-sync.ts +70 -0
- package/src/config.ts +58 -2
- package/src/doctor.ts +4 -2
- package/src/index.ts +1 -0
- package/src/model-cache.ts +22 -2
- package/src/oauth/callback-server.ts +44 -16
- package/src/oauth/cursor.ts +188 -0
- package/src/oauth/index.ts +29 -3
- package/src/oauth/key-providers.ts +20 -33
- package/src/oauth/login-cli.ts +7 -4
- package/src/open-url.ts +5 -1
- package/src/ports.ts +13 -0
- package/src/process-control.ts +76 -0
- package/src/provider-label.ts +10 -5
- package/src/providers/derive.ts +30 -3
- package/src/providers/registry.ts +39 -1
- package/src/proxy-liveness.ts +122 -0
- package/src/responses/parser.ts +1 -0
- package/src/responses/state.ts +83 -0
- package/src/router.ts +38 -23
- package/src/server/adapter-resolve.ts +3 -0
- package/src/server.ts +130 -18
- package/src/service.ts +94 -32
- package/src/types.ts +24 -1
- package/src/update-job.ts +360 -0
- package/src/update.ts +73 -11
- package/src/usage-log.ts +3 -3
- package/src/usage-summary.ts +3 -2
- package/src/win-paths.ts +68 -0
- package/gui/dist/assets/index-DIBiVVC0.css +0 -1
- package/gui/dist/assets/index-DcnD944i.js +0 -9
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { create, fromJson, toBinary, type JsonValue } from "@bufbuild/protobuf";
|
|
2
|
+
import { ValueSchema } from "@bufbuild/protobuf/wkt";
|
|
3
|
+
import type { OcxRequestOptions, OcxTool } from "../../types";
|
|
4
|
+
import { namespacedToolName } from "../../types";
|
|
5
|
+
import { McpToolDefinitionSchema, type McpToolDefinition } from "./gen/agent_pb";
|
|
6
|
+
|
|
7
|
+
export const OCX_RESPONSES_TOOL_PROVIDER = "opencodex-responses";
|
|
8
|
+
export const CODEX_EXEC_COMMAND_TOOL = "exec_command";
|
|
9
|
+
export const CODEX_APPLY_PATCH_TOOL = "apply_patch";
|
|
10
|
+
export const CURSOR_EXEC_COMMAND_TOOL = CODEX_EXEC_COMMAND_TOOL;
|
|
11
|
+
export const CURSOR_SHELL_ALIAS_SYSTEM_NOTE = 'Shell commands use the `exec_command` tool with JSON arguments like {"cmd":"..."}.';
|
|
12
|
+
export const CURSOR_SHELL_ALIAS_USER_HINT = "Use exec_command for this shell command.";
|
|
13
|
+
const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as const;
|
|
14
|
+
|
|
15
|
+
export const CURSOR_GENERIC_TOOL_USE_USER_HINT = [
|
|
16
|
+
"For generic tool-use/count demos, satisfy the request with repeated `exec_command` calls for harmless commands.",
|
|
17
|
+
"`exec_command` is the Codex Responses bridge exec tool exposed through Cursor's tool protocol; do not describe it as an external MCP server tool.",
|
|
18
|
+
"Do not use `run_shell` unless this turn's tool catalog lists it.",
|
|
19
|
+
"A request for N tools means N separate `exec_command` invocations/results; never satisfy it with one chained shell command such as `cmd1 && cmd2`.",
|
|
20
|
+
"For independent read-only or output-only commands, emit all requested `exec_command` calls in the same response before waiting when the runtime supports parallel tool calls.",
|
|
21
|
+
"The Cursor bridge may suspend after the first returned bridge tool call, so emit sibling calls together before any result is needed.",
|
|
22
|
+
"If parallel emission is unavailable, continue with separate `exec_command` calls until the requested count has returned.",
|
|
23
|
+
"Do not use `tool_search`, external MCP, or resource discovery just to pad the count unless explicitly asked.",
|
|
24
|
+
"Do not suggest or switch to neighboring-agent tools such as `Grep`, `Read`, `Glob`, `Bash`, or `LS` unless this turn's catalog lists those exact names.",
|
|
25
|
+
].join(" ");
|
|
26
|
+
|
|
27
|
+
export const CURSOR_EXEC_COMMAND_INPUT_SCHEMA = {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
cmd: { type: "string", description: "Shell command to execute." },
|
|
31
|
+
workdir: { type: "string", description: "Working directory for the command. Defaults to the turn cwd." },
|
|
32
|
+
shell: { type: "string", description: "Shell binary to launch. Defaults to the user's default shell." },
|
|
33
|
+
tty: { type: "boolean", description: "True allocates a PTY for the command; false or omitted uses plain pipes." },
|
|
34
|
+
yield_time_ms: { type: "number", description: "Wait before yielding output. Defaults to 10000 ms; effective range is 250-30000 ms." },
|
|
35
|
+
max_output_tokens: { type: "number", description: "Output token budget. Defaults to 10000 tokens; larger requests may be capped by policy." },
|
|
36
|
+
},
|
|
37
|
+
required: ["cmd"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
function isBareCodexExecCommandTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
|
|
42
|
+
return !tool.namespace && tool.name === CODEX_EXEC_COMMAND_TOOL;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function cursorRequestHasShellAlias(tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined): boolean {
|
|
46
|
+
return tools?.some(isBareCodexExecCommandTool) ?? false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function cursorRequestAdvertisesApplyPatch(
|
|
50
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
51
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
52
|
+
): boolean {
|
|
53
|
+
return tools?.some(tool => !tool.namespace && tool.name === CODEX_APPLY_PATCH_TOOL && tool.freeform === true && toolChoiceAllows(tool, toolChoice)) ?? false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
|
|
57
|
+
return namespacedToolName(tool.namespace, tool.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Cursor's harness shows MCP tools to the model as `mcp_<providerIdentifier>_<toolName>`; models
|
|
62
|
+
* sometimes call that display name verbatim instead of the advertised short name (live 20:41/21:00
|
|
63
|
+
* sessions: `mcp_opencodex-responses_exec_command`). Fold the display prefix back to the advertised
|
|
64
|
+
* wire name so both spellings resolve to the same client tool instead of "unknown Responses tool".
|
|
65
|
+
*/
|
|
66
|
+
const CURSOR_MCP_DISPLAY_PREFIX = `mcp_${OCX_RESPONSES_TOOL_PROVIDER}_`;
|
|
67
|
+
|
|
68
|
+
export function normalizeCursorWireName(name: string): string {
|
|
69
|
+
return name.startsWith(CURSOR_MCP_DISPLAY_PREFIX) ? name.slice(CURSOR_MCP_DISPLAY_PREFIX.length) : name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function responsesToolNameFromCursorWire(name: string, cursorToolNameMap?: ReadonlyMap<string, string>): string {
|
|
73
|
+
const normalized = normalizeCursorWireName(name);
|
|
74
|
+
return cursorToolNameMap?.get(normalized) ?? normalized;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function cursorToolInputSchema(tool: OcxTool): unknown {
|
|
78
|
+
return isBareCodexExecCommandTool(tool) ? CURSOR_EXEC_COMMAND_INPUT_SCHEMA : (tool.parameters ?? {});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function activeTextMentionsExecCommand(text: string): boolean {
|
|
82
|
+
return /\bexec_command\b/i.test(text);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function looksLikeShellCommandRequest(text: string): boolean {
|
|
86
|
+
const hasKnownCommand = /(?:^|[\s`$])(?:echo|pwd|ls|cat|grep|rg|find|python3?|node|bun|npm|pnpm|yarn|git|curl|wget|chmod|mkdir|rm|cp|mv|touch|docker|kubectl|make|cargo|go|pytest)(?=\s|$|[`:;|&])/i.test(text);
|
|
87
|
+
const hasRunIntent = /\b(?:run|execute|exec)\b/i.test(text) || /\b(?:stdout|stderr|exit\s+code)\b/i.test(text);
|
|
88
|
+
const hasShellTarget = /\b(?:shell|terminal|command|cmd)\b/i.test(text);
|
|
89
|
+
return /\b(?:run|execute|exec)\s*:/i.test(text) || hasKnownCommand || (hasRunIntent && hasShellTarget);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function isGenericToolUseCountDemoPrompt(text: string): boolean {
|
|
93
|
+
const trimmed = text.trim();
|
|
94
|
+
if (trimmed.length === 0) return false;
|
|
95
|
+
return [
|
|
96
|
+
/\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?\d+\s+tools?\b/i,
|
|
97
|
+
/\buse\s+any\s+tools?\b/i,
|
|
98
|
+
/\bactually\s+(?:call|use|invoke)\s+(?:the\s+)?tools?\b/i,
|
|
99
|
+
/\b\d+\s+tools?\b/i,
|
|
100
|
+
/\btools?\s+\d+\b/i,
|
|
101
|
+
/\btool\s+use\b/i,
|
|
102
|
+
/아무\s*(?:tool|tools?|도구|툴)/i,
|
|
103
|
+
/(?:tool|tools?|도구|툴)\s*\d+\s*(?:개|번)?/i,
|
|
104
|
+
/\d+\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i,
|
|
105
|
+
/(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}\d+\s*(?:개|번)?/i,
|
|
106
|
+
].some(pattern => pattern.test(trimmed));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function requestedCursorToolUseCount(text: string): number | undefined {
|
|
110
|
+
const patterns = [
|
|
111
|
+
/\b(?:use|call|invoke|try|exercise)\s+(?:any\s+)?(\d+)\s+tools?\b/i,
|
|
112
|
+
/\b(\d+)\s+tools?\b/i,
|
|
113
|
+
/\btools?\s+(\d+)\b/i,
|
|
114
|
+
/(?:tool|tools?|도구|툴)\s*(\d+)\s*(?:개|번)?/i,
|
|
115
|
+
/(\d+)\s*(?:개|번)?\s*(?:tool|tools?|도구|툴)/i,
|
|
116
|
+
/(?:도구|툴).{0,12}(?:써|사용|호출).{0,12}(\d+)\s*(?:개|번)?/i,
|
|
117
|
+
];
|
|
118
|
+
for (const pattern of patterns) {
|
|
119
|
+
const match = pattern.exec(text);
|
|
120
|
+
const count = Number(match?.[1]);
|
|
121
|
+
if (Number.isInteger(count) && count > 0 && count <= 50) return count;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cursorGenericToolUseHint(text: string): string {
|
|
127
|
+
const count = requestedCursorToolUseCount(text);
|
|
128
|
+
if (!count) return CURSOR_GENERIC_TOOL_USE_USER_HINT;
|
|
129
|
+
return [
|
|
130
|
+
`This turn requests ${count} tool uses: emit exactly ${count} separate \`exec_command\` function calls/results.`,
|
|
131
|
+
`One \`exec_command\` containing chained commands counts as 1 tool call, not ${count}.`,
|
|
132
|
+
`Prefer one parallel tool-call batch containing all ${count} independent \`exec_command\` calls before waiting for results.`,
|
|
133
|
+
CURSOR_GENERIC_TOOL_USE_USER_HINT,
|
|
134
|
+
].join(" ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function activeTextMentionsGenericToolUseHint(text: string): boolean {
|
|
138
|
+
return text.includes("Codex native exec tool")
|
|
139
|
+
|| text.includes("Codex Responses bridge exec tool")
|
|
140
|
+
|| text.includes("generic tool-use/count demos");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function shouldAppendCursorGenericToolUseHint(
|
|
144
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
145
|
+
text: string,
|
|
146
|
+
): boolean {
|
|
147
|
+
const trimmed = text.trim();
|
|
148
|
+
return trimmed.length > 0
|
|
149
|
+
&& cursorRequestHasShellAlias(tools)
|
|
150
|
+
&& isGenericToolUseCountDemoPrompt(trimmed)
|
|
151
|
+
&& !activeTextMentionsGenericToolUseHint(trimmed);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function appendCursorGenericToolUseHint(
|
|
155
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
156
|
+
text: string,
|
|
157
|
+
): string {
|
|
158
|
+
if (!shouldAppendCursorGenericToolUseHint(tools, text)) return text;
|
|
159
|
+
return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${cursorGenericToolUseHint(text)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function shouldUseNativeExecOnlyForGenericToolUse(
|
|
163
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
164
|
+
text: string,
|
|
165
|
+
): boolean {
|
|
166
|
+
const trimmed = text.trim();
|
|
167
|
+
if (trimmed.length === 0 || !cursorRequestHasShellAlias(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false;
|
|
168
|
+
return !/\b(?:mcp|resource|resources|tool_search|plugin|plugins|app connector|github)\b/i.test(trimmed)
|
|
169
|
+
&& !/(?:리소스|플러그인|깃허브|github)/i.test(trimmed);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" | "name">>(
|
|
173
|
+
tools: readonly T[] | undefined,
|
|
174
|
+
activeText: string,
|
|
175
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
176
|
+
): readonly T[] | undefined {
|
|
177
|
+
if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools;
|
|
178
|
+
const execTools = tools?.filter(isBareCodexExecCommandTool);
|
|
179
|
+
if (execTools?.length && !execTools.some(tool => toolChoiceAllows(tool, toolChoice))) return tools;
|
|
180
|
+
return execTools && execTools.length > 0 ? execTools : tools;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function shouldAppendCursorShellAliasHint(
|
|
184
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
185
|
+
text: string,
|
|
186
|
+
): boolean {
|
|
187
|
+
const trimmed = text.trim();
|
|
188
|
+
return trimmed.length > 0
|
|
189
|
+
&& cursorRequestHasShellAlias(tools)
|
|
190
|
+
&& !activeTextMentionsExecCommand(trimmed)
|
|
191
|
+
&& looksLikeShellCommandRequest(trimmed);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function appendCursorShellAliasHint(
|
|
195
|
+
tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
|
|
196
|
+
text: string,
|
|
197
|
+
): string {
|
|
198
|
+
if (!shouldAppendCursorShellAliasHint(tools, text)) return text;
|
|
199
|
+
return `${text}${text.endsWith("\n") ? "\n" : "\n\n"}${CURSOR_SHELL_ALIAS_USER_HINT}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function toolChoiceAllows(tool: Pick<OcxTool, "namespace" | "name">, toolChoice: OcxRequestOptions["toolChoice"] | undefined): boolean {
|
|
203
|
+
if (!toolChoice || toolChoice === "auto" || toolChoice === "required") return true;
|
|
204
|
+
if (toolChoice === "none") return false;
|
|
205
|
+
if ("allowedTools" in toolChoice) {
|
|
206
|
+
return toolChoice.allowedTools.includes(tool.name) || toolChoice.allowedTools.includes(cursorToolWireName(tool));
|
|
207
|
+
}
|
|
208
|
+
return tool.name === toolChoice.name || cursorToolWireName(tool) === toolChoice.name;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function quotedNames(names: readonly string[]): string {
|
|
212
|
+
return names.map(name => `\`${name}\``).join(", ");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string[] {
|
|
216
|
+
const advertised = new Set(wireNames);
|
|
217
|
+
return NEIGHBOR_AGENT_TOOL_NAMES.filter(name => !advertised.has(name));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function discoveryToolLabel(wireNames: readonly string[]): string | undefined {
|
|
221
|
+
const labels: string[] = [];
|
|
222
|
+
if (wireNames.includes("tool_search")) labels.push("`tool_search`");
|
|
223
|
+
if (wireNames.some(name => name.startsWith("mcp__"))) labels.push("MCP");
|
|
224
|
+
if (wireNames.some(name => /resource/i.test(name))) labels.push("resource discovery");
|
|
225
|
+
return labels.length > 0 ? labels.join(", ") : undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildCursorToolGuidanceSystemNote(
|
|
229
|
+
tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
|
|
230
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
231
|
+
): string | undefined {
|
|
232
|
+
if (!tools?.length) return undefined;
|
|
233
|
+
const wireNames = [...new Set(
|
|
234
|
+
tools
|
|
235
|
+
.filter(tool => toolChoiceAllows(tool, toolChoice))
|
|
236
|
+
.map(tool => cursorToolWireName(tool)),
|
|
237
|
+
)];
|
|
238
|
+
if (wireNames.length === 0) return undefined;
|
|
239
|
+
|
|
240
|
+
const listedNames = quotedNames(wireNames);
|
|
241
|
+
const hasBareExec = wireNames.includes(CODEX_EXEC_COMMAND_TOOL);
|
|
242
|
+
const hasApplyPatch = cursorRequestAdvertisesApplyPatch(tools, toolChoice);
|
|
243
|
+
const discoveryTools = discoveryToolLabel(wireNames);
|
|
244
|
+
const unavailableNeighborNames = unavailableNeighborAgentToolNames(wireNames);
|
|
245
|
+
const notes = [
|
|
246
|
+
`Cursor tool calls: available tool names are exactly ${listedNames}.`,
|
|
247
|
+
"Use the current tool catalog as ground truth and call only those exact names with their listed argument keys.",
|
|
248
|
+
unavailableNeighborNames.length > 0
|
|
249
|
+
? `This turn does not expose neighboring-agent tool names ${quotedNames(unavailableNeighborNames)}; do not call or suggest them unless the catalog lists them.`
|
|
250
|
+
: undefined,
|
|
251
|
+
hasBareExec
|
|
252
|
+
? "`exec_command` is the Codex Responses bridge exec tool for this turn, exposed through Cursor's tool protocol; it is not an external MCP server tool."
|
|
253
|
+
: undefined,
|
|
254
|
+
hasBareExec
|
|
255
|
+
? "Your tool list may display it under the longer name `mcp_opencodex-responses_exec_command`; both names are the SAME tool — call whichever your list shows, and do not comment on the naming difference to the user."
|
|
256
|
+
: undefined,
|
|
257
|
+
"Cursor product features (Chronicle, screen recording, Notes, Plans, background agents) are available only if this turn's catalog lists a matching tool; do not offer or promise them otherwise.",
|
|
258
|
+
hasBareExec
|
|
259
|
+
? "For file read/search/listing, use `exec_command` when no more specific listed tool is available."
|
|
260
|
+
: undefined,
|
|
261
|
+
hasApplyPatch
|
|
262
|
+
? "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
|
|
263
|
+
: undefined,
|
|
264
|
+
hasBareExec
|
|
265
|
+
? "For tool-count demos, each counted tool must be a separate `exec_command` invocation/result; do not collapse several requested tools into one chained shell command."
|
|
266
|
+
: undefined,
|
|
267
|
+
"For independent read-only tool-count or batch requests, prefer one response containing multiple tool calls before waiting for results when the runtime supports parallel tool calls.",
|
|
268
|
+
hasBareExec
|
|
269
|
+
? "For bridge tool-count batches, emit sibling `exec_command` calls together before any result is needed because the bridge may suspend after a returned tool call."
|
|
270
|
+
: undefined,
|
|
271
|
+
discoveryTools
|
|
272
|
+
? `Use ${discoveryTools} only for explicit discovery/resource tasks, not generic tool-count demos.`
|
|
273
|
+
: undefined,
|
|
274
|
+
"Do not count or report a tool call unless a tool result was actually returned.",
|
|
275
|
+
].filter((note): note is string => typeof note === "string");
|
|
276
|
+
return notes.join(" ");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function encodeCursorInputSchema(schema: unknown): Uint8Array {
|
|
280
|
+
const value: JsonValue = schema && typeof schema === "object"
|
|
281
|
+
? schema as JsonValue
|
|
282
|
+
: { type: "object", properties: {}, required: [] };
|
|
283
|
+
return toBinary(ValueSchema, fromJson(ValueSchema, value));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function buildCursorToolDefinitions(
|
|
287
|
+
tools: readonly OcxTool[] | undefined,
|
|
288
|
+
toolChoice?: OcxRequestOptions["toolChoice"],
|
|
289
|
+
): McpToolDefinition[] {
|
|
290
|
+
if (!tools?.length) return [];
|
|
291
|
+
return tools.filter(tool => toolChoiceAllows(tool, toolChoice)).map(tool => {
|
|
292
|
+
const wireName = cursorToolWireName(tool);
|
|
293
|
+
return create(McpToolDefinitionSchema, {
|
|
294
|
+
name: wireName,
|
|
295
|
+
toolName: wireName,
|
|
296
|
+
providerIdentifier: OCX_RESPONSES_TOOL_PROVIDER,
|
|
297
|
+
description: tool.description,
|
|
298
|
+
inputSchema: encodeCursorInputSchema(cursorToolInputSchema(tool)),
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { CursorRunRequest, CursorServerMessage } from "./types";
|
|
2
|
+
import type { CursorTransport, CursorTransportFactory, CursorTransportFactoryInput } from "./transport";
|
|
3
|
+
|
|
4
|
+
export const CURSOR_RETRY_ATTEMPTS = 3;
|
|
5
|
+
export const CURSOR_RETRY_BASE_MS = 250;
|
|
6
|
+
export const CURSOR_RETRY_MAX_MS = 2_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* True only for clearly transient failures that occur BEFORE the run request is committed to the
|
|
10
|
+
* wire (connection refused/reset/timeout, immediate HTTP/2 GOAWAY, gRPC/Connect "unavailable").
|
|
11
|
+
* Conservative by design: auth, invalid-request, and anything ambiguous is non-retryable so we
|
|
12
|
+
* never replay a turn the Cursor server might already have accepted.
|
|
13
|
+
*/
|
|
14
|
+
export function isRetryableCursorError(err: unknown): boolean {
|
|
15
|
+
const code = typeof err === "object" && err && "code" in err ? String((err as { code?: unknown }).code ?? "") : "";
|
|
16
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
17
|
+
const haystack = `${code} ${message}`.toLowerCase();
|
|
18
|
+
if (/auth|unauthor|forbidden|invalid|permission|denied|not found|unsupported/.test(haystack)) return false;
|
|
19
|
+
return (
|
|
20
|
+
haystack.includes("econnreset") ||
|
|
21
|
+
haystack.includes("econnrefused") ||
|
|
22
|
+
haystack.includes("etimedout") ||
|
|
23
|
+
haystack.includes("enetunreach") ||
|
|
24
|
+
haystack.includes("eai_again") ||
|
|
25
|
+
haystack.includes("goaway") ||
|
|
26
|
+
haystack.includes("nghttp2") ||
|
|
27
|
+
haystack.includes("socket hang up") ||
|
|
28
|
+
haystack.includes("connection reset") ||
|
|
29
|
+
haystack.includes("unavailable") ||
|
|
30
|
+
haystack.includes("timed out")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function cursorRetryDelayMs(attempt: number): number {
|
|
35
|
+
const exp = Math.min(CURSOR_RETRY_BASE_MS * 2 ** attempt, CURSOR_RETRY_MAX_MS);
|
|
36
|
+
return Math.floor(exp * (0.8 + Math.random() * 0.4));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function abortError(signal?: AbortSignal): unknown {
|
|
40
|
+
return signal?.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function abortAwareSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
44
|
+
if (ms <= 0) return;
|
|
45
|
+
if (signal?.aborted) throw abortError(signal);
|
|
46
|
+
await new Promise<void>((resolve, reject) => {
|
|
47
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
signal?.removeEventListener("abort", onAbort);
|
|
51
|
+
};
|
|
52
|
+
const onAbort = () => {
|
|
53
|
+
cleanup();
|
|
54
|
+
reject(abortError(signal));
|
|
55
|
+
};
|
|
56
|
+
timer = setTimeout(() => {
|
|
57
|
+
cleanup();
|
|
58
|
+
resolve();
|
|
59
|
+
}, ms);
|
|
60
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A transport is safe to retry only if it explicitly reports the run request was never committed.
|
|
66
|
+
* A transport without `requestCommitted` is treated as committed (not retryable) — fail safe.
|
|
67
|
+
*/
|
|
68
|
+
function requestUncommitted(transport: CursorTransport): boolean {
|
|
69
|
+
return typeof transport.requestCommitted === "function" && transport.requestCommitted() === false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run a Cursor turn with bounded retry on pre-commit transient failures.
|
|
74
|
+
*
|
|
75
|
+
* `onEvent` receives every server message. Retry happens only when ALL hold:
|
|
76
|
+
* - nothing has been emitted yet this turn,
|
|
77
|
+
* - the failing transport reports the run request was not committed to the wire,
|
|
78
|
+
* - the error is a transient pre-commit failure.
|
|
79
|
+
* Otherwise the error propagates (the adapter maps it to a user-facing message).
|
|
80
|
+
*/
|
|
81
|
+
export async function runCursorTurnWithRetry(
|
|
82
|
+
makeTransport: (input: CursorTransportFactoryInput) => CursorTransport,
|
|
83
|
+
input: CursorTransportFactoryInput,
|
|
84
|
+
request: CursorRunRequest,
|
|
85
|
+
signal: AbortSignal | undefined,
|
|
86
|
+
onEvent: (message: CursorServerMessage, transport: CursorTransport) => void,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
let lastError: unknown;
|
|
89
|
+
for (let attempt = 0; attempt < CURSOR_RETRY_ATTEMPTS; attempt++) {
|
|
90
|
+
if (signal?.aborted) throw abortError(signal);
|
|
91
|
+
const transport = makeTransport(input);
|
|
92
|
+
let emittedAny = false;
|
|
93
|
+
try {
|
|
94
|
+
for await (const message of transport.run(request, signal)) {
|
|
95
|
+
emittedAny = true;
|
|
96
|
+
onEvent(message, transport);
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
lastError = err;
|
|
101
|
+
const canRetry =
|
|
102
|
+
!emittedAny &&
|
|
103
|
+
attempt < CURSOR_RETRY_ATTEMPTS - 1 &&
|
|
104
|
+
!signal?.aborted &&
|
|
105
|
+
requestUncommitted(transport) &&
|
|
106
|
+
isRetryableCursorError(err);
|
|
107
|
+
if (!canRetry) throw err;
|
|
108
|
+
await abortAwareSleep(cursorRetryDelayMs(attempt), signal);
|
|
109
|
+
} finally {
|
|
110
|
+
await transport.close?.();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw lastError ?? new Error("Cursor transport failed");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type { CursorTransportFactory };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { OcxProviderConfig } from "../../types";
|
|
2
|
+
import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
|
|
3
|
+
|
|
4
|
+
export interface CursorTransport {
|
|
5
|
+
run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage>;
|
|
6
|
+
writeClient(message: CursorClientMessage): void | Promise<void>;
|
|
7
|
+
close?(): void | Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the run request has been committed to the wire. The retry orchestrator only re-dials
|
|
10
|
+
* failures that happened BEFORE this becomes true, so a turn the Cursor server may already have
|
|
11
|
+
* accepted is never replayed. Absent (undefined) is treated as "committed" — safe by default.
|
|
12
|
+
*/
|
|
13
|
+
requestCommitted?(): boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CursorTransportFactoryInput {
|
|
17
|
+
provider: OcxProviderConfig;
|
|
18
|
+
headers?: Headers;
|
|
19
|
+
/** Pre-first-frame deadline (dial + first server frame). Defaults to 30s when omitted. */
|
|
20
|
+
firstFrameTimeoutMs?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Grace window (ms) before a drained client-tool turn is finalized, so a sibling tool call
|
|
23
|
+
* announced in a later receive chunk can revoke a premature finalize. Defaults to 50ms.
|
|
24
|
+
*/
|
|
25
|
+
clientToolFinalizeGraceMs?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type CursorTransportFactory = (input: CursorTransportFactoryInput) => CursorTransport;
|
|
29
|
+
|
|
30
|
+
export class CursorTransportDisabledError extends Error {
|
|
31
|
+
readonly code = "cursor_transport_disabled";
|
|
32
|
+
|
|
33
|
+
constructor(message = "live Cursor transport is disabled") {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "CursorTransportDisabledError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createDisabledCursorTransport(): CursorTransport {
|
|
40
|
+
return {
|
|
41
|
+
async *run() {
|
|
42
|
+
throw new CursorTransportDisabledError();
|
|
43
|
+
},
|
|
44
|
+
writeClient() {},
|
|
45
|
+
close() {},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { OcxUsage } from "../../types";
|
|
2
|
+
import type { OcxMessage, OcxRequestOptions, OcxTool } from "../../types";
|
|
3
|
+
|
|
4
|
+
export interface CursorRunRequest {
|
|
5
|
+
modelId: string;
|
|
6
|
+
conversationId: string;
|
|
7
|
+
system: string[];
|
|
8
|
+
messages: CursorRequestMessage[];
|
|
9
|
+
rawMessages?: OcxMessage[];
|
|
10
|
+
tools?: OcxTool[];
|
|
11
|
+
toolChoice?: OcxRequestOptions["toolChoice"];
|
|
12
|
+
parallelToolCalls?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CursorRequestMessage {
|
|
16
|
+
role: "user" | "assistant" | "developer" | "tool";
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CursorServerMessage =
|
|
21
|
+
| { type: "text"; text: string }
|
|
22
|
+
| { type: "thinking"; thinking: string }
|
|
23
|
+
| { type: "tool_call_start"; id: string; name: string }
|
|
24
|
+
| { type: "tool_call_delta"; arguments: string }
|
|
25
|
+
| { type: "tool_call_end"; id?: string }
|
|
26
|
+
| { type: "done"; usage?: OcxUsage }
|
|
27
|
+
| { type: "error"; message: string; usage?: OcxUsage }
|
|
28
|
+
| { type: "heartbeat" }
|
|
29
|
+
| { type: "kv_get"; key: string }
|
|
30
|
+
| { type: "kv_set"; key: string; value: Uint8Array }
|
|
31
|
+
| { type: "exec"; execCase: string; requestId: string };
|
|
32
|
+
|
|
33
|
+
export type CursorClientMessage =
|
|
34
|
+
| { type: "kv_value"; key: string; value?: Uint8Array }
|
|
35
|
+
| { type: "kv_stored"; key: string }
|
|
36
|
+
| { type: "exec_result"; requestId: string; ok: boolean; message: string };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AdapterEvent, OcxProviderConfig } from "../types";
|
|
2
|
+
import type { ProviderAdapter } from "./base";
|
|
3
|
+
import { cursorExecDeniedMessage } from "./cursor/exec-policy";
|
|
4
|
+
import { safeCursorErrorMessage } from "./cursor/cursor-errors";
|
|
5
|
+
import { createCursorKvStore, type CursorKvStore } from "./cursor/kv-store";
|
|
6
|
+
import { mapCursorServerMessage } from "./cursor/message-mapper";
|
|
7
|
+
import { createCursorRequest, generatedCursorConversationId } from "./cursor/request-builder";
|
|
8
|
+
import { createLiveCursorTransport, CursorMissingCredentialError } from "./cursor/live-transport";
|
|
9
|
+
import { runCursorTurnWithRetry } from "./cursor/transport-retry";
|
|
10
|
+
import {
|
|
11
|
+
createDisabledCursorTransport,
|
|
12
|
+
CursorTransportDisabledError,
|
|
13
|
+
type CursorTransportFactory,
|
|
14
|
+
} from "./cursor/transport";
|
|
15
|
+
|
|
16
|
+
export const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
CURSOR_EXEC_CASES_DENIED,
|
|
20
|
+
cursorExecDeniedMessage,
|
|
21
|
+
type CursorDeniedExecCase,
|
|
22
|
+
} from "./cursor/exec-policy";
|
|
23
|
+
|
|
24
|
+
const CURSOR_TRANSPORT_DISABLED_MESSAGE = [
|
|
25
|
+
"An explicit disabled Cursor transport was injected.",
|
|
26
|
+
"Production Cursor requests use live transport when a Cursor access token is configured.",
|
|
27
|
+
].join(" ");
|
|
28
|
+
|
|
29
|
+
export interface CursorAdapterDeps {
|
|
30
|
+
createTransport?: CursorTransportFactory;
|
|
31
|
+
kv?: CursorKvStore;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeCursorTransportError(err: unknown): string {
|
|
35
|
+
if (err instanceof CursorTransportDisabledError) return CURSOR_TRANSPORT_DISABLED_MESSAGE;
|
|
36
|
+
if (err instanceof CursorMissingCredentialError) {
|
|
37
|
+
return "Cursor live transport is enabled, but no Cursor access token is configured. Set provider.apiKey or OPENCODEX_CURSOR_TEST_TOKEN.";
|
|
38
|
+
}
|
|
39
|
+
const message = err instanceof Error ? err.message : typeof err === "string" ? err : undefined;
|
|
40
|
+
if (message) return safeCursorErrorMessage(message);
|
|
41
|
+
return "Cursor upstream error: transport failed before completion.";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createCursorAdapter(provider: OcxProviderConfig, deps: CursorAdapterDeps = {}): ProviderAdapter {
|
|
45
|
+
return {
|
|
46
|
+
name: "cursor",
|
|
47
|
+
|
|
48
|
+
buildRequest() {
|
|
49
|
+
return {
|
|
50
|
+
url: provider.baseUrl || CURSOR_API_URL,
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {},
|
|
53
|
+
body: "",
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async *parseStream(): AsyncGenerator<AdapterEvent> {
|
|
58
|
+
yield {
|
|
59
|
+
type: "error",
|
|
60
|
+
message: "Cursor adapter uses runTurn; the fetch/parseStream path is disabled.",
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async runTurn(_parsed, incoming, emit) {
|
|
65
|
+
if (incoming.abortSignal?.aborted) {
|
|
66
|
+
emit({ type: "error", message: "Cursor turn was aborted before start." });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const makeTransport = deps.createTransport ?? createLiveCursorTransport;
|
|
71
|
+
const kv = deps.kv ?? createCursorKvStore();
|
|
72
|
+
_parsed._cursorConversationId ??= generatedCursorConversationId();
|
|
73
|
+
const request = createCursorRequest(_parsed);
|
|
74
|
+
await runCursorTurnWithRetry(
|
|
75
|
+
makeTransport,
|
|
76
|
+
{ provider, headers: incoming.headers },
|
|
77
|
+
request,
|
|
78
|
+
incoming.abortSignal,
|
|
79
|
+
(message, activeTransport) => {
|
|
80
|
+
if (incoming.abortSignal?.aborted) {
|
|
81
|
+
emit({ type: "error", message: "Cursor turn was aborted." });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const events = mapCursorServerMessage(message, {
|
|
85
|
+
kv,
|
|
86
|
+
writeClient: clientMessage => {
|
|
87
|
+
void activeTransport.writeClient(clientMessage);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
for (const event of events) emit(event);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const partialUsage = (err as { partialUsage?: import("../types").OcxUsage }).partialUsage;
|
|
95
|
+
emit({ type: "error", message: safeCursorTransportError(err), ...(partialUsage ? { usage: partialUsage } : {}) });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/adapters/google.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { sanitizeGeminiToolParameters } from "./google-tool-schema";
|
|
|
21
21
|
import { neutralizeIdentity } from "./identity";
|
|
22
22
|
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
23
23
|
import { resolveAntigravityWireModelId } from "../providers/antigravity-models";
|
|
24
|
+
import { buildNonOpenAIToolCatalogNudgeForTools } from "./tool-catalog-nudge";
|
|
24
25
|
|
|
25
26
|
// Google-family models (Gemini/Vertex/Antigravity) tend to emit long running commentary between
|
|
26
27
|
// tool calls. This steers them to keep the BETWEEN-STEP text to one line and reason internally
|
|
@@ -87,7 +88,12 @@ function toolResultImageParts(content: string | OcxContentPart[]): unknown[] {
|
|
|
87
88
|
function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } {
|
|
88
89
|
// Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model
|
|
89
90
|
// never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream.
|
|
90
|
-
const
|
|
91
|
+
const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice);
|
|
92
|
+
const systemText = neutralizeIdentity([
|
|
93
|
+
...(parsed.context.systemPrompt ?? []),
|
|
94
|
+
...(toolCatalogNudge ? [toolCatalogNudge] : []),
|
|
95
|
+
GOOGLE_BREVITY_INSTRUCTION,
|
|
96
|
+
].join("\n\n"));
|
|
91
97
|
const systemInstruction = { parts: [{ text: systemText }] };
|
|
92
98
|
|
|
93
99
|
const contents: unknown[] = [];
|
package/src/adapters/kiro.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { extractKiroImages, type KiroImage } from "./kiro-images";
|
|
|
29
29
|
import { fetchKiroWithRetry } from "./kiro-retry";
|
|
30
30
|
import { convertKiroToolContext } from "./kiro-tools";
|
|
31
31
|
import { neutralizeIdentity } from "./identity";
|
|
32
|
+
import { buildNonOpenAIToolCatalogNudgeFromNames } from "./tool-catalog-nudge";
|
|
32
33
|
|
|
33
34
|
const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
|
|
34
35
|
const SDK_VERSION = "1.0.27";
|
|
@@ -57,6 +58,16 @@ interface KiroHistoryEntry {
|
|
|
57
58
|
userInputMessage?: KiroUserInputMessage;
|
|
58
59
|
assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
function kiroToolWireNames(tools: readonly unknown[]): string[] {
|
|
63
|
+
return tools
|
|
64
|
+
.map(tool => {
|
|
65
|
+
const spec = (tool as { toolSpecification?: { name?: unknown } }).toolSpecification;
|
|
66
|
+
return typeof spec?.name === "string" ? spec.name : undefined;
|
|
67
|
+
})
|
|
68
|
+
.filter((name): name is string => typeof name === "string");
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
function userContentText(content: string | OcxContentPart[]): string {
|
|
61
72
|
if (typeof content === "string") return content;
|
|
62
73
|
return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
|
|
@@ -202,6 +213,10 @@ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string |
|
|
|
202
213
|
// Neutralize Codex's GPT-5 identity line so a routed Kiro model never misreports as GPT-5/OpenAI
|
|
203
214
|
// and the proxy identity never leaks upstream.
|
|
204
215
|
if (!parsed.previousResponseId && parsed.context.systemPrompt?.length) systemParts.push(neutralizeIdentity(parsed.context.systemPrompt.join("\n\n")));
|
|
216
|
+
const toolCatalogNudge = !parsed.previousResponseId
|
|
217
|
+
? buildNonOpenAIToolCatalogNudgeFromNames(kiroToolWireNames(kiroTools))
|
|
218
|
+
: undefined;
|
|
219
|
+
if (toolCatalogNudge) systemParts.push(toolCatalogNudge);
|
|
205
220
|
if (toolContext.systemAdditions.length > 0) systemParts.push(...toolContext.systemAdditions);
|
|
206
221
|
const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
|
|
207
222
|
const structuredToolIds = new Set<string>();
|