@bitkyc08/opencodex 2.63.0 → 2.64.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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-CpuDF3ci.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-B4uEIa1O.js} +1 -1
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/index-SggB6t3z.js +86 -0
- package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -138,25 +138,72 @@ function asString(value: unknown): string | undefined {
|
|
|
138
138
|
return typeof value === "string" ? value : undefined;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
/** Extract OpenCodex usage from
|
|
142
|
-
|
|
143
|
-
const usage = asRecord(message.usage);
|
|
144
|
-
if (!usage) return undefined;
|
|
141
|
+
/** Extract OpenCodex usage from the Anthropic-shaped usage record shared by frames and deltas. */
|
|
142
|
+
function usageFromAnthropicShape(usage: Record<string, unknown>): OcxUsage | undefined {
|
|
145
143
|
const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
146
144
|
const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
147
145
|
const cachedInputTokens = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined;
|
|
148
146
|
const cacheCreationInputTokens =
|
|
149
147
|
typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined;
|
|
150
|
-
|
|
148
|
+
// A snapshot is zero-only when every counter is absent or zero. Testing only the cache-read
|
|
149
|
+
// field dropped a cache-creation-only snapshot (input/output 0 with, say, 200 cache-creation
|
|
150
|
+
// tokens), and a capture-only tool leg terminated at message_stop never sees a result frame
|
|
151
|
+
// that could carry those tokens instead, so the turn under-reported usage and cost.
|
|
152
|
+
const cacheReadTotal = cachedInputTokens ?? 0;
|
|
153
|
+
const cacheCreationTotal = cacheCreationInputTokens ?? 0;
|
|
154
|
+
if (inputTokens === 0 && outputTokens === 0 && cacheReadTotal === 0 && cacheCreationTotal === 0) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
inputTokens,
|
|
159
|
+
outputTokens,
|
|
160
|
+
totalTokens: inputTokens + outputTokens,
|
|
161
|
+
...(cacheReadTotal > 0 ? { cachedInputTokens: cacheReadTotal, cacheReadInputTokens: cacheReadTotal } : {}),
|
|
162
|
+
...(cacheCreationTotal > 0 ? { cacheCreationInputTokens: cacheCreationTotal } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Extract OpenCodex usage from a `result` frame's Anthropic-shaped usage object. */
|
|
167
|
+
export function usageFromResult(message: StreamMessage): OcxUsage | undefined {
|
|
168
|
+
const usage = asRecord(message.usage);
|
|
169
|
+
return usage ? usageFromAnthropicShape(usage) : undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Fold a pre-result usage snapshot into the running partial usage.
|
|
174
|
+
*
|
|
175
|
+
* `message_delta` and assistant-frame snapshots are cumulative per message, but a later snapshot
|
|
176
|
+
* can repeat or extend an earlier one, so each field keeps its maximum. The `result` frame stays
|
|
177
|
+
* authoritative for a text-only turn; partial state exists so a capture-only tool-bridge turn —
|
|
178
|
+
* which is terminated at `message_stop` before any result frame can arrive — still reports real
|
|
179
|
+
* token usage instead of zero.
|
|
180
|
+
*/
|
|
181
|
+
function mergePartialUsage(previous: OcxUsage | undefined, next: OcxUsage): OcxUsage {
|
|
182
|
+
if (!previous) return next;
|
|
183
|
+
const inputTokens = Math.max(previous.inputTokens, next.inputTokens);
|
|
184
|
+
const outputTokens = Math.max(previous.outputTokens, next.outputTokens);
|
|
185
|
+
const cacheRead = Math.max(
|
|
186
|
+
previous.cacheReadInputTokens ?? previous.cachedInputTokens ?? 0,
|
|
187
|
+
next.cacheReadInputTokens ?? next.cachedInputTokens ?? 0,
|
|
188
|
+
);
|
|
189
|
+
const cacheCreation = Math.max(previous.cacheCreationInputTokens ?? 0, next.cacheCreationInputTokens ?? 0);
|
|
151
190
|
return {
|
|
152
191
|
inputTokens,
|
|
153
192
|
outputTokens,
|
|
154
193
|
totalTokens: inputTokens + outputTokens,
|
|
155
|
-
...(
|
|
156
|
-
...(
|
|
194
|
+
...(cacheRead > 0 ? { cachedInputTokens: cacheRead, cacheReadInputTokens: cacheRead } : {}),
|
|
195
|
+
...(cacheCreation > 0 ? { cacheCreationInputTokens: cacheCreation } : {}),
|
|
157
196
|
};
|
|
158
197
|
}
|
|
159
198
|
|
|
199
|
+
/** Record one usage snapshot; absent, malformed, or zero-only snapshots leave state untouched. */
|
|
200
|
+
function observePartialUsage(state: StreamParseState, value: unknown): void {
|
|
201
|
+
const usage = asRecord(value);
|
|
202
|
+
if (!usage) return;
|
|
203
|
+
const next = usageFromAnthropicShape(usage);
|
|
204
|
+
if (next) state.partialUsage = mergePartialUsage(state.partialUsage, next);
|
|
205
|
+
}
|
|
206
|
+
|
|
160
207
|
/**
|
|
161
208
|
* Mutable per-turn parse state shared across frames of one stream (§十二).
|
|
162
209
|
* Thinking and text states are strictly decoupled.
|
|
@@ -166,6 +213,16 @@ export interface StreamParseState {
|
|
|
166
213
|
sawPartialThinking: boolean;
|
|
167
214
|
sawTerminalResult: boolean;
|
|
168
215
|
openToolCallId?: string;
|
|
216
|
+
/** A `message_stop` stream event arrived: the assistant message is complete. */
|
|
217
|
+
sawMessageStop?: boolean;
|
|
218
|
+
/** Completed tool_use content blocks observed in this stream. */
|
|
219
|
+
completedToolCalls?: number;
|
|
220
|
+
/** Tool IDs already captured through partial events, for complete-assistant deduplication. */
|
|
221
|
+
partialToolCallIds?: Set<string>;
|
|
222
|
+
/** A complete assistant tool block had no matching partial capture. */
|
|
223
|
+
uncapturedToolUse?: boolean;
|
|
224
|
+
/** Highest-seen usage snapshot from `message_delta`/assistant frames before a terminal result. */
|
|
225
|
+
partialUsage?: OcxUsage;
|
|
169
226
|
}
|
|
170
227
|
|
|
171
228
|
/**
|
|
@@ -188,7 +245,8 @@ export function mapStreamMessageToEvents(message: StreamMessage, state: StreamPa
|
|
|
188
245
|
if (type === "assistant") {
|
|
189
246
|
// Fallback path: a complete assistant message. Surface text and thinking independently
|
|
190
247
|
// only when the partial delta stream did not already carry them (§十二).
|
|
191
|
-
const
|
|
248
|
+
const messageRecord = asRecord(message.message);
|
|
249
|
+
const content = messageRecord?.content;
|
|
192
250
|
if (Array.isArray(content)) {
|
|
193
251
|
for (const block of content) {
|
|
194
252
|
const part = asRecord(block);
|
|
@@ -200,9 +258,13 @@ export function mapStreamMessageToEvents(message: StreamMessage, state: StreamPa
|
|
|
200
258
|
} else if (blockType === "thinking" && !state.sawPartialThinking) {
|
|
201
259
|
const thinking = asString(part.thinking);
|
|
202
260
|
if (thinking) events.push({ type: "thinking_delta", thinking });
|
|
261
|
+
} else if (blockType === "tool_use") {
|
|
262
|
+
const id = asString(part.id);
|
|
263
|
+
if (!id || !state.partialToolCallIds?.has(id)) state.uncapturedToolUse = true;
|
|
203
264
|
}
|
|
204
265
|
}
|
|
205
266
|
}
|
|
267
|
+
observePartialUsage(state, messageRecord?.usage);
|
|
206
268
|
return events;
|
|
207
269
|
}
|
|
208
270
|
|
|
@@ -281,8 +343,9 @@ function mapRawStreamEvent(event: StreamMessage, state: StreamParseState): Adapt
|
|
|
281
343
|
events.push({ type: "thinking_delta", thinking });
|
|
282
344
|
}
|
|
283
345
|
} else if (deltaType === "input_json_delta") {
|
|
284
|
-
// Tool-input streaming.
|
|
285
|
-
//
|
|
346
|
+
// Tool-input streaming. Live for capture-only bridge turns, where the advertised MCP
|
|
347
|
+
// catalog makes the CLI emit real tool_use blocks; parsed unconditionally so a stray
|
|
348
|
+
// frame on a tools-disabled turn is ignored rather than crashing.
|
|
286
349
|
const partial = asString(delta?.partial_json);
|
|
287
350
|
if (partial && state.openToolCallId) events.push({ type: "tool_call_delta", arguments: partial });
|
|
288
351
|
}
|
|
@@ -296,6 +359,7 @@ function mapRawStreamEvent(event: StreamMessage, state: StreamParseState): Adapt
|
|
|
296
359
|
const name = asString(block?.name) ?? "tool";
|
|
297
360
|
if (id) {
|
|
298
361
|
state.openToolCallId = id;
|
|
362
|
+
state.partialToolCallIds?.add(id);
|
|
299
363
|
events.push({ type: "tool_call_start", id, name });
|
|
300
364
|
}
|
|
301
365
|
}
|
|
@@ -305,14 +369,63 @@ function mapRawStreamEvent(event: StreamMessage, state: StreamParseState): Adapt
|
|
|
305
369
|
if (eventType === "content_block_stop") {
|
|
306
370
|
if (state.openToolCallId) {
|
|
307
371
|
state.openToolCallId = undefined;
|
|
372
|
+
state.completedToolCalls = (state.completedToolCalls ?? 0) + 1;
|
|
308
373
|
events.push({ type: "tool_call_end" });
|
|
309
374
|
}
|
|
310
375
|
return events;
|
|
311
376
|
}
|
|
312
377
|
|
|
378
|
+
if (eventType === "message_stop") {
|
|
379
|
+
state.sawMessageStop = true;
|
|
380
|
+
return events;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (eventType === "message_start") {
|
|
384
|
+
// Anthropic-shaped streams report input tokens on `message_start.message.usage` and output
|
|
385
|
+
// tokens later on `message_delta.usage`. A capture-only tool leg is terminated at
|
|
386
|
+
// `message_stop`, so without this branch the synthesized done(tool_use) undercounts input
|
|
387
|
+
// tokens whenever the CLI puts them here (and `message_stop` arrives before any assistant
|
|
388
|
+
// fallback frame that would otherwise carry them).
|
|
389
|
+
const messageRecord = asRecord(event.message);
|
|
390
|
+
observePartialUsage(state, messageRecord?.usage);
|
|
391
|
+
return events;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (eventType === "message_delta") {
|
|
395
|
+
// Pre-result usage snapshots: a capture-only tool-bridge turn ends at message_stop with no
|
|
396
|
+
// result frame, so these snapshots are the only token accounting that leg will ever see.
|
|
397
|
+
observePartialUsage(state, event.usage);
|
|
398
|
+
return events;
|
|
399
|
+
}
|
|
400
|
+
|
|
313
401
|
return events;
|
|
314
402
|
}
|
|
315
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Validate a `system/init` frame against an active capture-only tool bridge.
|
|
406
|
+
*
|
|
407
|
+
* With the bridge armed, the CLI must report exactly the bridge's MCP server as connected: a
|
|
408
|
+
* missing or failed server means the model never saw the advertised catalog, so the turn fails
|
|
409
|
+
* closed instead of silently degrading to a text-only answer.
|
|
410
|
+
*/
|
|
411
|
+
export function toolBridgeInitError(message: StreamMessage, serverName: string): string | undefined {
|
|
412
|
+
if (message.type !== "system" || message.subtype !== "init") return undefined;
|
|
413
|
+
const servers = message.mcp_servers;
|
|
414
|
+
if (!Array.isArray(servers) || servers.length !== 1) {
|
|
415
|
+
return "Coding-agent system/init reported an unexpected MCP server set for the tool bridge.";
|
|
416
|
+
}
|
|
417
|
+
const server = servers[0];
|
|
418
|
+
if (
|
|
419
|
+
!server
|
|
420
|
+
|| typeof server !== "object"
|
|
421
|
+
|| server.name !== serverName
|
|
422
|
+
|| server.status !== "connected"
|
|
423
|
+
) {
|
|
424
|
+
return `Coding-agent system/init did not report the ${serverName} MCP server as connected.`;
|
|
425
|
+
}
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
|
|
316
429
|
/** One content part on the stream-json input wire (Anthropic message shape). */
|
|
317
430
|
type WireContentPart = Record<string, unknown>;
|
|
318
431
|
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { execFileSync, spawn as nodeSpawn, type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../../types";
|
|
3
6
|
import { commandInvocation } from "../../lib/win-exec";
|
|
7
|
+
import { isStandaloneBinary } from "../../lib/standalone";
|
|
4
8
|
import { modelRecordValue } from "../../reasoning-effort";
|
|
5
9
|
import type { IncomingMeta } from "../base";
|
|
6
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildConversationInput,
|
|
12
|
+
CodingAgentProtocolError,
|
|
13
|
+
mapStreamMessageToEvents,
|
|
14
|
+
projectedHistoryCharLimit,
|
|
15
|
+
readJsonLines,
|
|
16
|
+
toolBridgeInitError,
|
|
17
|
+
type StreamParseState,
|
|
18
|
+
} from "./protocol";
|
|
7
19
|
import { resolveCodingAgentBinary, resolveProfileByBaseUrl, type CodingAgentProviderProfile, type WhichFn } from "./profile";
|
|
8
20
|
|
|
9
21
|
/** Injectable spawn for tests; production uses node:child_process. */
|
|
@@ -26,6 +38,8 @@ export interface CodingAgentDeps {
|
|
|
26
38
|
platform?: NodeJS.Platform;
|
|
27
39
|
/** Test seam for terminating a Windows CLI and all descendants. */
|
|
28
40
|
killWindowsProcessTree?: KillWindowsProcessTreeFn;
|
|
41
|
+
/** Test seam for a catalog/config write failure after private bridge-directory creation. */
|
|
42
|
+
writeToolBridgeFile?: typeof writeFile;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
@@ -88,9 +102,41 @@ export interface CodingAgentTurnInput {
|
|
|
88
102
|
buildArgs: (profile: CodingAgentProviderProfile, parsed: OcxParsedRequest, provider: OcxProviderConfig) => string[];
|
|
89
103
|
/** Family-specific scoped env builder (credential + region switch on top of baseScopedEnv). */
|
|
90
104
|
buildEnv: (profile: CodingAgentProviderProfile, apiKey: string) => Record<string, string>;
|
|
105
|
+
/**
|
|
106
|
+
* Opt-in capture-only tool bridge. When present with a non-empty catalog, the turn writes a
|
|
107
|
+
* validated catalog plus an MCP config to a private temp dir, passes `--mcp-config` (with exact
|
|
108
|
+
* `--allowedTools`) alongside the family's tools-disabled args, translates captured tool_use
|
|
109
|
+
* names back to request wire names, and terminates the process tree at `message_stop` because
|
|
110
|
+
* the capture-only MCP handler intentionally never answers. Execution stays with the client.
|
|
111
|
+
*/
|
|
112
|
+
toolBridge?: CodingAgentToolBridgeInput;
|
|
91
113
|
deps: CodingAgentDeps;
|
|
92
114
|
}
|
|
93
115
|
|
|
116
|
+
/** Opt-in capture-only tool bridge for one coding-agent CLI turn. */
|
|
117
|
+
export interface CodingAgentToolBridgeInput {
|
|
118
|
+
/** MCP server name advertised to the CLI; tool_use blocks render it as `mcp__<name>__<tool>`. */
|
|
119
|
+
serverName: string;
|
|
120
|
+
/** Absolute path of the capture-only MCP server module, run with the serving runtime. */
|
|
121
|
+
serverModulePath: string;
|
|
122
|
+
/** Validated tool catalog advertised over ListTools; the server never executes a call. */
|
|
123
|
+
tools: ReadonlyArray<{ name: string; description: string; inputSchema: Record<string, unknown> }>;
|
|
124
|
+
/** CLI-emitted tool name (`mcp__<server>__<tool>`) to the request's wire tool name. */
|
|
125
|
+
emittedNameMap: Map<string, string>;
|
|
126
|
+
/** Captured tool_use blocks accepted in one assistant message. */
|
|
127
|
+
maxTurnToolCalls: number;
|
|
128
|
+
/**
|
|
129
|
+
* The request's `tool_choice` requires a tool call (`required`, or a named selection).
|
|
130
|
+
* The nested CLI has no documented force-tool flag, so this is enforced locally: a
|
|
131
|
+
* terminal text result on a required turn fails closed instead of silently succeeding.
|
|
132
|
+
*/
|
|
133
|
+
requireToolCall?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function codeBuddyMcpInvocation(serverModulePath: string, catalogPath: string, standalone = isStandaloneBinary()): string[] {
|
|
137
|
+
return standalone ? ["__codebuddy-mcp", catalogPath] : [serverModulePath, catalogPath];
|
|
138
|
+
}
|
|
139
|
+
|
|
94
140
|
/**
|
|
95
141
|
* Run one headless coding-agent CLI turn as an OpenCodex `runTurn` (§七/§三十).
|
|
96
142
|
*
|
|
@@ -153,7 +199,62 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
153
199
|
return;
|
|
154
200
|
}
|
|
155
201
|
|
|
202
|
+
const toolBridge = input.toolBridge;
|
|
203
|
+
const writeToolBridgeFile = deps.writeToolBridgeFile ?? writeFile;
|
|
204
|
+
let toolBridgeDir: string | undefined;
|
|
205
|
+
let toolBridgeMcpConfigPath: string | undefined;
|
|
206
|
+
if (toolBridge) {
|
|
207
|
+
if (toolBridge.tools.length === 0 || toolBridge.emittedNameMap.size === 0) {
|
|
208
|
+
emit({
|
|
209
|
+
type: "error",
|
|
210
|
+
message: "Coding-agent tool bridge was supplied without any isolated tools.",
|
|
211
|
+
status: 500,
|
|
212
|
+
errorType: "server_error",
|
|
213
|
+
code: "tool_bridge_empty",
|
|
214
|
+
retryable: false,
|
|
215
|
+
});
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
toolBridgeDir = await mkdtemp(join(tmpdir(), "ocx-coding-agent-tools-"));
|
|
220
|
+
const catalogPath = join(toolBridgeDir, "catalog.json");
|
|
221
|
+
toolBridgeMcpConfigPath = join(toolBridgeDir, "mcp.json");
|
|
222
|
+
await writeToolBridgeFile(catalogPath, JSON.stringify(toolBridge.tools), { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
223
|
+
await writeToolBridgeFile(
|
|
224
|
+
toolBridgeMcpConfigPath,
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
mcpServers: {
|
|
227
|
+
[toolBridge.serverName]: {
|
|
228
|
+
type: "stdio",
|
|
229
|
+
command: process.execPath,
|
|
230
|
+
args: codeBuddyMcpInvocation(toolBridge.serverModulePath, catalogPath),
|
|
231
|
+
defer_loading: false,
|
|
232
|
+
alwaysLoad: true,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
{ encoding: "utf8", mode: 0o600, flag: "wx" },
|
|
237
|
+
);
|
|
238
|
+
} catch {
|
|
239
|
+
emit({
|
|
240
|
+
type: "error",
|
|
241
|
+
message: "Coding-agent tool bridge could not be staged securely.",
|
|
242
|
+
status: 500,
|
|
243
|
+
errorType: "server_error",
|
|
244
|
+
code: "tool_bridge_setup_failed",
|
|
245
|
+
retryable: false,
|
|
246
|
+
});
|
|
247
|
+
if (toolBridgeDir) await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
156
252
|
const args = buildArgs(profile, parsed, provider);
|
|
253
|
+
if (toolBridge && toolBridgeMcpConfigPath) {
|
|
254
|
+
// Exact names close the wildcard domain; --strict-mcp-config (family args) keeps user
|
|
255
|
+
// servers out, so the capture server is the only capability this turn can reach.
|
|
256
|
+
args.push("--allowedTools", [...toolBridge.emittedNameMap.keys()].join(","), "--mcp-config", toolBridgeMcpConfigPath);
|
|
257
|
+
}
|
|
157
258
|
const env = buildEnv(profile, apiKey);
|
|
158
259
|
const invocation = commandInvocation(binary, args, platform, { env });
|
|
159
260
|
|
|
@@ -174,6 +275,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
174
275
|
code: "cli_spawn_failed",
|
|
175
276
|
retryable: false,
|
|
176
277
|
});
|
|
278
|
+
// A synchronous spawn() throw skips the event-loop `finally` below, so the private
|
|
279
|
+
// bridge dir would leak unless it is removed here as well.
|
|
280
|
+
if (toolBridgeDir) await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
177
281
|
return;
|
|
178
282
|
}
|
|
179
283
|
|
|
@@ -218,6 +322,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
218
322
|
return;
|
|
219
323
|
} catch { /* fall back to terminating the direct child */ }
|
|
220
324
|
}
|
|
325
|
+
// The capture-only MCP server is the CLI's child. Its stdin closes when the CLI dies, and
|
|
326
|
+
// mcp-server.ts exits on stdin EOF, so this ladder reaps the whole tree without knowing
|
|
327
|
+
// the grandchild pid.
|
|
221
328
|
try { child.kill("SIGTERM"); } catch { /* already gone */ }
|
|
222
329
|
killTimer = setTimeout(() => {
|
|
223
330
|
try { child.kill("SIGKILL"); } catch { /* already gone */ }
|
|
@@ -253,11 +360,18 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
253
360
|
|
|
254
361
|
let streamProtocolError: string | undefined;
|
|
255
362
|
let turnError: string | undefined;
|
|
363
|
+
// A successful result frame that arrived after every captured tool call completed but
|
|
364
|
+
// before message_stop. The bridge contract still ends the leg with the synthesized
|
|
365
|
+
// done(tool_use) at message_stop, so the result-derived done(stop) is deferred and its
|
|
366
|
+
// usage (authoritative vendor accounting) is folded into the synthesis. Set inside the
|
|
367
|
+
// stream loop; read by the synthesis and the stream-end error selection below.
|
|
368
|
+
let deferredResultDone: Extract<AdapterEvent, { type: "done" }> | undefined;
|
|
256
369
|
const state: StreamParseState = {
|
|
257
370
|
sawPartialText: false,
|
|
258
371
|
sawPartialThinking: false,
|
|
259
372
|
sawTerminalResult: false,
|
|
260
373
|
openToolCallId: undefined,
|
|
374
|
+
partialToolCallIds: toolBridge ? new Set<string>() : undefined,
|
|
261
375
|
};
|
|
262
376
|
|
|
263
377
|
try {
|
|
@@ -278,13 +392,209 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
278
392
|
const stdout = child.stdout;
|
|
279
393
|
if (!stdout) throw new CodingAgentProtocolError(`${profile.label} CLI produced no stdout stream`);
|
|
280
394
|
try {
|
|
395
|
+
let initValidated = false;
|
|
396
|
+
let toolCallStarts = 0;
|
|
397
|
+
let failClosed = false;
|
|
281
398
|
for await (const message of readJsonLines(stdout)) {
|
|
282
399
|
if (incoming.abortSignal?.aborted) break;
|
|
283
|
-
|
|
400
|
+
if (toolBridge) {
|
|
401
|
+
const initError = toolBridgeInitError(message, toolBridge.serverName);
|
|
402
|
+
if (initError) {
|
|
403
|
+
emitOnce({
|
|
404
|
+
type: "error",
|
|
405
|
+
message: initError,
|
|
406
|
+
status: 502,
|
|
407
|
+
errorType: "upstream_error",
|
|
408
|
+
code: "tool_bridge_init_mismatch",
|
|
409
|
+
retryable: false,
|
|
410
|
+
});
|
|
411
|
+
kill();
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
if (message.type === "system" && message.subtype === "init") initValidated = true;
|
|
415
|
+
}
|
|
416
|
+
const mappedEvents = mapStreamMessageToEvents(message, state);
|
|
417
|
+
if (toolBridge && state.uncapturedToolUse) {
|
|
418
|
+
emitOnce({
|
|
419
|
+
type: "error",
|
|
420
|
+
message: "Coding-agent CLI returned a tool call without a partial tool capture.",
|
|
421
|
+
status: 502,
|
|
422
|
+
errorType: "upstream_error",
|
|
423
|
+
code: "protocol_error",
|
|
424
|
+
retryable: false,
|
|
425
|
+
});
|
|
426
|
+
kill();
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
for (const event of mappedEvents) {
|
|
430
|
+
if (toolBridge && !initValidated && event.type === "done") {
|
|
431
|
+
emitOnce({
|
|
432
|
+
type: "error",
|
|
433
|
+
message: "Coding-agent CLI ended before the tool bridge init handshake completed.",
|
|
434
|
+
status: 502,
|
|
435
|
+
errorType: "upstream_error",
|
|
436
|
+
code: "tool_bridge_init_missing",
|
|
437
|
+
retryable: false,
|
|
438
|
+
});
|
|
439
|
+
failClosed = true;
|
|
440
|
+
kill();
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
if (toolBridge && event.type === "tool_call_start") {
|
|
444
|
+
// The catalog is only advertised once the CLI has acknowledged the bridge server in
|
|
445
|
+
// its init handshake. A tool call that arrives before that acknowledgement means the
|
|
446
|
+
// model acted on a catalog this bridge never validated, so fail closed before the
|
|
447
|
+
// call is counted or renamed. Checking at arrival matters: a later init frame used to
|
|
448
|
+
// set initValidated and let an early call finish as a successful done(tool_use).
|
|
449
|
+
if (!initValidated) {
|
|
450
|
+
emitOnce({
|
|
451
|
+
type: "error",
|
|
452
|
+
message: "Coding-agent CLI called a tool before the tool bridge init handshake completed.",
|
|
453
|
+
status: 502,
|
|
454
|
+
errorType: "upstream_error",
|
|
455
|
+
code: "tool_bridge_init_missing",
|
|
456
|
+
retryable: false,
|
|
457
|
+
});
|
|
458
|
+
failClosed = true;
|
|
459
|
+
kill();
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
toolCallStarts += 1;
|
|
463
|
+
if (toolCallStarts > toolBridge.maxTurnToolCalls) {
|
|
464
|
+
emitOnce({
|
|
465
|
+
type: "error",
|
|
466
|
+
message: `Coding-agent CLI returned more than the ${toolBridge.maxTurnToolCalls}-tool-call turn limit.`,
|
|
467
|
+
status: 502,
|
|
468
|
+
errorType: "upstream_error",
|
|
469
|
+
code: "tool_call_limit",
|
|
470
|
+
retryable: false,
|
|
471
|
+
});
|
|
472
|
+
failClosed = true;
|
|
473
|
+
kill();
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
const wireName = toolBridge.emittedNameMap.get(event.name);
|
|
477
|
+
if (wireName === undefined) {
|
|
478
|
+
emitOnce({
|
|
479
|
+
type: "error",
|
|
480
|
+
message: "Coding-agent CLI called a tool outside the isolated catalog.",
|
|
481
|
+
status: 502,
|
|
482
|
+
errorType: "upstream_error",
|
|
483
|
+
code: "undeclared_tool_call",
|
|
484
|
+
retryable: false,
|
|
485
|
+
});
|
|
486
|
+
failClosed = true;
|
|
487
|
+
kill();
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
emitOnce({ ...event, name: wireName });
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
toolBridge?.requireToolCall === true
|
|
495
|
+
&& !terminalEmitted
|
|
496
|
+
&& event.type === "done"
|
|
497
|
+
&& event.stopReason !== "tool_use"
|
|
498
|
+
&& (state.completedToolCalls ?? 0) === 0
|
|
499
|
+
) {
|
|
500
|
+
// `tool_choice: required|named` on a bridge turn: a text-only terminal result must not
|
|
501
|
+
// become a successful completion the client can accept. The capture-only bridge has no
|
|
502
|
+
// way to force the nested CLI, so fail closed with the same stable error shape the
|
|
503
|
+
// other bridge contract violations use.
|
|
504
|
+
emitOnce({
|
|
505
|
+
type: "error",
|
|
506
|
+
message: "CodeBuddy finished without calling the required tool.",
|
|
507
|
+
status: 502,
|
|
508
|
+
errorType: "upstream_error",
|
|
509
|
+
code: "tool_call_required",
|
|
510
|
+
retryable: false,
|
|
511
|
+
});
|
|
512
|
+
failClosed = true;
|
|
513
|
+
kill();
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
if (
|
|
517
|
+
toolBridge
|
|
518
|
+
&& !terminalEmitted
|
|
519
|
+
&& event.type === "done"
|
|
520
|
+
&& toolCallStarts > 0
|
|
521
|
+
&& (state.completedToolCalls ?? 0) !== toolCallStarts
|
|
522
|
+
) {
|
|
523
|
+
// A terminal result that arrives while a captured tool call is still open must not
|
|
524
|
+
// become a successful completion the client can accept. The message_stop check after
|
|
525
|
+
// the event loop cannot cover this path: the CLI normally parks on the
|
|
526
|
+
// never-answering capture server, but a stream that delivers the result frame
|
|
527
|
+
// without (or before) message_stop emits done here, and started-but-unfinished
|
|
528
|
+
// calls slipped through as successful turns.
|
|
529
|
+
emitOnce({
|
|
530
|
+
type: "error",
|
|
531
|
+
message: "Coding-agent CLI ended with an incomplete tool call.",
|
|
532
|
+
status: 502,
|
|
533
|
+
errorType: "upstream_error",
|
|
534
|
+
code: "protocol_error",
|
|
535
|
+
retryable: false,
|
|
536
|
+
});
|
|
537
|
+
failClosed = true;
|
|
538
|
+
kill();
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
if (
|
|
542
|
+
toolBridge
|
|
543
|
+
&& !terminalEmitted
|
|
544
|
+
&& event.type === "done"
|
|
545
|
+
&& toolCallStarts > 0
|
|
546
|
+
&& (state.completedToolCalls ?? 0) === toolCallStarts
|
|
547
|
+
) {
|
|
548
|
+
// Every captured call completed and the CLI settled with a successful result before
|
|
549
|
+
// message_stop (instead of parking on the never-answering capture server). Emitting
|
|
550
|
+
// this done(stop) now would end the turn as a text completion and skip the
|
|
551
|
+
// synthesized done(tool_use) the client contract expects. Defer it: message_stop
|
|
552
|
+
// synthesis emits the terminal event with this frame's usage, and a stream that
|
|
553
|
+
// ends without message_stop fails closed with protocol_error below.
|
|
554
|
+
deferredResultDone = event;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
284
557
|
emitOnce(event.type === "error"
|
|
285
558
|
? { ...event, message: redactSecrets(event.message, profile.tokenEnv, apiKey) }
|
|
286
559
|
: event);
|
|
287
560
|
}
|
|
561
|
+
if (failClosed) break;
|
|
562
|
+
if (
|
|
563
|
+
toolBridge
|
|
564
|
+
&& !terminalEmitted
|
|
565
|
+
&& state.sawMessageStop
|
|
566
|
+
&& toolCallStarts > 0
|
|
567
|
+
&& (state.completedToolCalls ?? 0) !== toolCallStarts
|
|
568
|
+
) {
|
|
569
|
+
emitOnce({
|
|
570
|
+
type: "error",
|
|
571
|
+
message: "Coding-agent CLI ended with an incomplete tool call.",
|
|
572
|
+
status: 502,
|
|
573
|
+
errorType: "upstream_error",
|
|
574
|
+
code: "protocol_error",
|
|
575
|
+
retryable: false,
|
|
576
|
+
});
|
|
577
|
+
kill();
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
if (toolBridge && !terminalEmitted && state.sawMessageStop && (state.completedToolCalls ?? 0) > 0) {
|
|
581
|
+
// No init re-check here: a completed call implies a tool_call_start was mapped, and the
|
|
582
|
+
// arrival-time gate above already refuses any start that lands before the handshake.
|
|
583
|
+
// The capture-only MCP handler never answers, so the CLI parks after message_stop.
|
|
584
|
+
// The completed tool_use blocks are this turn's structured output: end the leg here
|
|
585
|
+
// and terminate the tree; the client executes, and the next request continues.
|
|
586
|
+
// Pre-result usage snapshots keep this terminated leg accountable: no result frame
|
|
587
|
+
// ever arrives for a turn parked on the never-answering capture server.
|
|
588
|
+
const terminalUsage = deferredResultDone?.usage ?? state.partialUsage;
|
|
589
|
+
emitOnce({
|
|
590
|
+
type: "done",
|
|
591
|
+
stopReason: "tool_use",
|
|
592
|
+
endTurn: false,
|
|
593
|
+
...(terminalUsage ? { usage: terminalUsage } : {}),
|
|
594
|
+
});
|
|
595
|
+
kill();
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
288
598
|
if (terminalEmitted) break;
|
|
289
599
|
}
|
|
290
600
|
} catch (err) {
|
|
@@ -296,6 +606,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
296
606
|
turnError = err instanceof Error ? err.message : String(err);
|
|
297
607
|
} finally {
|
|
298
608
|
cleanup();
|
|
609
|
+
if (toolBridgeDir) {
|
|
610
|
+
await rm(toolBridgeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
611
|
+
}
|
|
299
612
|
}
|
|
300
613
|
|
|
301
614
|
// Reap the process so no zombie is left behind (§三十): wait for the real `close`, and
|
|
@@ -353,6 +666,15 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
|
|
|
353
666
|
code: "process_exit_error",
|
|
354
667
|
retryable: false,
|
|
355
668
|
});
|
|
669
|
+
} else if (toolBridge && deferredResultDone !== undefined && !state.sawMessageStop) {
|
|
670
|
+
emitOnce({
|
|
671
|
+
type: "error",
|
|
672
|
+
message: `${profile.label} CLI delivered a terminal result before message_stop on a tool-bridge turn.`,
|
|
673
|
+
status: 502,
|
|
674
|
+
errorType: "upstream_error",
|
|
675
|
+
code: "protocol_error",
|
|
676
|
+
retryable: false,
|
|
677
|
+
});
|
|
356
678
|
} else if (!state.sawTerminalResult) {
|
|
357
679
|
const msg = stderr
|
|
358
680
|
? `${profile.label} CLI ended without a terminal result frame: ${stderr}`
|