@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923
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-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.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-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AdapterEvent } from "../types";
|
|
3
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
4
|
+
import { validatesRestoredValue } from "./command-code-restored-schema";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MiMo tool-call markup on the Command Code /alpha/generate stream.
|
|
8
|
+
*
|
|
9
|
+
* Xiaomi MiMo writes tool calls in its native chat-template grammar,
|
|
10
|
+
* `<tool_call><function=NAME><parameter=KEY>VALUE</parameter></function></tool_call>`, with string
|
|
11
|
+
* values raw, every other type as JSON, and a freeform tool's input as the raw body with no
|
|
12
|
+
* parameter tags (MiMo-V2.6 chat template; the `mimo` tool parsers in vLLM and SGLang read the same
|
|
13
|
+
* grammar). When Command Code's gateway cannot turn that markup into a call — Codex's freeform
|
|
14
|
+
* `exec` body is raw JavaScript, which fails the gateway's JSON parse of the lowered `{input}`
|
|
15
|
+
* schema — it forwards the markup as a `text-delta` block and then, in the observed case, a native
|
|
16
|
+
* `tool-call` marked `invalid` carrying the same input. Relaying both put the call on screen as
|
|
17
|
+
* assistant text (captured 2026-09-23 from `codex exec` on `xiaomi/mimo-v2.6-flash`).
|
|
18
|
+
* The echo can also omit `</function>` entirely (`<tool_call><function=exec>RAW</parameter></tool_call>`,
|
|
19
|
+
* captured the same day through the live proxy); a parameter-free body is read the same way then.
|
|
20
|
+
* Parameter bodies keep the canonical close, which SGLang's and vLLM's MiMo parsers require too.
|
|
21
|
+
*
|
|
22
|
+
* A text block that opens with `<tool_call>` is therefore held instead of streamed. It is dropped
|
|
23
|
+
* when a native call proves it is a duplicate, or restored on an eligible clean MiMo finish when
|
|
24
|
+
* it names a declared tool with arguments that fit its schema. Other markup is released unchanged.
|
|
25
|
+
* Later text waits behind unresolved markup within the same byte bound.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const TOOL_CALL_MARKER = "<tool_call>";
|
|
29
|
+
/** A held block larger than this is released as text rather than buffered further. */
|
|
30
|
+
export const MAX_HELD_TOOL_TEXT_BYTES = 64 * 1024;
|
|
31
|
+
|
|
32
|
+
export interface CommandCodeDeclaredTool {
|
|
33
|
+
freeform: boolean;
|
|
34
|
+
schema: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export type CommandCodeDeclaredTools = ReadonlyMap<string, CommandCodeDeclaredTool>;
|
|
37
|
+
|
|
38
|
+
export type ToolCallMarkup =
|
|
39
|
+
| { name: string; kind: "raw"; value: string }
|
|
40
|
+
| { name: string; kind: "params"; values: Record<string, string> };
|
|
41
|
+
|
|
42
|
+
/** One wrapping newline on each side is template layout, not value (vLLM `_trim_wrapping_newlines`). */
|
|
43
|
+
function trimWrappingNewlines(value: string): string {
|
|
44
|
+
return value.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const WRAPPER = /^<tool_call>\s*<function=([^>\s]+)>([\s\S]*)<\/tool_call>$/;
|
|
48
|
+
const FUNCTION_CLOSE = /<\/function>\s*$/;
|
|
49
|
+
const PARAMETER = /<parameter=([^>]+)>([\s\S]*?)<\/parameter>/g;
|
|
50
|
+
|
|
51
|
+
/** Parse one complete MiMo tool-call block, or undefined when the text is anything else. */
|
|
52
|
+
export function parseToolCallMarkup(text: string): ToolCallMarkup | undefined {
|
|
53
|
+
const match = WRAPPER.exec(text.trim());
|
|
54
|
+
if (!match) return undefined;
|
|
55
|
+
const name = match[1]!;
|
|
56
|
+
// A trailing </function> is always the close, so a body can keep a literal one only inside a closed block.
|
|
57
|
+
const closed = FUNCTION_CLOSE.test(match[2]!);
|
|
58
|
+
const body = closed ? match[2]!.replace(FUNCTION_CLOSE, "") : match[2]!;
|
|
59
|
+
// A second </tool_call> means two blocks with text between them, never one call.
|
|
60
|
+
if (body.includes(TOOL_CALL_MARKER) || body.includes("</tool_call>") || body.includes("<function=")) return undefined;
|
|
61
|
+
if (!body.includes("<parameter=")) {
|
|
62
|
+
// A parameter-free body is a freeform input. The gateway's echo can close it with a stray
|
|
63
|
+
// `</parameter>` that has no opening tag; that tag is markup, not input.
|
|
64
|
+
return { name, kind: "raw", value: trimWrappingNewlines(body.replace(/<\/parameter>\s*$/, "")) };
|
|
65
|
+
}
|
|
66
|
+
if (!closed) return undefined;
|
|
67
|
+
const values: Record<string, string> = {};
|
|
68
|
+
let consumed = "";
|
|
69
|
+
for (const parameter of body.matchAll(PARAMETER)) {
|
|
70
|
+
const key = parameter[1]!.trim();
|
|
71
|
+
if (!key || Object.hasOwn(values, key)) return undefined;
|
|
72
|
+
values[key] = trimWrappingNewlines(parameter[2]!);
|
|
73
|
+
consumed += parameter[0];
|
|
74
|
+
}
|
|
75
|
+
// Complete means every byte of the body belongs to a parameter; anything left over is prose.
|
|
76
|
+
if (body.replace(PARAMETER, "").trim() !== "" || consumed === "") return undefined;
|
|
77
|
+
return { name, kind: "params", values };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tryJson(value: string): unknown {
|
|
81
|
+
try { return JSON.parse(value); } catch { return undefined; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function deepEqual(left: unknown, right: unknown): boolean {
|
|
85
|
+
if (Object.is(left, right)) return true;
|
|
86
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) return false;
|
|
87
|
+
if (Array.isArray(left) !== Array.isArray(right)) return false;
|
|
88
|
+
const leftKeys = Object.keys(left);
|
|
89
|
+
const rightKeys = Object.keys(right);
|
|
90
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
91
|
+
return leftKeys.every(key => Object.hasOwn(right, key)
|
|
92
|
+
&& deepEqual((left as Record<string, unknown>)[key], (right as Record<string, unknown>)[key]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a parsed block encodes exactly the input of a native call. */
|
|
96
|
+
export function markupMatchesInput(markup: ToolCallMarkup, input: unknown): boolean {
|
|
97
|
+
const value = typeof input === "string" && markup.kind === "params" ? tryJson(input) : input;
|
|
98
|
+
if (markup.kind === "raw") {
|
|
99
|
+
if (typeof value === "string") return value.trim() === markup.value.trim();
|
|
100
|
+
// A valid freeform call arrives as its lowered single-key object.
|
|
101
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
102
|
+
const entries = Object.entries(value);
|
|
103
|
+
return entries.length === 1 && typeof entries[0]![1] === "string"
|
|
104
|
+
&& (entries[0]![1] as string).trim() === markup.value.trim();
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
109
|
+
const record = value as Record<string, unknown>;
|
|
110
|
+
const keys = Object.keys(record);
|
|
111
|
+
if (keys.length !== Object.keys(markup.values).length) return false;
|
|
112
|
+
return keys.every(key => {
|
|
113
|
+
if (!Object.hasOwn(markup.values, key)) return false;
|
|
114
|
+
const raw = markup.values[key]!;
|
|
115
|
+
const expected = record[key];
|
|
116
|
+
if (typeof expected === "string") return expected === raw;
|
|
117
|
+
const decoded = tryJson(raw);
|
|
118
|
+
return decoded !== undefined && deepEqual(decoded, expected);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function schemaTypes(schema: unknown): string[] | undefined {
|
|
123
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
124
|
+
const record = schema as Record<string, unknown>;
|
|
125
|
+
if (typeof record.type === "string") return [record.type];
|
|
126
|
+
if (Array.isArray(record.type)) return record.type.filter((entry): entry is string => typeof entry === "string");
|
|
127
|
+
const alternatives = Array.isArray(record.anyOf) ? record.anyOf : Array.isArray(record.oneOf) ? record.oneOf : undefined;
|
|
128
|
+
if (!alternatives) return undefined;
|
|
129
|
+
const types = alternatives.flatMap(entry => schemaTypes(entry) ?? []);
|
|
130
|
+
return types.length > 0 ? types : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const DECODE_FAILED = Symbol("decode-failed");
|
|
134
|
+
|
|
135
|
+
function decodeTyped(raw: string, type: string): unknown {
|
|
136
|
+
switch (type) {
|
|
137
|
+
case "string": return raw;
|
|
138
|
+
case "integer": {
|
|
139
|
+
// An integer past 2^53 would serialize as a different number (or null once it overflows).
|
|
140
|
+
const parsed = /^-?\d+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
|
|
141
|
+
return Number.isSafeInteger(parsed) ? parsed : DECODE_FAILED;
|
|
142
|
+
}
|
|
143
|
+
case "number": {
|
|
144
|
+
const trimmed = raw.trim();
|
|
145
|
+
const parsed = trimmed === "" ? Number.NaN : Number(trimmed);
|
|
146
|
+
return Number.isFinite(parsed) ? parsed : DECODE_FAILED;
|
|
147
|
+
}
|
|
148
|
+
case "boolean": return raw.trim() === "true" ? true : raw.trim() === "false" ? false : DECODE_FAILED;
|
|
149
|
+
case "null": return raw.trim() === "null" ? null : DECODE_FAILED;
|
|
150
|
+
case "object": {
|
|
151
|
+
const parsed = tryJson(raw);
|
|
152
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : DECODE_FAILED;
|
|
153
|
+
}
|
|
154
|
+
case "array": {
|
|
155
|
+
const parsed = tryJson(raw);
|
|
156
|
+
return Array.isArray(parsed) ? parsed : DECODE_FAILED;
|
|
157
|
+
}
|
|
158
|
+
default: return DECODE_FAILED;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Decode one parameter by its declared schema; a value that fits no declared type fails. */
|
|
163
|
+
function decodeParameter(raw: string, schema: unknown): unknown {
|
|
164
|
+
const types = schemaTypes(schema);
|
|
165
|
+
if (!types) {
|
|
166
|
+
const parsed = tryJson(raw);
|
|
167
|
+
if (parsed !== undefined && validatesRestoredValue(parsed, schema)) return parsed;
|
|
168
|
+
return validatesRestoredValue(raw, schema) ? raw : DECODE_FAILED;
|
|
169
|
+
}
|
|
170
|
+
// Non-string types first: MiMo writes them as JSON, and a string type would accept anything.
|
|
171
|
+
const ordered = [...types.filter(type => type !== "string"), ...types.filter(type => type === "string")];
|
|
172
|
+
for (const type of ordered) {
|
|
173
|
+
const decoded = decodeTyped(raw, type);
|
|
174
|
+
if (decoded !== DECODE_FAILED && validatesRestoredValue(decoded, schema)) return decoded;
|
|
175
|
+
}
|
|
176
|
+
return DECODE_FAILED;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Arguments for a call restored from markup, or undefined when the markup does not fit the
|
|
181
|
+
* declared tool. A freeform tool takes a parameter-free body as its single lowered string field; a
|
|
182
|
+
* function tool takes parameters that include every required key, name only declared keys, and
|
|
183
|
+
* decode to their declared types.
|
|
184
|
+
*/
|
|
185
|
+
export function salvagedArguments(markup: ToolCallMarkup, tool: CommandCodeDeclaredTool): string | undefined {
|
|
186
|
+
const properties = tool.schema.properties && typeof tool.schema.properties === "object" && !Array.isArray(tool.schema.properties)
|
|
187
|
+
? tool.schema.properties as Record<string, unknown>
|
|
188
|
+
: {};
|
|
189
|
+
const required = Array.isArray(tool.schema.required)
|
|
190
|
+
? tool.schema.required.filter((entry): entry is string => typeof entry === "string")
|
|
191
|
+
: [];
|
|
192
|
+
if (tool.freeform) {
|
|
193
|
+
if (markup.kind !== "raw") return undefined;
|
|
194
|
+
const keys = Object.keys(properties);
|
|
195
|
+
const output = { [keys.length === 1 ? keys[0]! : "input"]: markup.value };
|
|
196
|
+
return validatesRestoredValue(output, tool.schema) ? JSON.stringify(output) : undefined;
|
|
197
|
+
}
|
|
198
|
+
if (markup.kind !== "params") return undefined;
|
|
199
|
+
const output: Record<string, unknown> = {};
|
|
200
|
+
for (const [key, raw] of Object.entries(markup.values)) {
|
|
201
|
+
if (!Object.hasOwn(properties, key)) return undefined;
|
|
202
|
+
const decoded = decodeParameter(raw, properties[key]);
|
|
203
|
+
if (decoded === DECODE_FAILED) return undefined;
|
|
204
|
+
output[key] = decoded;
|
|
205
|
+
}
|
|
206
|
+
if (required.some(key => !Object.hasOwn(output, key))) return undefined;
|
|
207
|
+
return validatesRestoredValue(output, tool.schema) ? JSON.stringify(output) : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface TextBlock {
|
|
211
|
+
id: string;
|
|
212
|
+
markupParts: string[];
|
|
213
|
+
probe: string;
|
|
214
|
+
bytes: number;
|
|
215
|
+
state: "probing" | "held" | "queued" | "dropped" | "streaming";
|
|
216
|
+
ended: boolean;
|
|
217
|
+
interrupted: boolean;
|
|
218
|
+
/** Tool inputs open when the block started; the native call that duplicates it is one of them. */
|
|
219
|
+
candidates: Set<string>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
interface TextChunk {
|
|
223
|
+
kind: "chunk";
|
|
224
|
+
block: TextBlock;
|
|
225
|
+
parts: string[];
|
|
226
|
+
bytes: number;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface NativeCall {
|
|
230
|
+
kind: "native";
|
|
231
|
+
id: string;
|
|
232
|
+
name: string;
|
|
233
|
+
argumentsText: string;
|
|
234
|
+
/** Retained only while an earlier text block blocks delivery. */
|
|
235
|
+
bytes: number;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface QueuedEvent {
|
|
239
|
+
kind: "event";
|
|
240
|
+
event: AdapterEvent;
|
|
241
|
+
bytes: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface Terminal { kind: "finish"; restore: boolean }
|
|
245
|
+
type Pending = TextChunk | NativeCall | QueuedEvent | Terminal;
|
|
246
|
+
|
|
247
|
+
const DEFAULT_TEXT_ID = "\u0000default";
|
|
248
|
+
const encoder = new TextEncoder();
|
|
249
|
+
|
|
250
|
+
/** Stream-side state for one Command Code response. */
|
|
251
|
+
export class CommandCodeToolTextFilter {
|
|
252
|
+
private readonly openInputs = new Map<string, string>();
|
|
253
|
+
private readonly blocks = new Map<string, TextBlock>();
|
|
254
|
+
/** Only nonempty blocks still deciding whether their text is markup need boundary visits. */
|
|
255
|
+
private readonly activeProbes = new Map<string, TextBlock>();
|
|
256
|
+
/** Held blocks in arrival order, including ended ones awaiting a verdict. */
|
|
257
|
+
private held: TextBlock[] = [];
|
|
258
|
+
/** Every output-bearing event shares this wire-order queue. */
|
|
259
|
+
private pending: Pending[] = [];
|
|
260
|
+
private head = 0;
|
|
261
|
+
private queuedBytes = 0;
|
|
262
|
+
private queueOperations = 0;
|
|
263
|
+
|
|
264
|
+
constructor(
|
|
265
|
+
private readonly budget: TranslatorBudget,
|
|
266
|
+
private readonly declared: CommandCodeDeclaredTools | undefined,
|
|
267
|
+
) {}
|
|
268
|
+
|
|
269
|
+
/** Counts queue item visits and appends for the bounded-work regression. */
|
|
270
|
+
queueOperationsForTest(): number { return this.queueOperations; }
|
|
271
|
+
openBlockCountForTest(): number { return this.blocks.size; }
|
|
272
|
+
|
|
273
|
+
toolInputStart(id: unknown, name: unknown): AdapterEvent[] {
|
|
274
|
+
const events = this.breakOpenBlocks();
|
|
275
|
+
if (typeof id === "string" && typeof name === "string") this.openInputs.set(id, name);
|
|
276
|
+
return events;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
boundary(): AdapterEvent[] { return this.breakOpenBlocks(); }
|
|
280
|
+
|
|
281
|
+
private breakOpenBlocks(exceptKey?: string): AdapterEvent[] {
|
|
282
|
+
let changed = false;
|
|
283
|
+
for (const [key, block] of this.activeProbes) {
|
|
284
|
+
this.queueOperations++;
|
|
285
|
+
if (key === exceptKey) continue;
|
|
286
|
+
this.activeProbes.delete(key);
|
|
287
|
+
block.interrupted = true;
|
|
288
|
+
block.state = "queued";
|
|
289
|
+
block.markupParts = [];
|
|
290
|
+
changed = true;
|
|
291
|
+
}
|
|
292
|
+
if (changed) this.held = this.held.filter(block => block.state === "held");
|
|
293
|
+
return changed ? this.drain() : [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
textStart(id: unknown): AdapterEvent[] {
|
|
297
|
+
const key = typeof id === "string" ? id : DEFAULT_TEXT_ID;
|
|
298
|
+
const events = this.blocks.has(key) ? this.textEnd(key) : [];
|
|
299
|
+
events.push(...this.breakOpenBlocks(key));
|
|
300
|
+
const block: TextBlock = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
|
|
301
|
+
this.blocks.set(key, block);
|
|
302
|
+
return events;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
textDelta(id: unknown, text: string): AdapterEvent[] {
|
|
306
|
+
const key = typeof id === "string" ? id : DEFAULT_TEXT_ID;
|
|
307
|
+
const boundaryEvents = this.breakOpenBlocks(key);
|
|
308
|
+
let block = this.blocks.get(key);
|
|
309
|
+
if (!block) {
|
|
310
|
+
block = { id: key, markupParts: [], probe: "", bytes: 0, state: "probing", ended: false, interrupted: false, candidates: new Set(this.openInputs.keys()) };
|
|
311
|
+
this.blocks.set(key, block);
|
|
312
|
+
}
|
|
313
|
+
if (block.state === "streaming" && this.head === this.pending.length) {
|
|
314
|
+
return [...boundaryEvents, { type: "text_delta", text }];
|
|
315
|
+
}
|
|
316
|
+
// Once a duplicate is dropped, later text is a new chunk at its own wire position.
|
|
317
|
+
if (block.state === "dropped" || block.state === "streaming") {
|
|
318
|
+
block = { id: key, markupParts: [], probe: "", bytes: 0, state: "queued", ended: false, interrupted: true, candidates: new Set() };
|
|
319
|
+
this.blocks.set(key, block);
|
|
320
|
+
}
|
|
321
|
+
const preceding = this.makeRoom(encoder.encode(text).byteLength);
|
|
322
|
+
this.retain(block, text);
|
|
323
|
+
if (block.state === "probing" || block.state === "held") this.activeProbes.set(key, block);
|
|
324
|
+
const bytes = encoder.encode(text).byteLength;
|
|
325
|
+
const tail = this.pending.at(-1);
|
|
326
|
+
if (tail?.kind === "chunk" && tail.block === block && this.head < this.pending.length) {
|
|
327
|
+
tail.parts.push(text);
|
|
328
|
+
tail.bytes += bytes;
|
|
329
|
+
this.queueOperations++;
|
|
330
|
+
} else {
|
|
331
|
+
this.pending.push({ kind: "chunk", block, parts: [text], bytes });
|
|
332
|
+
this.queueOperations++;
|
|
333
|
+
}
|
|
334
|
+
this.queuedBytes += bytes;
|
|
335
|
+
if (block.state === "probing") {
|
|
336
|
+
for (const char of text) {
|
|
337
|
+
if (!block.probe && char.trim() === "") continue;
|
|
338
|
+
block.probe += char;
|
|
339
|
+
if (!TOOL_CALL_MARKER.startsWith(block.probe)) {
|
|
340
|
+
block.state = "queued";
|
|
341
|
+
block.markupParts = [];
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
if (block.probe === TOOL_CALL_MARKER) {
|
|
345
|
+
block.state = "held";
|
|
346
|
+
block.probe = "";
|
|
347
|
+
this.held.push(block);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (block.state !== "probing" && block.state !== "held") this.activeProbes.delete(key);
|
|
353
|
+
return [...boundaryEvents, ...preceding, ...this.limitPending()];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
textEnd(id: unknown): AdapterEvent[] {
|
|
357
|
+
const key = typeof id === "string" ? id : DEFAULT_TEXT_ID;
|
|
358
|
+
const block = this.blocks.get(key);
|
|
359
|
+
if (!block) return [];
|
|
360
|
+
this.blocks.delete(key);
|
|
361
|
+
this.activeProbes.delete(key);
|
|
362
|
+
block.ended = true;
|
|
363
|
+
// A block that never committed to the marker (whitespace, or a marker prefix) is ordinary text.
|
|
364
|
+
if (block.state === "probing") {
|
|
365
|
+
block.state = "queued";
|
|
366
|
+
block.markupParts = [];
|
|
367
|
+
return this.drain();
|
|
368
|
+
}
|
|
369
|
+
if (block.state === "queued") return this.drain();
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Called before a native call is relayed; returns text that must precede it. */
|
|
374
|
+
toolCall(id: string, name: string, input: unknown): AdapterEvent[] {
|
|
375
|
+
this.matchNative(id, name, input);
|
|
376
|
+
return this.drain();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Put the native call at its wire position, after matching any earlier held markup. */
|
|
380
|
+
nativeCall(id: string, name: string, input: unknown): AdapterEvent[] {
|
|
381
|
+
const boundaryEvents = this.breakOpenBlocks();
|
|
382
|
+
this.matchNative(id, name, input);
|
|
383
|
+
const argumentsText = typeof input === "string" ? input : JSON.stringify(input);
|
|
384
|
+
const preceding = this.makeRoom(encoder.encode(argumentsText).byteLength);
|
|
385
|
+
const bytes = this.head < this.pending.length ? encoder.encode(argumentsText).byteLength : 0;
|
|
386
|
+
if (bytes > 0) this.retainQueued(bytes);
|
|
387
|
+
this.pending.push({ kind: "native", id, name, argumentsText, bytes });
|
|
388
|
+
this.queueOperations++;
|
|
389
|
+
this.queuedBytes += bytes;
|
|
390
|
+
return [...boundaryEvents, ...preceding, ...this.limitPending()];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** Reasoning shares the same ordering barrier as text and native calls. */
|
|
394
|
+
enqueueEvent(event: AdapterEvent, textValue: string): AdapterEvent[] {
|
|
395
|
+
const boundaryEvents = this.breakOpenBlocks();
|
|
396
|
+
const preceding = this.makeRoom(encoder.encode(textValue).byteLength);
|
|
397
|
+
const bytes = this.head < this.pending.length ? encoder.encode(textValue).byteLength : 0;
|
|
398
|
+
if (bytes > 0) this.retainQueued(bytes);
|
|
399
|
+
this.pending.push({ kind: "event", event, bytes });
|
|
400
|
+
this.queueOperations++;
|
|
401
|
+
this.queuedBytes += bytes;
|
|
402
|
+
return [...boundaryEvents, ...preceding, ...this.limitPending()];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private matchNative(id: string, name: string, input: unknown): void {
|
|
406
|
+
this.openInputs.delete(id);
|
|
407
|
+
const remaining: TextBlock[] = [];
|
|
408
|
+
for (const block of this.held) {
|
|
409
|
+
const pairs = block.candidates.size === 0 || block.candidates.has(id);
|
|
410
|
+
if (!pairs) {
|
|
411
|
+
remaining.push(block);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const markup = parseToolCallMarkup(block.markupParts.join(""));
|
|
415
|
+
if (markup && markup.name === name && markupMatchesInput(markup, input)) {
|
|
416
|
+
this.drop(block);
|
|
417
|
+
block.state = "dropped";
|
|
418
|
+
this.activeProbes.delete(block.id);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
block.candidates.delete(id);
|
|
422
|
+
if (block.candidates.size === 0) {
|
|
423
|
+
block.state = "queued";
|
|
424
|
+
block.markupParts = [];
|
|
425
|
+
this.activeProbes.delete(block.id);
|
|
426
|
+
} else {
|
|
427
|
+
remaining.push(block);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this.held = remaining;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Release every held block as text, without restoring any call (used when the turn failed). */
|
|
434
|
+
releaseAll(): AdapterEvent[] {
|
|
435
|
+
return this.settle(false).events;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Terminal verdict for every block still held: restore it as a call when it qualifies, else release it. */
|
|
439
|
+
finish(): { events: AdapterEvent[]; salvaged: boolean } {
|
|
440
|
+
return this.settle(true);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private settle(restore: boolean): { events: AdapterEvent[]; salvaged: boolean } {
|
|
444
|
+
const events: AdapterEvent[] = [];
|
|
445
|
+
let salvaged = false;
|
|
446
|
+
let lastTextBlock: TextBlock | undefined;
|
|
447
|
+
this.pending.push({ kind: "finish", restore });
|
|
448
|
+
const pending = this.pending.slice(this.head);
|
|
449
|
+
this.queueOperations += pending.length;
|
|
450
|
+
this.held = [];
|
|
451
|
+
this.pending = [];
|
|
452
|
+
this.head = 0;
|
|
453
|
+
this.queuedBytes = 0;
|
|
454
|
+
this.blocks.clear();
|
|
455
|
+
this.activeProbes.clear();
|
|
456
|
+
for (const item of pending) {
|
|
457
|
+
this.queueOperations++;
|
|
458
|
+
if (item.kind === "finish") break;
|
|
459
|
+
if (item.kind === "native") { events.push(...this.emitNative(item)); lastTextBlock = undefined; continue; }
|
|
460
|
+
if (item.kind === "event") { this.releaseQueued(item.bytes); events.push(item.event); lastTextBlock = undefined; continue; }
|
|
461
|
+
const block = item.block;
|
|
462
|
+
if (block.state === "held") {
|
|
463
|
+
const markup = restore && !block.interrupted ? parseToolCallMarkup(block.markupParts.join("")) : undefined;
|
|
464
|
+
const tool = markup ? this.declared?.get(markup.name) : undefined;
|
|
465
|
+
const args = markup && tool ? salvagedArguments(markup, tool) : undefined;
|
|
466
|
+
if (markup && args !== undefined) {
|
|
467
|
+
this.drop(block);
|
|
468
|
+
block.state = "dropped";
|
|
469
|
+
const callId = `call_ocx_${randomUUID().replace(/-/g, "")}`;
|
|
470
|
+
events.push({ type: "tool_call_start", id: callId, name: markup.name });
|
|
471
|
+
events.push({ type: "tool_call_delta", arguments: args });
|
|
472
|
+
events.push({ type: "tool_call_end" });
|
|
473
|
+
salvaged = true;
|
|
474
|
+
lastTextBlock = undefined;
|
|
475
|
+
} else {
|
|
476
|
+
block.state = "queued";
|
|
477
|
+
block.markupParts = [];
|
|
478
|
+
}
|
|
479
|
+
} else if (block.state === "probing") {
|
|
480
|
+
block.state = "queued";
|
|
481
|
+
block.markupParts = [];
|
|
482
|
+
}
|
|
483
|
+
if (block.state === "dropped") {
|
|
484
|
+
lastTextBlock = undefined;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const emitted = this.releaseChunk(item);
|
|
488
|
+
if (emitted && lastTextBlock === block && events.at(-1)?.type === "text_delta") {
|
|
489
|
+
(events.at(-1) as { type: "text_delta"; text: string }).text += emitted.text;
|
|
490
|
+
} else if (emitted) events.push(emitted);
|
|
491
|
+
lastTextBlock = block;
|
|
492
|
+
}
|
|
493
|
+
return { events, salvaged };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private retain(block: TextBlock, text: string): void {
|
|
497
|
+
const bytes = encoder.encode(text).byteLength;
|
|
498
|
+
const reservation = this.budget.reserveTransient(bytes, { kind: "live_transient" });
|
|
499
|
+
reservation.commitRetained();
|
|
500
|
+
if (block.state === "probing" || block.state === "held") block.markupParts.push(text);
|
|
501
|
+
block.bytes += bytes;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private drop(block: TextBlock): void {
|
|
505
|
+
this.budget.releaseRetained(block.bytes, { kind: "live_transient" });
|
|
506
|
+
this.queuedBytes = Math.max(0, this.queuedBytes - block.bytes);
|
|
507
|
+
block.bytes = 0;
|
|
508
|
+
block.markupParts = [];
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private releaseChunk(chunk: TextChunk): { type: "text_delta"; text: string } | undefined {
|
|
512
|
+
if (chunk.block.state === "dropped") return undefined;
|
|
513
|
+
this.budget.releaseRetained(chunk.bytes, { kind: "live_transient" });
|
|
514
|
+
this.queuedBytes = Math.max(0, this.queuedBytes - chunk.bytes);
|
|
515
|
+
chunk.block.bytes = Math.max(0, chunk.block.bytes - chunk.bytes);
|
|
516
|
+
const text = chunk.parts.join("");
|
|
517
|
+
return text ? { type: "text_delta", text } : undefined;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private retainQueued(bytes: number): void {
|
|
521
|
+
this.budget.reserveTransient(bytes, { kind: "live_transient" }).commitRetained();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private releaseQueued(bytes: number): void {
|
|
525
|
+
if (bytes > 0) {
|
|
526
|
+
this.budget.releaseRetained(bytes, { kind: "live_transient" });
|
|
527
|
+
this.queuedBytes = Math.max(0, this.queuedBytes - bytes);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private emitNative(call: NativeCall): AdapterEvent[] {
|
|
532
|
+
this.releaseQueued(call.bytes);
|
|
533
|
+
return [
|
|
534
|
+
{ type: "tool_call_start", id: call.id, name: call.name },
|
|
535
|
+
{ type: "tool_call_delta", arguments: call.argumentsText },
|
|
536
|
+
{ type: "tool_call_end" },
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private drain(): AdapterEvent[] {
|
|
541
|
+
const events: AdapterEvent[] = [];
|
|
542
|
+
let lastTextBlock: TextBlock | undefined;
|
|
543
|
+
while (this.head < this.pending.length) {
|
|
544
|
+
const item = this.pending[this.head]!;
|
|
545
|
+
this.queueOperations++;
|
|
546
|
+
if (item.kind === "finish" || (item.kind === "chunk" && (item.block.state === "held" || item.block.state === "probing"))) break;
|
|
547
|
+
this.head++;
|
|
548
|
+
if (item.kind === "chunk") {
|
|
549
|
+
const emitted = this.releaseChunk(item);
|
|
550
|
+
if (emitted && lastTextBlock === item.block && events.at(-1)?.type === "text_delta") {
|
|
551
|
+
(events.at(-1) as { type: "text_delta"; text: string }).text += emitted.text;
|
|
552
|
+
} else if (emitted) events.push(emitted);
|
|
553
|
+
if (item.block.state === "queued" && item.block.bytes === 0 && !item.block.ended) item.block.state = "streaming";
|
|
554
|
+
lastTextBlock = item.block.state === "dropped" ? undefined : item.block;
|
|
555
|
+
} else if (item.kind === "native") { events.push(...this.emitNative(item)); lastTextBlock = undefined; }
|
|
556
|
+
else { this.releaseQueued(item.bytes); events.push(item.event); lastTextBlock = undefined; }
|
|
557
|
+
}
|
|
558
|
+
if (this.head === this.pending.length) { this.pending = []; this.head = 0; }
|
|
559
|
+
else if (this.head >= 1024 && this.head * 2 >= this.pending.length) {
|
|
560
|
+
this.queueOperations += this.pending.length - this.head;
|
|
561
|
+
this.pending = this.pending.slice(this.head);
|
|
562
|
+
this.head = 0;
|
|
563
|
+
}
|
|
564
|
+
return events;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private limitPending(): AdapterEvent[] {
|
|
568
|
+
if (this.queuedBytes <= MAX_HELD_TOOL_TEXT_BYTES) return this.drain();
|
|
569
|
+
return this.flushPendingAsText();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private makeRoom(additionalBytes: number): AdapterEvent[] {
|
|
573
|
+
return this.queuedBytes + additionalBytes > MAX_HELD_TOOL_TEXT_BYTES ? this.flushPendingAsText() : [];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
private flushPendingAsText(): AdapterEvent[] {
|
|
577
|
+
// Drop restoration once the ordered queue fills, then release all text in arrival order.
|
|
578
|
+
this.held = [];
|
|
579
|
+
this.activeProbes.clear();
|
|
580
|
+
for (let index = this.head; index < this.pending.length; index++) {
|
|
581
|
+
const item = this.pending[index]!;
|
|
582
|
+
this.queueOperations++;
|
|
583
|
+
if (item.kind !== "chunk" || (item.block.state !== "held" && item.block.state !== "probing")) continue;
|
|
584
|
+
item.block.state = "queued";
|
|
585
|
+
item.block.markupParts = [];
|
|
586
|
+
}
|
|
587
|
+
return this.drain();
|
|
588
|
+
}
|
|
589
|
+
}
|